Skip to content

Commit

Permalink
add persistence examples
Browse files Browse the repository at this point in the history
  • Loading branch information
Goldziher committed Apr 2, 2023
1 parent 5ce039d commit c1b7f49
Show file tree
Hide file tree
Showing 33 changed files with 428 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ repos:
rev: "v1.1.1"
hooks:
- id: mypy
exclude: "test_decimal_constraints|examples/factory_fields/test_example_2"
exclude: "test_decimal_constraints|examples/fields/test_example_2|examples/configuration"
additional_dependencies:
[
beanie,
Expand Down
File renamed without changes.
26 changes: 26 additions & 0 deletions docs/examples/configuration/test_example_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from dataclasses import dataclass

from polyfactory import Use
from polyfactory.factories import DataclassFactory


@dataclass
class Person:
name: str
age: float
height: float
weight: float


class PersonFactory(DataclassFactory[Person]):
__model__ = Person
__random_seed__ = 1

name = Use(DataclassFactory.__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"
27 changes: 27 additions & 0 deletions docs/examples/configuration/test_example_2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from dataclasses import dataclass
from random import Random

from polyfactory import Use
from polyfactory.factories import DataclassFactory


@dataclass
class Person:
name: str
age: float
height: float
weight: float


class PersonFactory(DataclassFactory[Person]):
__model__ = Person
__random__ = Random(10)

name = Use(DataclassFactory.__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 == "Alice"
assert PersonFactory.build().name == "John"
assert PersonFactory.build().name == "Alice"
29 changes: 29 additions & 0 deletions docs/examples/configuration/test_example_3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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]):
__model__ = 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 german locale.
assert PersonFactory.build().name == "Asunción Céspedes"
77 changes: 77 additions & 0 deletions docs/examples/configuration/test_example_4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from asyncio import sleep
from dataclasses import dataclass
from typing import List, Dict
from uuid import UUID

from polyfactory import SyncPersistenceProtocol, AsyncPersistenceProtocol
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]):
__model__ = 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
48 changes: 48 additions & 0 deletions docs/examples/configuration/test_example_5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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]):
__model__ = Pet
__set_as_default_factory_for_type__ = True

name = Use(DataclassFactory.__random__.choice, ["Roxy", "Spammy", "Moshe"])


class PersonFactory(DataclassFactory[Person]):
__model__ = 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"]
File renamed without changes.
40 changes: 40 additions & 0 deletions docs/examples/declaring_factories/test_example_5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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]):
__model__ = Person


def test_dynamic_factory_generation() -> None:
person_instance = PersonFactory.build()
assert len(person_instance.pets) > 0
assert isinstance(person_instance.pets[0], Pet)
22 changes: 22 additions & 0 deletions docs/examples/declaring_factories/test_example_6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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
species: Species
sound: str


def test_imperative_factory_creation() -> None:
pet_factory = DataclassFactory.create_factory(model=Pet)
pet_instance = pet_factory.build()
assert isinstance(pet_instance, Pet)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Empty file.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ Installation
.. code-block:: bash
pip install polyfactory
.. literalinclude:: /examples/defining_factories/test_example_1.py
:caption: Minimal example using a dataclass
:language: python

Example
-------

Expand Down
81 changes: 81 additions & 0 deletions docs/usage/configuration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 :class:`BaseFactory <polyfactory.factories.BaseFactory>`.
Below we discuss some configuration options in some depth.

Seeding Randomness
------------------

.. literalinclude:: /examples/configuration/test_example_1.py
:caption: Seeding the factory's 'random.Random'
:language: python

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
--------------

.. literalinclude:: /examples/configuration/test_example_2.py
:caption: Setting the factory's 'random.Random'
:language: python

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
-------------

.. literalinclude:: /examples/configuration/test_example_3.py
:caption: Setting the factory's 'Faker'
:language: python

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 :doc:`factory fields <./fields.rst>`.

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:

.. literalinclude:: /examples/configuration/test_example_4.py
:caption: Defining and using persistence handlers.
:language: python

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 :doc:`declaring factories <./declaring_factories.rst>`,
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:

.. literalinclude:: /examples/defining_factories/test_example_5.py
:caption: Dynamic factory generation
:language: python

You can also control the default factory for a type by declaring a factory as the type default:

.. literalinclude:: /examples/configuration/test_example_5.py
:caption: Setting a factory as a type default.
:language: python
Loading

0 comments on commit c1b7f49

Please sign in to comment.