diff --git a/.gitignore b/.gitignore index efd0a109..0741c826 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ build/ lib64 pyvenv.cfg dist/ +.project diff --git a/CHANGELOG.md b/CHANGELOG.md index d0048c88..0d5542ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/source/how_bakery_behaves.rst b/docs/source/how_bakery_behaves.rst index 7b2bbad3..8500787f 100644 --- a/docs/source/how_bakery_behaves.rst +++ b/docs/source/how_bakery_behaves.rst @@ -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 `_ 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 ----------------------------- diff --git a/model_bakery/baker.py b/model_bakery/baker.py index 86dd2a84..66562a0e 100644 --- a/model_bakery/baker.py +++ b/model_bakery/baker.py @@ -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 @@ -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: ... @@ -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]: ... @@ -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 @@ -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__( @@ -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 @@ -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)) @@ -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.""" @@ -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: + 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 ) diff --git a/model_bakery/faker_gen.py b/model_bakery/faker_gen.py new file mode 100644 index 00000000..0068c526 --- /dev/null +++ b/model_bakery/faker_gen.py @@ -0,0 +1,18 @@ +from typing import Callable, Dict + +from faker import Faker + +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, +} diff --git a/requirements.txt b/requirements.txt index 0d1e31d4..8cb6ccc9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ django>=3.2 +Faker==15.1.3 diff --git a/tests/test_baker.py b/tests/test_baker.py index d777d763..fb844fa8 100644 --- a/tests/test_baker.py +++ b/tests/test_baker.py @@ -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 + )