Skip to content

Commit

Permalink
Add factory_boy plugin support (#1497)
Browse files Browse the repository at this point in the history
  • Loading branch information
lk-geimfari authored Feb 27, 2024
1 parent fd8250e commit 1491f52
Show file tree
Hide file tree
Showing 16 changed files with 861 additions and 190 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version 15.0.0
--------------

- Integrated `mimesis-factory` into Mimesis itself. See `mimesis-factory#246 <https://github.com/lk-geimfari/mimesis-factory/issues/246>`_ and `mimesis#1494 <https://github.com/lk-geimfari/mimesis/issues/1494>`_ for more information.


Version 14.0.0
--------------

Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@
# built documents.
#
# The short X.Y version.
version = "14.0"
version = "15.0"
# The full version, including alpha/beta/rc tags.
release = "14.0.0"
release = "15.0.0"

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
1 change: 1 addition & 0 deletions docs/contents.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ documentation explains the different parts of the Mimesis and how they can be us
schema
random_and_seed
pytest_plugin
factory_plugin
tips


Expand Down
97 changes: 97 additions & 0 deletions docs/factory_plugin.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
.. _factory_plugin:

Integration with factory_boy
============================

.. versionadded:: 15.0.0

You no longer require any third-party packages to integrate Mimesis with ``factory_boy``.

Mimesis requires ``factory_boy`` to be installed, but it's not a hard dependency.
Therefore, you'll need to install it manually:

.. code-block:: bash
poetry add --group dev factory_boy
Utilization
-----------

Look at the example below and you’ll understand how it works:

.. code-block:: python
class Account(object):
def __init__(self, username, email, name, surname, age):
self.username = username
self.email = email
self.name = name
self.surname = surname
self.age = age
Now, use the ``MimesisField`` class to define how fake data is generated:

.. code-block:: python
import factory
from mimesis.plugins.factory import MimesisField
from account import Account
class AccountFactory(factory.Factory):
class Meta(object):
model = Account
username = MimesisField('username', template='l_d')
name = MimesisField('name', gender='female')
surname = MimesisField('surname', gender='female')
age = MimesisField('age', minimum=18, maximum=90)
email = factory.LazyAttribute(
lambda instance: '{0}@example.org'.format(instance.username)
)
access_token = MimesisField('token', entropy=32)
See `factory_boy <https://factoryboy.readthedocs.io/>`_ documentation for more information about how to use factories.


Configuration
-------------

You can also define custom field handlers for your factories. To do this, you need to
define an attribute named ``field_handlers`` in the ``Params`` class of your factory.

Just like this:

.. code-block:: python
import factory
from mimesis.plugins.factory import MimesisField
class FactoryWithCustomFieldHandlers(factory.Factory):
class Meta(object):
model = Guest # Your model here
class Params(object):
field_handlers = [
("num", lambda rand, **kwargs: rand.randint(1, 99)),
("nick", lambda rand, **kwargs: rand.choice(["john", "alice"])),
]
age = MimesisField("num")
nickname = MimesisField("nick")
See `Custom Field Handlers <https://mimesis.name/en/master/schema.html#custom-field-handlers>`_ for more information
about how to define custom field handlers.

Factories and pytest
--------------------

We also recommend to use `pytest-factoryboy <https://github.com/pytest-dev/pytest-factoryboy>`_.
This way it will be possible to integrate your factories into pytest fixtures.
11 changes: 6 additions & 5 deletions docs/pytest_plugin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
Integration with Pytest
=======================

Starting from version `14.0.0`, Mimesis now supports `pytest` out of the box. You no longer
require any third-party packages to seamlessly integrate Mimesis with `pytest`.
.. versionadded:: 14.0.0

You no longer require any third-party packages to seamlessly integrate Mimesis with `pytest`.


Usage
~~~~~
-----

Using the personal provider as part of a test.

Expand Down Expand Up @@ -51,7 +52,7 @@ You can also specify locales:
Fixtures
~~~~~~~~
--------

We offer two public fixtures: `mimesis_locale` and `mimesis`. While `mimesis_locale` is
an enum object (e.g., `Locale.EN`, `Locale.RU`), `mimesis` is an instance of :class:`mimesis.schema.Field`.
Expand All @@ -60,7 +61,7 @@ See :class:`mimesis.enums.Locale`.


Impact on Test Speed
~~~~~~~~~~~~~~~~~~~~
--------------------

We employ caching of Mimesis instances for various locales throughout the entire test session, making
the creation of new instances cost-effective.
2 changes: 1 addition & 1 deletion mimesis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
"__license__",
]

__version__ = "14.0.0"
__version__ = "15.0.0"
__title__ = "mimesis"
__description__ = "Mimesis: Fake Data Generator."
__url__ = "https://github.com/lk-geimfari/mimesis"
Expand Down
118 changes: 118 additions & 0 deletions mimesis/plugins/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from contextlib import contextmanager
from typing import Any, ClassVar, Iterator

from mimesis.locales import Locale
from mimesis.schema import Field, RegisterableFieldHandlers

try:
from factory import declarations
from factory.builder import BuildStep, Resolver
except ImportError:
raise ImportError("This plugin requires factory_boy to be installed.")

__all__ = ["MimesisField"]


class MimesisField(declarations.BaseDeclaration): # type: ignore[misc]
"""
Mimesis integration with FactoryBoy starts here.
This class provides a common interface for FactoryBoy,
but inside it has Mimesis generators.
"""

_default_locale: ClassVar[Locale] = Locale.EN
_cached_instances: ClassVar[dict[str, Field]] = {}

def __init__(
self,
field: str,
locale: Locale | None = None,
**kwargs: Any,
) -> None:
"""
Creates a field instance.
The created field is lazy. It also receives build time parameters.
These parameters are not applied yet.
:param field: name to be passed to :class:`~mimesis.schema.Field`.
:param locale: locale to use. This parameter has the highest priority.
:param kwargs: optional parameters that would be passed to ``Field``.
"""
super().__init__()
self.locale = locale
self.kwargs = kwargs
self.field = field

def evaluate(
self,
instance: Resolver,
step: BuildStep,
extra: dict[str, Any] | None = None,
) -> Any:
"""Evaluates the lazy field.
:param instance: (factory.builder.Resolver): The object holding currently computed attributes.
:param step: (factory.builder.BuildStep): The object holding the current build step.
:param extra: Extra call-time added kwargs that would be passed to ``Field``.
"""
kwargs: dict[str, Any] = {}
kwargs.update(self.kwargs)
kwargs.update(extra or {})

field_handlers = step.builder.factory_meta.declarations.get(
"field_handlers", []
)

mimesis_field = self._get_cached_instance(
locale=self.locale,
field_handlers=field_handlers,
)
return mimesis_field(self.field, **kwargs)

@classmethod
@contextmanager
def override_locale(cls, locale: Locale) -> Iterator[None]:
"""
Overrides unspecified locales.
Remember that implicit locales would not be overridden.
"""
old_locale = cls._default_locale
cls._default_locale = locale
yield
cls._default_locale = old_locale

@classmethod
def _get_cached_instance(
cls,
locale: Locale | None = None,
field_handlers: RegisterableFieldHandlers | None = None,
) -> Field:
"""Returns cached instance.
:param locale: locale to use.
:param field_handlers: custom field handlers.
:return: cached instance of Field.
"""
if locale is None:
locale = cls._default_locale

field_names = "-".join(
sorted(
dict(field_handlers if field_handlers else []).keys(),
)
)

key = f"{locale}{field_names}"

if key not in cls._cached_instances:
field = Field(locale)

if field_handlers:
field.register_handlers(field_handlers)

cls._cached_instances[key] = field

return cls._cached_instances[key]
Loading

0 comments on commit 1491f52

Please sign in to comment.