Factory Configuration#
Factories can be configured by setting special dunder (double underscore) class attributes.
You can read the reference for these in the API reference for
BaseFactory
. Below we discuss some configuration options in some depth.
Seeding Randomness#
from dataclasses import dataclass
from polyfactory.factories import DataclassFactory
@dataclass
class Person:
name: str
age: float
height: float
weight: float
class PersonFactory(DataclassFactory[Person]):
__random_seed__ = 1
@classmethod
def name(cls) -> str:
return cls.__random__.choice(["John", "Alice", "George"])
def test_random_seed() -> None:
# the outcome of 'factory.__random__.choice' is deterministic, because Random has been seeded with a set value.
assert PersonFactory.build().name == "John"
assert PersonFactory.build().name == "George"
assert PersonFactory.build().name == "John"
Seeding randomness allows you to control the random generation of values produced by the factory. This affects all random.Random
methods as well as faker.
Setting Random#
from dataclasses import dataclass
from random import Random
from polyfactory.factories import DataclassFactory
@dataclass
class Person:
name: str
age: float
height: float
weight: float
class PersonFactory(DataclassFactory[Person]):
__random__ = Random(10)
@classmethod
def name(cls) -> str:
return cls.__random__.choice(["John", "Alice", "George"])
def test_setting_random() -> None:
# the outcome of 'factory.__random__.choice' is deterministic, because Random is configured with a set value.
assert PersonFactory.build().name == "George"
assert PersonFactory.build().name == "John"
assert PersonFactory.build().name == "Alice"
This configuration option is functionally identical to the previous, with the difference being that here we are setting
the actual instance of random.Random
. This is useful when embedding factories inside more complex logic, such as in
other libraries, as well as when factories are being dynamically generated.
Setting Faker#
from dataclasses import dataclass
from faker import Faker
from polyfactory.factories import DataclassFactory
@dataclass
class Person:
name: str
age: float
height: float
weight: float
class PersonFactory(DataclassFactory[Person]):
__faker__ = Faker(locale="es_ES")
__random_seed__ = 10
@classmethod
def name(cls) -> str:
return cls.__faker__.name()
def test_setting_faker() -> None:
# the outcome of faker deterministic because we seeded random, and it uses a spanish locale.
assert PersonFactory.build().name == "Alejandra Romeu-Tolosa"
In the above example we are setting the factory’s instance of Faker
and configure its locale to Spanish. Because
we are also setting the random seed value, the results of the test are deterministic.
Note
To understand why we are using a classmethod here, see the documentation about factory fields.
Persistence Handlers#
Factory classes have four optional persistence methods:
.create_sync(**kwargs)
- builds and persists a single instance of the factory’s model synchronously.create_batch_sync(size: int, **kwargs)
- builds and persists a list of size n instances synchronously.create_async(**kwargs)
- builds and persists a single instance of the factory’s model asynchronously.create_batch_async(size: int, **kwargs)
- builds and persists a list of size n instances asynchronously
To use these methods, you must first specify a sync and/or async persistence handlers for the factory:
from asyncio import sleep
from dataclasses import dataclass
from typing import Dict, List
from uuid import UUID
from polyfactory import AsyncPersistenceProtocol, SyncPersistenceProtocol
from polyfactory.factories import DataclassFactory
@dataclass
class Person:
id: UUID
name: str
# we will use a dictionary to persist values for the example
mock_db: Dict[UUID, Person] = {}
class SyncPersistenceHandler(SyncPersistenceProtocol[Person]):
def save(self, data: Person) -> Person:
# do stuff here to persist the value, such as use an ORM or ODM, cache in redis etc.
# in our case we simply save it in the dictionary.
mock_db[data.id] = data
return data
def save_many(self, data: List[Person]) -> List[Person]:
# same as for save, here we should store the list in persistence.
# in this case, we use the same dictionary.
for person in data:
mock_db[person.id] = person
return data
class AsyncPersistenceHandler(AsyncPersistenceProtocol[Person]):
async def save(self, data: Person) -> Person:
# do stuff here to persist the value using an async method, such as an async ORM or ODM.
# in our case we simply save it in the dictionary and add a minimal sleep to mock async.
mock_db[data.id] = data
await sleep(0.0001)
return data
async def save_many(self, data: List[Person]) -> List[Person]:
# same as for the async save, here we should store the list in persistence using async logic.
# we again store in dict, and mock async using sleep.
for person in data:
mock_db[person.id] = person
await sleep(0.0001)
return data
class PersonFactory(DataclassFactory[Person]):
__sync_persistence__ = SyncPersistenceHandler
__async_persistence__ = AsyncPersistenceHandler
def test_sync_persistence_build() -> None:
person_instance = PersonFactory.create_sync()
assert mock_db[person_instance.id] is person_instance
def test_sync_persistence_batch() -> None:
person_batch = PersonFactory.create_batch_sync(10)
for person_instance in person_batch:
assert mock_db[person_instance.id] is person_instance
async def test_async_persistence_build() -> None:
person_instance = await PersonFactory.create_async()
assert mock_db[person_instance.id] is person_instance
async def test_async_persistence_batch() -> None:
person_batch = await PersonFactory.create_batch_async(10)
for person_instance in person_batch:
assert mock_db[person_instance.id] is person_instance
With the persistence handlers in place, you can now use all persistence methods.
Note
You do not need to define both persistence handlers. If you will only use sync or async persistence, you only need to define the respective handler to use these methods.
Defining Default Factories#
As explained in the section about dynamic factory generation in declaring factories, factories generate new factories for supported types dynamically. This process requires no intervention from the user. Once a factory is generated, it is then cached and reused - when the same type is used.
For example, when build is called for the PersonFactory
below, a PetFactory
will be dynamically generated and reused:
from dataclasses import dataclass
from datetime import date, datetime
from enum import Enum
from typing import Any, Dict, List, Union
from uuid import UUID
from polyfactory.factories import DataclassFactory
class Species(str, Enum):
CAT = "Cat"
DOG = "Dog"
@dataclass
class Pet:
name: str
species: Species
sound: str
@dataclass
class Person:
id: UUID
name: str
hobbies: List[str]
age: Union[float, int]
birthday: Union[datetime, date]
pets: List[Pet]
assets: List[Dict[str, Dict[str, Any]]]
class PersonFactory(DataclassFactory[Person]):
...
def test_dynamic_factory_generation() -> None:
person_instance = PersonFactory.build()
assert len(person_instance.pets) > 0
assert isinstance(person_instance.pets[0], Pet)
You can also control the default factory for a type by declaring a factory as the type default:
from dataclasses import dataclass
from datetime import date, datetime
from enum import Enum
from typing import Any, Dict, List, Union
from uuid import UUID
from polyfactory import Use
from polyfactory.factories import DataclassFactory
class Species(str, Enum):
CAT = "Cat"
DOG = "Dog"
@dataclass
class Pet:
name: str
species: Species
sound: str
@dataclass
class Person:
id: UUID
name: str
hobbies: List[str]
age: Union[float, int]
birthday: Union[datetime, date]
pets: List[Pet]
assets: List[Dict[str, Dict[str, Any]]]
class PetFactory(DataclassFactory[Pet]):
__set_as_default_factory_for_type__ = True
name = Use(DataclassFactory.__random__.choice, ["Roxy", "Spammy", "Moshe"])
class PersonFactory(DataclassFactory[Person]):
...
def test_default_pet_factory() -> None:
person_instance = PersonFactory.build()
assert len(person_instance.pets) > 0
assert person_instance.pets[0].name in ["Roxy", "Spammy", "Moshe"]
Randomized Collection Length#
Set of fields that allow you to generate a collection with random lengths. By default only one item is generated.
from dataclasses import dataclass
from typing import Tuple
from polyfactory.factories import DataclassFactory
@dataclass
class Owner:
cars: Tuple[str, ...]
class OwnerFactory(DataclassFactory[Owner]):
__randomize_collection_length__ = True
__min_collection_length__ = 2
__max_collection_length__ = 5
def test_randomized_collection_length() -> None:
owner = OwnerFactory.build()
assert 2 <= len(owner.cars) <= 5
Allow None Optionals#
Allow None to be generated as a value for types marked as optional. When set to True, the outputted value will be randomly chosen between None and other allowed types. By default, this is set to True.
By setting to False, then optional types will always be treated as the wrapped type:
from dataclasses import dataclass
from typing import Optional
from uuid import UUID
from polyfactory.factories.dataclass_factory import DataclassFactory
@dataclass
class Person:
id: UUID
name: Optional[str]
class PersonFactory(DataclassFactory[Person]):
__allow_none_optionals__ = False
def test_optional_type_ignored() -> None:
person_instance = PersonFactory.build()
assert isinstance(person_instance.name, str)
Check Factory Fields#
When __check_model__ is set to True, declaring fields on the factory that don’t exist on the model will trigger an exception.
This is only true when fields are declared with Use
, PostGenerated
, Ignore
and Require
.
Any other field definition will not be checked.
from dataclasses import dataclass
from uuid import UUID
import pytest
from polyfactory import ConfigurationException, PostGenerated
from polyfactory.factories.dataclass_factory import DataclassFactory
@dataclass
class Person:
id: UUID
def test_check_factory_fields() -> None:
with pytest.raises(
ConfigurationException,
match="unknown_field is declared on the factory PersonFactory but it is not part of the model Person",
):
class PersonFactory(DataclassFactory[Person]):
__check_model__ = True
unknown_field = PostGenerated(lambda: "foo")
Use Default Values#
If __use_defaults__
is set to True
, then the default value will be used instead of creating a random value
for a given field, provided there’s a default value for that field.
By default, __use_defaults__
is set to False.
If you need more fine grained control, you can override the
should_use_default_value()
classmethod.
Note
Setting __use_defaults__
has no effect for TypedDictFactory
since you cannot set default values for
TypedDict
.
from dataclasses import dataclass
from enum import Enum
from polyfactory.factories import DataclassFactory
class Species(str, Enum):
CAT = "Cat"
DOG = "Dog"
@dataclass
class Pet:
name: str
sound: str = "meow"
species: Species = Species.CAT
class PetFactory(DataclassFactory[Pet]):
__model__ = Pet
__use_defaults__ = True
def test_use_default() -> None:
pet = PetFactory.build()
assert pet.species == Species.CAT
assert pet.sound == "meow"