diff --git a/docs/changelog.rst b/docs/changelog.rst index 94fe8699..7cbb7950 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -59,6 +59,7 @@ ChangeLog - Add support for Django 3.1 - Add support for Python 3.9 + - Add support for `unique` Faker feature *Removed:* diff --git a/docs/reference.rst b/docs/reference.rst index a7e9be01..dd1b8222 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -713,6 +713,24 @@ Faker date_end=datetime.date(2020, 5, 31), ) + Since Faker 4.9.0 version (see `Faker documentation `_), + 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. diff --git a/factory/faker.py b/factory/faker.py index 6ed2e28c..e8db0af7 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -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)`` @@ -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 = {} @@ -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) diff --git a/tests/test_faker.py b/tests/test_faker.py index d1a16da0..3ebf3876 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -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 @@ -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): @@ -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()