Repository: joke2k/faker

Issue:
`pydecimal(left_digits=14)` produced value with 13 left digits
* Faker version: 22.6.0
* OS: macOS, Linux

I've encountered a situation where `pydecimal` appears to not respect `left_digits`:

```python
from faker import Faker

fake = Faker()
Faker.seed(0)

fake.pydecimal(left_digits=14, right_digits=2)  # Decimal("5550397399547.96")
```

"5550397399547.96" has 13 left digits. How is this possible?

Note I found this while running a suite of a couple of thousands of tests. One of the tests failed because the value produced did not have the expectated number of digits. If I run the failing test in isolation, it passes i.e. that specific value doesn't get generated. I'm only getting this when I run the whole suite.

Discussion:
alexei: I fear the tests I added are only relevant to the original algorithm. Let me know how to go about this.

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 9d3360e1..4b14f82c 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 23.1.0
+current_version = 23.2.0
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f359126..a9328523 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## Changelog
 
+### [v23.2.0 - 2024-02-14](https://github.com/joke2k/faker/compare/v23.1.0...v23.2.0)
+
+* Allow Uniqueness and Localization. Thanks @moshemoshe137.
+
 ### [v23.1.0 - 2024-02-07](https://github.com/joke2k/faker/compare/v23.0.0...v23.1.0)
 
 * Add `uk_UA` lorem provider. Thanks @lozik.
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 862a29ec..39e9cd76 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -24,7 +24,7 @@ Making Changes
    the new branch with ``git checkout fix/master/my_contribution``.
    Please avoid working directly on the ``master`` branch.
 -  Make commits of logical units.
--  Follow our `coding style`_. Your can run ``make lint`` to format your code.
+-  Follow our `coding style`_. You can run ``make lint`` to format your code.
 -  Check for unnecessary whitespace with ``git diff --check`` before
    committing.
 -  Make sure you have added the necessary tests for your changes.
diff --git a/README.rst b/README.rst
index 8765eee0..9bceeecc 100644
--- a/README.rst
+++ b/README.rst
@@ -380,8 +380,8 @@ Seeding the Generator
 ---------------------
 
 When using Faker for unit testing, you will often want to generate the same
-data set. For convenience, the generator also provide a ``seed()`` method,
-which seeds the shared random number generator. Seed produces the same result
+data set. For convenience, the generator also provides a ``seed()`` method,
+which seeds the shared random number generator. A Seed produces the same result
 when the same methods with the same version of faker are called.
 
 .. code:: python
@@ -393,8 +393,8 @@ when the same methods with the same version of faker are called.
     print(fake.name())
     # 'Margaret Boehm'
 
-Each generator can also be switched to its own instance of ``random.Random``,
-separate to the shared one, by using the ``seed_instance()`` method, which acts
+Each generator can also be switched to use its own instance of ``random.Random``,
+separated from the shared one, by using the ``seed_instance()`` method, which acts
 the same way. For example:
 
 .. code:: python
diff --git a/VERSION b/VERSION
index f8aed3e0..11ebcd0e 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-23.1.0
+23.2.0
diff --git a/docs/coding_style.rst b/docs/coding_style.rst
index 433d8a1c..a0263b74 100644
--- a/docs/coding_style.rst
+++ b/docs/coding_style.rst
@@ -19,7 +19,7 @@ For each data set, please provide a comment with reference to the source
 and/or origin of the data.
 
 We only accept new data if it's coming from statistical sources, such as census or government institutions.
-This include names and their distribution.
+This includes names and their distribution.
 
 
 Name Lists
diff --git a/docs/communityproviders.rst b/docs/communityproviders.rst
index ed354349..e568acb3 100644
--- a/docs/communityproviders.rst
+++ b/docs/communityproviders.rst
@@ -68,7 +68,7 @@ Here's a list of Providers written by the community:
 
 If you want to add your own provider to this list, please submit a Pull Request to our `repo`_.
 
-In order to be included, your provider must satisfy these requirement:
+In order to be included, your provider must satisfy these requirements:
 
 * it must have tests.
 * it must be published on PyPI.
diff --git a/docs/conf.py b/docs/conf.py
index d540784f..588ad239 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -51,9 +51,9 @@ copyright = "2014-{0}, Daniele Faraglia".format(datetime.now().year)
 # built documents.
 #
 # The short X.Y version.
-version = "23.1.0"
+version = "23.2.0"
 # The full version, including alpha/beta/rc tags.
-release = "23.1.0"
+release = "23.2.0"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/docs/writing-docs.rst b/docs/writing-docs.rst
index 714b1659..594be659 100644
--- a/docs/writing-docs.rst
+++ b/docs/writing-docs.rst
@@ -85,7 +85,7 @@ specify ``:sample seed=12345: a=2``, the sample usage section generated will loo
 
 You can mix and match ``SIZE``, ``SEED``, and ``KWARGS``, and if ``KWARGS`` is becoming too long to
 fit a single line, you can break ``KWARGS`` into multiple lines in the same way you can break keyword
-arguments across multiples lines in actual Python code. For example, let us say the docstring contains
+arguments across multiple lines in actual Python code. For example, let us say the docstring contains
 this:
 
 .. code-block:: text
diff --git a/faker/__init__.py b/faker/__init__.py
index e6391eba..8b474c10 100644
--- a/faker/__init__.py
+++ b/faker/__init__.py
@@ -2,6 +2,6 @@ from faker.factory import Factory
 from faker.generator import Generator
 from faker.proxy import Faker
 
-VERSION = "23.1.0"
+VERSION = "23.2.0"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index 9501d97b..f16f30c3 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -262,6 +262,21 @@ class Provider(BaseProvider):
     def pyint(self, min_value: int = 0, max_value: int = 9999, step: int = 1) -> int:
         return self.generator.random_int(min_value, max_value, step=step)
 
+    def _random_int_of_length(self, length: int) -> int:
+        """Generate a random integer of a given length
+
+        If length is 0, so is the number. Otherwise the first digit must not be 0.
+        """
+
+        if length < 0:
+            raise ValueError("Length must be a non-negative integer.")
+        elif length == 0:
+            return 0
+        else:
+            min_value = 10 ** (length - 1)
+            max_value = (10**length) - 1
+            return self.pyint(min_value=min_value, max_value=max_value)
+
     def pydecimal(
         self,
         left_digits: Optional[int] = None,
@@ -310,7 +325,7 @@ class Provider(BaseProvider):
                 min_left_digits = math.ceil(math.log10(max(min_value or 1, 1)))
                 if left_digits is None:
                     left_digits = self.random_int(min_left_digits, max_left_random_digits)
-                left_number = "".join([str(self.random_digit()) for i in range(0, left_digits)]) or "0"
+                left_number = str(self._random_int_of_length(left_digits))
         else:
             if min_value is not None:
                 left_number = str(self.random_int(int(max(max_value or 0, 0)), int(abs(min_value))))
@@ -318,7 +333,7 @@ class Provider(BaseProvider):
                 min_left_digits = math.ceil(math.log10(abs(min(max_value or 1, 1))))
                 if left_digits is None:
                     left_digits = self.random_int(min_left_digits, max_left_random_digits)
-                left_number = "".join([str(self.random_digit()) for i in range(0, left_digits)]) or "0"
+                left_number = str(self._random_int_of_length(left_digits))
 
         if right_digits is None:
             right_digits = self.random_int(0, max_random_digits)
diff --git a/faker/proxy.py b/faker/proxy.py
index 653a5943..03a6e42e 100644
--- a/faker/proxy.py
+++ b/faker/proxy.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import copy
 import functools
 import re
@@ -35,7 +37,7 @@ class Faker:
         use_weighting: bool = True,
         **config: Any,
     ) -> None:
-        self._factory_map = OrderedDict()
+        self._factory_map: OrderedDict[str, Generator | Faker] = OrderedDict()
         self._weights = None
         self._unique_proxy = UniqueProxy(self)
         self._optional_proxy = OptionalProxy(self)
@@ -54,7 +56,7 @@ class Faker:
                 if final_locale not in locales:
                     locales.append(final_locale)
 
-        elif isinstance(locale, OrderedDict):
+        elif isinstance(locale, (OrderedDict, dict)):
             assert all(isinstance(v, (int, float)) for v in locale.values())
             odict = OrderedDict()
             for k, v in locale.items():
@@ -66,15 +68,25 @@ class Faker:
         else:
             locales = [DEFAULT_LOCALE]
 
-        for locale in locales:
-            self._factory_map[locale] = Factory.create(
-                locale,
+        if len(locales) == 1:
+            self._factory_map[locales[0]] = Factory.create(
+                locales[0],
                 providers,
                 generator,
                 includes,
                 use_weighting=use_weighting,
                 **config,
             )
+        else:
+            for locale in locales:
+                self._factory_map[locale] = Faker(
+                    locale,
+                    providers,
+                    generator,
+                    includes,
+                    use_weighting=use_weighting,
+                    **config,
+                )
 
         self._locales = locales
         self._factories = list(self._factory_map.values())
@@ -85,8 +97,12 @@ class Faker:
             attributes |= {attr for attr in dir(factory) if not attr.startswith("_")}
         return sorted(attributes)
 
-    def __getitem__(self, locale: str) -> Generator:
-        return self._factory_map[locale.replace("-", "_")]
+    def __getitem__(self, locale: str) -> Faker:
+        if locale.replace("-", "_") in self.locales and len(self.locales) == 1:
+            return self
+        instance = self._factory_map[locale.replace("-", "_")]
+        assert isinstance(instance, Faker)  # for mypy
+        return instance
 
     def __getattribute__(self, attr: str) -> Any:
         """
@@ -273,10 +289,10 @@ class Faker:
         return self._weights
 
     @property
-    def factories(self) -> List[Generator]:
+    def factories(self) -> List[Generator | Faker]:
         return self._factories
 
-    def items(self) -> List[Tuple[str, Generator]]:
+    def items(self) -> List[Tuple[str, Generator | Faker]]:
         return list(self._factory_map.items())
 
 


Test patch:
diff --git a/tests/providers/test_misc.py b/tests/providers/test_misc.py
index ec9fe57a..df735eee 100644
--- a/tests/providers/test_misc.py
+++ b/tests/providers/test_misc.py
@@ -423,7 +423,7 @@ class TestMiscProvider:
     def test_dsv_data_columns(self, faker):
         num_rows = 10
         data_columns = ["{{name}}", "#??-####", "{{address}}", "{{phone_number}}"]
-        with patch.object(faker["en_US"], "pystr_format") as mock_pystr_format:
+        with patch.object(faker["en_US"].factories[0], "pystr_format") as mock_pystr_format:
             mock_pystr_format.assert_not_called()
             faker.dsv(data_columns=data_columns, num_rows=num_rows)
 
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
index 6342bfb9..4935528e 100644
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -523,7 +523,7 @@ class TestPystrFormat(unittest.TestCase):
         Faker.seed(0)
 
     def test_formatter_invocation(self):
-        with patch.object(self.fake["en_US"], "foo") as mock_foo:
+        with patch.object(self.fake["en_US"].factories[0], "foo") as mock_foo:
             with patch("faker.providers.BaseProvider.bothify", wraps=self.fake.bothify) as mock_bothify:
                 mock_foo.return_value = "barbar"
                 value = self.fake.pystr_format("{{foo}}?#?{{foo}}?#?{{foo}}", letters="abcde")
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
index 322bc396..8b4b6d63 100644
--- a/tests/test_proxy.py
+++ b/tests/test_proxy.py
@@ -103,14 +103,14 @@ class TestFakerProxyClass:
         fake = Faker(locale)
         for locale_name, factory in fake.items():
             assert locale_name in processed_locale
-            assert isinstance(factory, Generator)
+            assert isinstance(factory, (Generator, Faker))
 
     def test_dunder_getitem(self):
         locale = ["de_DE", "en-US", "en-PH", "ja_JP"]
         fake = Faker(locale)
 
         for code in locale:
-            assert isinstance(fake[code], Generator)
+            assert isinstance(fake[code], (Generator, Faker))
 
         with pytest.raises(KeyError):
             fake["en_GB"]