Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mazulo/adds faker generator feature #365

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ build/
lib64
pyvenv.cfg
dist/
.project
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased](https://github.com/model-bakers/model_bakery/tree/main)

### Added
- [dev] Adds support to use values generated by [Faker](https://pypi.org/project/Faker/) [#365](https://github.com/model-bakers/model_bakery/pull/365)

### Changed
- [dev] Switch to Python 3.11 release in CI [#357](https://github.com/model-bakers/model_bakery/pull/357)
Expand Down
23 changes: 23 additions & 0 deletions docs/source/how_bakery_behaves.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,29 @@ Additionaly, if you want to your created instance to be returned respecting one
movie = baker.make(Movie, title='Old Boys', _from_manager='availables') # This will use the Movie.availables model manager


Also passing ``_use_faker_generator=True`` will make ``baker`` to use `Faker <https://pypi.org/project/Faker/>`_ to generate values. ``baker`` will read the field name and then select a generator from there. Currently we only support the following fields, however this list will increase:

- ``username``
- ``email``
- ``first_name``
- ``last_name``
- ``name``
- ``fullname``
- ``full_name``
- ``ip``
- ``ipv4``
- ``ipv6``

Examples:

.. code-block:: python

profile = baker.make(
models.Profile,
_use_faker_generator=True,
)
print(profile.name) # would print a more realistic fake email, for example: 'Erik Barnett'

Save method custom parameters
-----------------------------

Expand Down
27 changes: 24 additions & 3 deletions model_bakery/baker.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
ModelNotFound,
RecipeIteratorEmpty,
)
from .faker_gen import faker_generator_mapping
from .utils import seq # NoQA: enable seq to be imported from baker
from .utils import import_from_str

Expand All @@ -69,6 +70,7 @@ def make(
_create_files: bool = False,
_using: str = "",
_bulk_create: bool = False,
_use_faker_generator: Optional[bool] = False,
**attrs: Any,
) -> M:
...
Expand All @@ -85,6 +87,7 @@ def make(
_using: str = "",
_bulk_create: bool = False,
_fill_optional: Union[List[str], bool] = False,
_use_faker_generator: Optional[bool] = False,
**attrs: Any,
) -> List[M]:
...
Expand All @@ -100,17 +103,22 @@ def make(
_using: str = "",
_bulk_create: bool = False,
_fill_optional: Union[List[str], bool] = False,
_use_faker_generator: Optional[bool] = False,
**attrs: Any,
):
"""Create a persisted instance from a given model its associated models.

It fill the fields with random values or you can specify which
It fills the fields with random values or you can specify which
fields you want to define its values by yourself.
"""
_save_kwargs = _save_kwargs or {}
attrs.update({"_fill_optional": _fill_optional})
baker: Baker = Baker.create(
_model, make_m2m=make_m2m, create_files=_create_files, _using=_using
_model,
make_m2m=make_m2m,
create_files=_create_files,
_using=_using,
_use_faker_generator=_use_faker_generator,
)
if _valid_quantity(_quantity):
raise InvalidQuantityException
Expand Down Expand Up @@ -327,11 +335,16 @@ def create(
make_m2m: bool = False,
create_files: bool = False,
_using: str = "",
_use_faker_generator: Optional[bool] = False,
) -> "Baker[NewM]":
"""Create the baker class defined by the `BAKER_CUSTOM_CLASS` setting."""
baker_class = _custom_baker_class() or cls
return cast(Type[Baker[NewM]], baker_class)(
_model, make_m2m, create_files, _using=_using
_model,
make_m2m,
create_files,
_using=_using,
_use_faker_generator=_use_faker_generator,
)

def __init__(
Expand All @@ -340,6 +353,7 @@ def __init__(
make_m2m: bool = False,
create_files: bool = False,
_using: str = "",
_use_faker_generator: Optional[bool] = False,
) -> None:
self.make_m2m = make_m2m
self.create_files = create_files
Expand All @@ -349,6 +363,7 @@ def __init__(
self.rel_attrs: Dict[str, Any] = {}
self.rel_fields: List[str] = []
self._using = _using
self._use_faker_generator = _use_faker_generator

if isinstance(_model, str):
self.model = cast(Type[M], self.finder.get_model(_model))
Expand All @@ -371,6 +386,7 @@ def make(
_refresh_after_create: bool = False,
_from_manager=None,
_fill_optional: Union[List[str], bool] = False,
_use_faker_generator: Optional[bool] = False,
**attrs: Any,
):
"""Create and persist an instance of the model associated with Baker instance."""
Expand Down Expand Up @@ -660,6 +676,11 @@ def generate_value(self, field: Field, commit: bool = True) -> Any:
`attr_mapping` and `type_mapping` can be defined easily overwriting the
model.
"""
field_name = field.attname
if self._use_faker_generator and field_name in faker_generator_mapping:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other comment about ImportError. I think that if we fail to import faker we should output a warning message (when _use_faker_generator is True) and continue past this logic so that it does not break, but still gives the user feedback that they are trying to use an optional feature for which they do not have all the requirements.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's cleaner to fail when someone tries to use the feature without installing faker.
But not using the feature should still be possible without faker being installed.

moving the from .faker_gen import faker_generator_mapping inside this if clause should do the trick...?

generator = faker_generator_mapping.get(field_name)
return generator()

is_content_type_fk = isinstance(field, ForeignKey) and issubclass(
self._remote_field(field).model, contenttypes.models.ContentType
)
Expand Down
18 changes: 18 additions & 0 deletions model_bakery/faker_gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Callable, Dict

from faker import Faker
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import (and its subsequent behavior) should be resilient to ImportError, since we don't want to require users to install faker if they are not going to use it.


FAKER = Faker()

faker_generator_mapping: Dict[str, Callable] = {
"username": FAKER.user_name,
"email": FAKER.email,
"first_name": FAKER.first_name,
"last_name": FAKER.last_name,
"name": FAKER.name,
"fullname": FAKER.name,
"full_name": FAKER.name,
"ip": FAKER.ipv4,
"ipv4": FAKER.ipv4,
"ipv6": FAKER.ipv6,
}
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
django>=3.2
Faker==15.1.3
Copy link
Contributor

@hiaselhans hiaselhans Dec 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be moved to extras_require["faker"]

39 changes: 39 additions & 0 deletions tests/test_baker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1049,3 +1049,42 @@ def test_create(self):
)
c1, c2 = models.Classroom.objects.all()[:2]
assert list(c1.students.all()) == list(c2.students.all()) == [person]


class TestCreateProfileWithFakerGenerator(TestCase):
@pytest.mark.django_db
def test_create_profile_with_email_generated_by_faker(self):
profile = baker.make(
models.Profile,
_use_faker_generator=True,
)
assert profile.email

@pytest.mark.django_db
def test_create_profile_with_username_generated_by_faker(self):
user = baker.make(
models.User,
_use_faker_generator=True,
)
another_user = baker.make(models.User)
assert user.username and len(another_user.username) > len(user.username)

@pytest.mark.django_db
def test_create_person_with_name_generated_by_faker(self):
person = baker.make(
models.Person,
_use_faker_generator=True,
)
assert len(person.name.split(" ")) == 2

@pytest.mark.django_db
def test_create_person_with_name_generated_by_faker_different_than_default(self):
person = baker.make(
models.Person,
_use_faker_generator=True,
)
another_person = baker.make(models.Person)
assert (
len(another_person.name.split(" ")) == 1
and another_person.name != person.name
)