Repository: joke2k/faker

Issue:
Return list of n unique companies (n_unique keyword for all providers?)
* Faker version: 1.0.7
* OS: Ubuntu 16.04

I was wondering if it is possible to generate a list of 'n'  unique company names? I saw some PR's which gave a unique keyword for 'words' but doesn't seem to extend to other providers? I understand i could just keep regenerating and dropping duplicates until I got a unique set of length n, but would be nice to just have a keyword for that (plus this may be suboptimal depending on how Faker implemented under the hood, which i don't know enough about i confess)



Discussion:
Has this issue been tackled by anyone already?

Resolving patch:
diff --git a/faker/contrib/pytest/plugin.py b/faker/contrib/pytest/plugin.py
--- a/faker/contrib/pytest/plugin.py
+++ b/faker/contrib/pytest/plugin.py
@@ -33,5 +33,6 @@ def faker(request):
     if 'faker_seed' in request.fixturenames:
         seed = request.getfixturevalue('faker_seed')
     fake.seed_instance(seed=seed)
+    fake.unique.clear()
 
     return fake
diff --git a/faker/exceptions.py b/faker/exceptions.py
new file mode 100644
--- /dev/null
+++ b/faker/exceptions.py
@@ -0,0 +1,8 @@
+class BaseFakerException(Exception):
+    """The base exception for all Faker exceptions."""
+
+
+class UniquenessException(BaseFakerException):
+    """To avoid infinite loops, after a certain number of attempts,
+    the "unique" attribute of the Proxy will throw this exception.
+    """
diff --git a/faker/proxy.py b/faker/proxy.py
--- a/faker/proxy.py
+++ b/faker/proxy.py
@@ -1,13 +1,17 @@
+import functools
 import random
 import re
 
 from collections import OrderedDict
 
 from faker.config import DEFAULT_LOCALE
+from faker.exceptions import UniquenessException
 from faker.factory import Factory
 from faker.generator import Generator
 from faker.utils.distribution import choices_distribution
 
+_UNIQUE_ATTEMPTS = 1000
+
 
 class Faker:
     """Proxy class capable of supporting multiple locales"""
@@ -23,6 +27,7 @@ def __init__(self, locale=None, providers=None,
                  generator=None, includes=None, **config):
         self._factory_map = OrderedDict()
         self._weights = None
+        self._unique_proxy = UniqueProxy(self)
 
         if isinstance(locale, str):
             locales = [locale.replace('-', '_')]
@@ -105,6 +110,10 @@ def __getattr__(self, attr):
             factory = self._select_factory(attr)
             return getattr(factory, attr)
 
+    @property
+    def unique(self):
+        return self._unique_proxy
+
     def _select_factory(self, method_name):
         """
         Returns a random factory that supports the provider method
@@ -236,3 +245,44 @@ def factories(self):
 
     def items(self):
         return self._factory_map.items()
+
+
+class UniqueProxy:
+    def __init__(self, proxy):
+        self._proxy = proxy
+        self._seen = {}
+        self._sentinel = object()
+
+    def clear(self):
+        self._seen = {}
+
+    def __getattr__(self, name: str):
+        obj = getattr(self._proxy, name)
+        if callable(obj):
+            return self._wrap(name, obj)
+        else:
+            raise TypeError("Accessing non-functions through .unique is not supported.")
+
+    def _wrap(self, name, function):
+        @functools.wraps(function)
+        def wrapper(*args, **kwargs):
+            key = (name, args, tuple(sorted(kwargs.items())))
+
+            generated = self._seen.setdefault(key, {self._sentinel})
+
+            # With use of a sentinel value rather than None, we leave
+            # None open as a valid return value.
+            retval = self._sentinel
+
+            for i in range(_UNIQUE_ATTEMPTS):
+                if retval not in generated:
+                    break
+                retval = function(*args, **kwargs)
+            else:
+                raise UniquenessException("Got duplicated values after {0:,} iterations.".format(_UNIQUE_ATTEMPTS))
+
+            generated.add(retval)
+
+            return retval
+
+        return wrapper


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1 @@
+pytest_plugins = ["pytester"]
diff --git a/tests/pytest/test_unique_clear.py b/tests/pytest/test_unique_clear.py
new file mode 100644
--- /dev/null
+++ b/tests/pytest/test_unique_clear.py
@@ -0,0 +1,37 @@
+def test_unique_clears(testdir):
+    """Successive uses of the `faker` pytest fixture have the
+    generated unique values cleared between functions."""
+
+    testdir.makepyfile(
+        """
+        import pytest
+        from faker.exceptions import UniquenessException
+
+        NUM_SAMPLES = 100
+
+        def test_fully_exhaust_unique_booleans(faker):
+            _dummy = [faker.boolean() for _ in range(NUM_SAMPLES)]
+
+            faker.unique.boolean()
+            faker.unique.boolean()
+            with pytest.raises(UniquenessException):
+                faker.unique.boolean()
+            _dummy = [faker.boolean() for _ in range(NUM_SAMPLES)]
+
+        def test_do_not_exhaust_booleans(faker):
+            faker.unique.boolean()
+
+        def test_fully_exhaust_unique_booleans_again(faker):
+            _dummy = [faker.boolean() for _ in range(NUM_SAMPLES)]
+
+            faker.unique.boolean()
+            faker.unique.boolean()
+            with pytest.raises(UniquenessException):
+                faker.unique.boolean()
+            _dummy = [faker.boolean() for _ in range(NUM_SAMPLES)]
+        """,
+    )
+
+    result = testdir.runpytest()
+
+    result.assert_outcomes(passed=3)
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
--- a/tests/test_proxy.py
+++ b/tests/test_proxy.py
@@ -322,6 +322,7 @@ def test_dir_include_all_providers_attribute_in_list(self):
         fake = Faker(['en_US', 'en_PH'])
         expected = set(dir(Faker) + [
             '_factories', '_locales', '_factory_map', '_weights',
+            '_unique_proxy',
         ])
         for factory in fake.factories:
             expected |= {
diff --git a/tests/test_unique.py b/tests/test_unique.py
new file mode 100644
--- /dev/null
+++ b/tests/test_unique.py
@@ -0,0 +1,62 @@
+import pytest
+
+from faker import Faker
+from faker.exceptions import UniquenessException
+
+
+class TestUniquenessClass:
+    def test_uniqueness(self):
+        fake = Faker("en_US")
+
+        names = set()
+        # There are (at time of writing 690) first names in the
+        # US identity provider. Birthday paradox puts the chances of
+        # no duplicates in 250 selections as low enough to be impossible
+        for i in range(250):
+            first_name = fake.unique.first_name()
+            assert first_name not in names
+            names.add(first_name)
+
+    def test_sanity_escape(self):
+        fake = Faker()
+
+        # Those of you who are especially astute may realise
+        # there are only 2 booleans, so the third boolean cannot
+        # be unique.
+        with pytest.raises(UniquenessException, match=r"Got duplicated values after [\d,]+ iterations."):
+            for i in range(3):
+                _ = fake.unique.boolean()
+
+    def test_uniqueness_clear(self):
+        fake = Faker()
+
+        for i in range(2):
+            fake.unique.boolean()
+
+        fake.unique.clear()
+
+        # Because we cleared the generated values, this will not
+        # throw an exception
+        fake.unique.boolean()
+
+    def test_exclusive_arguments(self):
+        """Calls through the "unique" portal will only affect
+        calls with that specific function signature.
+        """
+        fake = Faker()
+
+        for i in range(10):
+            fake.unique.random_int(min=1, max=10)
+
+        # Different signature, so new pool. If they shared a pool
+        # this would throw a sanity exception
+        fake.unique.random_int(min=2, max=10)
+
+    def test_functions_only(self):
+        """Accessing non-functions through the `.unique` attribute
+        will throw a TypeError."""
+
+        fake = Faker()
+
+        with pytest.raises(TypeError, match="Accessing non-functions through .unique is not supported."):
+            fake.unique.locales