Repository: joke2k/faker

Issue:
Faker changes data for same seed
* Faker version:4.17.1
* OS:mac OS Catalina

Brief summary of the issue goes here.

### Steps to reproduce

1. Initialize faker based on a constant: Faker.seed(int(constant_string))
2. Generate data such as faker.ssn(), faker.first_name() etc
3. 

### Expected behavior

The generated data should never change anytime based on a constant seed.

### Actual behavior

After a period of time, not sure when exactly, the data generated for the particular constant seed changes completely. I noticed this in few date objects, when the day changes. But for other fields, it was constant. But I think change in month triggered the faker data to be changed. Is there a root cause to it?



Discussion:
@dgajendr have you found a workaround for this? 
@chitopunk No, I am still looking for a solution. Do you know the root cause of this?
Your faker version is very old.

Please install the latest and see if this test program breaks for you.

```
import faker

faker.Faker.seed(25)
fake = faker.Faker()

data = fake.ssn(), fake.first_name(), fake.last_name(), faker.VERSION

assert data == ("387-99-0252", "Jason", "Wiggins", "6.6.3"), data
print("Good")
```
(actually I don't know for sure that my values will work on your computer so you may need to adjust them)

I can confirm the seed configuration is not working if using multiple locales
> Expected behavior
> The generated data should never change anytime based on a constant seed.

This is partially incorrect. The [expected behavior for seeding](https://github.com/joke2k/faker#seeding-the-generator) is that data should be constant within the same version (not _anytime_):

>  Calling the same methods with the same version of faker and seed produces the same results.

Keeping the datasets constant would mean we can't ever update or fix them. In order to find a balance between stability and moving the project forward, what we do is increment the patch version for _every commit_ and keep a detailed [`CHANGELOG`](https://github.com/joke2k/faker/blob/master/CHANGELOG.md).

If you require the data to never change, maybe you could pin `Faker` to a specific patch version?
This issue is stale because it has been open for 30 days with no activity.
> I can confirm the seed configuration is not working if using multiple locales

Me too. 
Using Faker version: 12.3.1
OS: Mac OS Monterey
Computer: MacBook Pro (14-inch, 2021)
```
from faker import Faker

fake = Faker(["en_GB", "fr_FR", "en_IN"])
Faker.seed(2043)

print(fake.name())
```
Will produce different names each time but if you use only one locale it works.

Current work around:
```
import random
from faker import Faker

random.seed(2043)
fake = Faker(["en_GB", "fr_FR", "en_IN"])
Faker.seed(2043)


print(fake.name())
```
Not sure why, but both **random.seed** and **Faker.seed** need to be there

Resolving patch:
diff --git a/faker/generator.py b/faker/generator.py
--- a/faker/generator.py
+++ b/faker/generator.py
@@ -11,6 +11,9 @@
 mod_random = random  # compat with name released in 0.8
 
 
+Sentinel = object()
+
+
 class Generator:
 
     __config: Dict[str, Dict[Hashable, Any]] = {
@@ -18,6 +21,7 @@ class Generator:
     }
 
     _is_seeded = False
+    _global_seed = Sentinel
 
     def __init__(self, **config: Dict) -> None:
         self.providers: List["BaseProvider"] = []
@@ -74,6 +78,7 @@ def seed_instance(self, seed: Optional[Hashable] = None) -> "Generator":
     @classmethod
     def seed(cls, seed: Optional[Hashable] = None) -> None:
         random.seed(seed)
+        cls._global_seed = seed
         cls._is_seeded = True
 
     def format(self, formatter: str, *args: Any, **kwargs: Any) -> str:
diff --git a/faker/proxy.py b/faker/proxy.py
--- a/faker/proxy.py
+++ b/faker/proxy.py
@@ -1,6 +1,5 @@
 import copy
 import functools
-import random
 import re
 
 from collections import OrderedDict
@@ -10,7 +9,7 @@
 from .config import DEFAULT_LOCALE
 from .exceptions import UniquenessException
 from .factory import Factory
-from .generator import Generator
+from .generator import Generator, Sentinel, random
 from .utils.distribution import choices_distribution
 
 _UNIQUE_ATTEMPTS = 1000
@@ -146,18 +145,27 @@ def _select_factory(self, method_name: str) -> Factory:
         """
 
         factories, weights = self._map_provider_method(method_name)
+
         if len(factories) == 0:
             msg = f"No generator object has attribute {method_name!r}"
             raise AttributeError(msg)
         elif len(factories) == 1:
             return factories[0]
 
+        if Generator._global_seed is not Sentinel:
+            random.seed(Generator._global_seed)
         if weights:
-            factory = choices_distribution(factories, weights, length=1)[0]
+            factory = self._select_factory_distribution(factories, weights)
         else:
-            factory = random.choice(factories)
+            factory = self._select_factory_choice(factories)
         return factory
 
+    def _select_factory_distribution(self, factories, weights):
+        return choices_distribution(factories, weights, random, length=1)[0]
+
+    def _select_factory_choice(self, factories):
+        return random.choice(factories)
+
     def _map_provider_method(self, method_name: str) -> Tuple[List[Factory], Optional[List[float]]]:
         """
         Creates a 2-tuple of factories and weights for the given provider method name


Test patch:
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
--- a/tests/test_proxy.py
+++ b/tests/test_proxy.py
@@ -128,6 +128,14 @@ def test_seed_classmethod(self):
             Faker.seed(0)
             mock_seed.assert_called_once_with(0)
 
+    def test_seed_class_locales(self):
+        Faker.seed(2043)
+        fake = Faker(["en_GB", "fr_FR", "en_IN"])
+        name = fake.name()
+
+        for _ in range(5):
+            assert fake.name() == name
+
     def test_seed_instance(self):
         locale = ["de_DE", "en-US", "en-PH", "ja_JP"]
         fake = Faker(locale)
@@ -232,51 +240,48 @@ def test_multiple_locale_caching_behavior(self):
                 # So each call to name() accesses the cached mapping twice
                 assert mock_cached_map.call_count == 200
 
-    @patch("faker.proxy.random.choice")
-    @patch("faker.proxy.choices_distribution")
-    def test_multiple_locale_factory_selection_no_weights(self, mock_choices_fn, mock_random_choice):
+    @patch("faker.proxy.Faker._select_factory_choice")
+    @patch("faker.proxy.Faker._select_factory_distribution")
+    def test_multiple_locale_factory_selection_no_weights(self, mock_factory_distribution, mock_factory_choice):
         fake = Faker(["de_DE", "en-US", "en-PH", "ja_JP"])
 
         # There are no distribution weights, so factory selection logic will use `random.choice`
         # if multiple factories have the specified provider method
         with patch("faker.proxy.Faker._select_factory", wraps=fake._select_factory) as mock_select_factory:
             mock_select_factory.assert_not_called()
-            mock_choices_fn.assert_not_called()
-            mock_random_choice.assert_not_called()
+            mock_factory_distribution.assert_not_called()
+            mock_factory_choice.assert_not_called()
 
             # All factories for the listed locales have the `name` provider method
             fake.name()
             mock_select_factory.assert_called_once_with("name")
-            mock_choices_fn.assert_not_called()
-            mock_random_choice.assert_called_once_with(fake.factories)
+            mock_factory_distribution.assert_not_called()
+            mock_factory_choice.assert_called_once_with(fake.factories)
             mock_select_factory.reset_mock()
-            mock_choices_fn.reset_mock()
-            mock_random_choice.reset_mock()
+            mock_factory_distribution.reset_mock()
+            mock_factory_choice.reset_mock()
 
             # Only `en_PH` factory has provider method `luzon_province`, so there is no
             # need for `random.choice` factory selection logic to run
             fake.luzon_province()
             mock_select_factory.assert_called_with("luzon_province")
-            mock_choices_fn.assert_not_called()
-            mock_random_choice.assert_not_called()
+            mock_factory_distribution.assert_not_called()
+            mock_factory_choice.assert_not_called()
             mock_select_factory.reset_mock()
-            mock_choices_fn.reset_mock()
-            mock_random_choice.reset_mock()
+            mock_factory_distribution.reset_mock()
+            mock_factory_choice.reset_mock()
 
             # Both `en_US` and `ja_JP` factories have provider method `zipcode`
             fake.zipcode()
             mock_select_factory.assert_called_once_with("zipcode")
-            mock_choices_fn.assert_not_called()
-            mock_random_choice.assert_called_once_with(
+            mock_factory_distribution.assert_not_called()
+            mock_factory_choice.assert_called_once_with(
                 [fake["en_US"], fake["ja_JP"]],
             )
-            mock_select_factory.reset_mock()
-            mock_choices_fn.reset_mock()
-            mock_random_choice.reset_mock()
 
-    @patch("faker.proxy.random.choice")
-    @patch("faker.proxy.choices_distribution")
-    def test_multiple_locale_factory_selection_with_weights(self, mock_choices_fn, mock_random_choice):
+    @patch("faker.proxy.Faker._select_factory_choice")
+    @patch("faker.proxy.Faker._select_factory_distribution")
+    def test_multiple_locale_factory_selection_with_weights(self, mock_factory_distribution, mock_factory_choice):
         locale = OrderedDict(
             [
                 ("de_DE", 3),
@@ -286,8 +291,8 @@ def test_multiple_locale_factory_selection_with_weights(self, mock_choices_fn, m
             ]
         )
         fake = Faker(locale)
-        mock_choices_fn.assert_not_called()
-        mock_random_choice.assert_not_called()
+        mock_factory_distribution.assert_not_called()
+        mock_factory_choice.assert_not_called()
 
         # Distribution weights have been specified, so factory selection logic will use
         # `choices_distribution` if multiple factories have the specified provider method
@@ -296,34 +301,52 @@ def test_multiple_locale_factory_selection_with_weights(self, mock_choices_fn, m
             # All factories for the listed locales have the `name` provider method
             fake.name()
             mock_select_factory.assert_called_once_with("name")
-            mock_choices_fn.assert_called_once_with(fake.factories, fake.weights, length=1)
-            mock_random_choice.assert_not_called()
-            mock_select_factory.reset_mock()
-            mock_choices_fn.reset_mock()
-            mock_random_choice.reset_mock()
+            mock_factory_distribution.assert_called_once_with(fake.factories, fake.weights)
+            mock_factory_choice.assert_not_called()
+
+    @patch("faker.proxy.Faker._select_factory_choice")
+    @patch("faker.proxy.Faker._select_factory_distribution")
+    def test_multiple_locale_factory_selection_single_provider(self, mock_factory_distribution, mock_factory_choice):
+        locale = OrderedDict(
+            [
+                ("de_DE", 3),
+                ("en-US", 2),
+                ("en-PH", 1),
+                ("ja_JP", 5),
+            ]
+        )
+        fake = Faker(locale)
+
+        # Distribution weights have been specified, so factory selection logic will use
+        # `choices_distribution` if multiple factories have the specified provider method
+        with patch("faker.proxy.Faker._select_factory", wraps=fake._select_factory) as mock_select_factory:
 
             # Only `en_PH` factory has provider method `luzon_province`, so there is no
             # need for `choices_distribution` factory selection logic to run
             fake.luzon_province()
             mock_select_factory.assert_called_once_with("luzon_province")
-            mock_choices_fn.assert_not_called()
-            mock_random_choice.assert_not_called()
-            mock_select_factory.reset_mock()
-            mock_choices_fn.reset_mock()
-            mock_random_choice.reset_mock()
+            mock_factory_distribution.assert_not_called()
+            mock_factory_choice.assert_not_called()
 
+    @patch("faker.proxy.Faker._select_factory_choice")
+    @patch("faker.proxy.Faker._select_factory_distribution")
+    def test_multiple_locale_factory_selection_shared_providers(self, mock_factory_distribution, mock_factory_choice):
+        locale = OrderedDict(
+            [
+                ("de_DE", 3),
+                ("en-US", 2),
+                ("en-PH", 1),
+                ("ja_JP", 5),
+            ]
+        )
+        fake = Faker(locale)
+
+        with patch("faker.proxy.Faker._select_factory", wraps=fake._select_factory) as mock_select_factory:
             # Both `en_US` and `ja_JP` factories have provider method `zipcode`
             fake.zipcode()
             mock_select_factory.assert_called_once_with("zipcode")
-            mock_choices_fn.assert_called_once_with(
-                [fake["en_US"], fake["ja_JP"]],
-                [2, 5],
-                length=1,
-            )
-            mock_random_choice.assert_not_called()
-            mock_select_factory.reset_mock()
-            mock_choices_fn.reset_mock()
-            mock_random_choice.reset_mock()
+            mock_factory_distribution.assert_called_once_with([fake["en_US"], fake["ja_JP"]], [2, 5])
+            mock_factory_choice.assert_not_called()
 
     def test_multiple_locale_factory_selection_unsupported_method(self):
         fake = Faker(["en_US", "en_PH"])