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

Feat/add unique feature arg in faker factory #997

Open
wants to merge 1 commit into
base: master
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 docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ ChangeLog

- Add support for Django 3.1
- Add support for Python 3.9
- Add support for `unique` Faker feature

*Removed:*

Expand Down
18 changes: 18 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,24 @@ Faker
date_end=datetime.date(2020, 5, 31),
)

Since Faker 4.9.0 version (see `Faker documentation <https://faker.readthedocs.io/en/master/fakerclass.html#unique-values>`_),
on every provider, you can specify whether to return an unique value or not:

.. code-block:: python

class UserFactory(fatory.Factory):
class Meta:
model = User

arrival = factory.Faker(
'date_between_dates',
date_start=datetime.date(2020, 1, 1),
date_end=datetime.date(2020, 5, 31),
unique=True # The generated date is guaranteed to be unique inside the test execution.
)

Note that an `UniquenessException` will be thrown if Faker fails to generate an unique value.

As with :class:`~factory.SubFactory`, the parameters can be any valid declaration.
This does not apply to the provider name or the locale.

Expand Down
20 changes: 14 additions & 6 deletions factory/faker.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Faker(declarations.BaseDeclaration):
Args:
provider (str): the name of the Faker field
locale (str): the locale to use for the faker
unique (bool): whether generated values must be unique

All other kwargs will be passed to the underlying provider
(e.g ``factory.Faker('ean', length=10)``
Expand All @@ -37,14 +38,17 @@ class Faker(declarations.BaseDeclaration):
"""
def __init__(self, provider, **kwargs):
locale = kwargs.pop('locale', None)
unique = kwargs.pop('unique', False)
self.provider = provider
super().__init__(
locale=locale,
unique=unique,
**kwargs)

def evaluate(self, instance, step, extra):
locale = extra.pop('locale')
subfaker = self._get_faker(locale)
unique = extra.pop('unique')
subfaker = self._get_faker(locale, unique)
return subfaker.format(self.provider, **extra)

_FAKER_REGISTRY = {}
Expand All @@ -61,17 +65,21 @@ def override_default_locale(cls, locale):
cls._DEFAULT_LOCALE = old_locale

@classmethod
def _get_faker(cls, locale=None):
def _get_faker(cls, locale=None, unique=False):
if locale is None:
locale = cls._DEFAULT_LOCALE

if locale not in cls._FAKER_REGISTRY:
cache_key = f"{locale}_{unique}"
if cache_key not in cls._FAKER_REGISTRY:
subfaker = faker.Faker(locale=locale)
cls._FAKER_REGISTRY[locale] = subfaker
if unique:
subfaker = subfaker.unique
cls._FAKER_REGISTRY[cache_key] = subfaker

return cls._FAKER_REGISTRY[locale]
return cls._FAKER_REGISTRY[cache_key]

@classmethod
def add_provider(cls, provider, locale=None):
"""Add a new Faker provider for the specified locale"""
cls._get_faker(locale).add_provider(provider)
cls._get_faker(locale, True).add_provider(provider)
cls._get_faker(locale, False).add_provider(provider)
112 changes: 112 additions & 0 deletions tests/test_faker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@

import collections
import datetime

import random
import unittest
from unittest import mock

import faker.providers
from faker.exceptions import UniquenessException

import factory


class MockUniqueProxy:

def __init__(self, expected):
self.expected = expected
self.random = random.Random()

def format(self, provider, **kwargs):
return "unique {}".format(self.expected[provider])


class MockFaker:
def __init__(self, expected):
self.expected = expected
Expand All @@ -18,6 +31,10 @@ def __init__(self, expected):
def format(self, provider, **kwargs):
return self.expected[provider]

@property
def unique(self):
return MockUniqueProxy(self.expected)


class AdvancedMockFaker:
def __init__(self, handlers):
Expand Down Expand Up @@ -168,3 +185,98 @@ def fake_select_date(start_date, end_date):
self.assertEqual(may_4th, trip.departure)
self.assertEqual(october_19th, trip.transfer)
self.assertEqual(may_25th, trip.arrival)

def test_faker_unique(self):
self._setup_mock_faker(name="John Doe", unique=True)
with mock.patch("factory.faker.faker_lib.Faker") as faker_mock:
faker_mock.return_value = MockFaker(dict(name="John Doe"))
faker_field = factory.Faker('name', unique=True)
self.assertEqual(
"unique John Doe",
faker_field.generate({'locale': None, 'unique': True})
)


class RealFakerTest(unittest.TestCase):

def test_faker_not_unique_not_raising_exception(self):
faker_field = factory.Faker('pyint')
# Make sure that without unique we can still create duplicated faker values.
self.assertEqual(1, faker_field.generate({'locale': None, 'min_value': 1, 'max_value': 1}))
self.assertEqual(1, faker_field.generate({'locale': None, 'min_value': 1, 'max_value': 1}))

def test_faker_unique_raising_exception(self):
faker_field = factory.Faker('pyint', min_value=1, max_value=1, unique=True)
# Make sure creating duplicated values raises an exception on the second call
# (which produces an identical value to the previous one).
self.assertEqual(1, faker_field.generate({'locale': None, 'min_value': 1, 'max_value': 1, 'unique': True}))
self.assertRaises(
UniquenessException,
faker_field.generate,
{'locale': None, 'min_value': 1, 'max_value': 1, 'unique': True}
)

def test_faker_shared_faker_instance(self):
class Foo:
def __init__(self, val):
self.val = val

class Bar:
def __init__(self, val):
self.val = val

class Factory1(factory.Factory):
val = factory.Faker('pyint', min_value=1, max_value=1, unique=True)

class Meta:
model = Foo

class Factory2(factory.Factory):
val = factory.Faker('pyint', min_value=1, max_value=1, unique=True)

class Meta:
model = Bar

f1 = Factory1.build()
f2 = Factory2.build()
self.assertEqual(f1.val, 1)
self.assertEqual(f2.val, 1)

def test_faker_inherited_faker_instance(self):
class Foo:
def __init__(self, val):
self.val = val

class Bar(Foo):
def __init__(self, val):
super().__init__(val)

class Factory1(factory.Factory):
val = factory.Faker('pyint', min_value=1, max_value=1, unique=True)

class Meta:
model = Foo

class Factory2(Factory1):

class Meta:
model = Bar

Factory1.build()
with self.assertRaises(UniquenessException):
Factory2.build()

def test_faker_clear_unique_store(self):
class Foo:
def __init__(self, val):
self.val = val

class Factory1(factory.Factory):
val = factory.Faker('pyint', min_value=1, max_value=1, unique=True)

class Meta:
model = Foo

Factory1.build()
Factory1.val.clear_unique_store()
Factory1.build()