Repository: joke2k/faker
Pull request #2088 (2024-08-22 19:33:18)

Add es_ES isbn provider
### What does this change

Add es_ES isbn provider and refactor the base provider to simplify localized provider creation.

### What was wrong

The base provider was not inheritable and the code assumptions there were confusing. 


Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 0079f6b2..8a1d68a6 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 27.4.0
+current_version = 28.1.0
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cdd164e8..9fa769a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 ## Changelog
 
+### [v28.1.0 - 2024-08-30](https://github.com/joke2k/faker/compare/v28.0.0...v28.1.0)
+
+* Fix Incorrect City Spelling in `uk_UA` locale. Thanks @ch4zzy.
+
+### [v28.0.0 - 2024-08-23](https://github.com/joke2k/faker/compare/v27.4.0...v28.0.0)
+
+* Fix `pydecimal` handling of `positive` keyword. Thanks @tahzeer.
+
 ### [v27.4.0 - 2024-08-21](https://github.com/joke2k/faker/compare/v27.3.0...v27.4.0)
 
 * Add person provider for `pk_PK` locale. Thanks @c2-tlhah
diff --git a/VERSION b/VERSION
index 5f5a7455..811e3cfc 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-27.4.0
+28.1.0
diff --git a/docs/coding_style.rst b/docs/coding_style.rst
index fba24e91..95f88ac2 100644
--- a/docs/coding_style.rst
+++ b/docs/coding_style.rst
@@ -27,7 +27,7 @@ and/or origin of the data. If the source is a wiki, please also include the date
 We only accept new data if it's coming from statistical sources, such as census or government institutions.
 This includes names and their distribution.
 
-You may include multiple sources, but each name must come from a source. We will not accept list of names compiled
+You may include multiple sources, but each name must come from a source. We will not accept names authored
 exclusively by contributors.
 
 
diff --git a/docs/conf.py b/docs/conf.py
index 3503dbb0..9365eaa9 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -51,9 +51,9 @@ copyright = f"2014-{datetime.now().year}, Daniele Faraglia"
 # built documents.
 #
 # The short X.Y version.
-version = "27.4.0"
+version = "28.1.0"
 # The full version, including alpha/beta/rc tags.
-release = "27.4.0"
+release = "28.1.0"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/faker/__init__.py b/faker/__init__.py
index 1c236355..1716675c 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 = "27.4.0"
+VERSION = "28.1.0"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/providers/automotive/uk_UA/__init__.py b/faker/providers/automotive/uk_UA/__init__.py
index 2e933f38..933153b3 100644
--- a/faker/providers/automotive/uk_UA/__init__.py
+++ b/faker/providers/automotive/uk_UA/__init__.py
@@ -18,20 +18,20 @@ class Provider(AutomotiveProvider):
         "Kyiv_reg": (("AI", "KI", "TI", "ME"), "10"),
         "Zhytomyr": (("AM", "KM", "TM", "MV"), "06"),
         "Zakarpattia": (("AO", "KO", "MT", "MO"), "07"),
-        "Zaporizhia": (("AR", "KR", "TR", "MR"), "08"),
+        "Zaporizhzhia": (("AR", "KR", "TR", "MR"), "08"),
         "IvanoFrankivsk": (("AT", "KT", "TO", "XS"), "09"),
         "Kirovohrad": (("BA", "NA", "XA", "EA"), "12"),
         "Luhansk": (("BB", "NV", "EE", "EV"), "13"),
         "Lviv": (("BS", "NS", "SS", "ES"), "14"),
         "Mykolaiv": (("BE", "NE", "XE", "XN"), "15"),
-        "Odessa": (("BN", "NN", "OO", "EN"), "16"),
+        "Odesa": (("BN", "NN", "OO", "EN"), "16"),
         "Poltava": (("BI", "NI", "XI", "EI"), "17"),
         "Rivne": (("BK", "NK", "XK", "EK"), "18"),
         "Sumy": (("BM", "NM", "XM", "EM"), "19"),
         "Ternopil": (("BO", "NO", "XO", "EO"), "20"),
         "Kharkiv": (("AX", "KX", "XX", "EX"), "21"),
         "Kherson": (("BT", "NT", "XT", "ET"), "22"),
-        "Khmelnytsky": (("BX", "NX", "OX", "RX"), "23"),
+        "Khmelnytskyi": (("BX", "NX", "OX", "RX"), "23"),
         "Cherkasy": (("SA", "IA", "OA", "RA"), "24"),
         "Chernihiv": (("SV", "IV", "OV", "RV"), "25"),
         "Chernivtsi": (("SE", "IE", "OE", "RE"), "26"),
diff --git a/faker/providers/color/color.py b/faker/providers/color/color.py
index 47b64e0c..756942bf 100644
--- a/faker/providers/color/color.py
+++ b/faker/providers/color/color.py
@@ -145,7 +145,7 @@ class RandomColor:
             self.random = generator.random
         else:
             self.seed = seed if seed else random.randint(0, sys.maxsize)
-            self.random = random.Random(self.seed)
+            self.random = random.Random(int(self.seed))
 
     def generate(
         self,
diff --git a/faker/providers/date_time/__init__.py b/faker/providers/date_time/__init__.py
index 788d761a..0c67fb13 100644
--- a/faker/providers/date_time/__init__.py
+++ b/faker/providers/date_time/__init__.py
@@ -1426,7 +1426,7 @@ class Provider(BaseProvider):
         ),
         Country(
             timezones=[
-                "Europe/Kiev",
+                "Europe/Kyiv",
                 "Europe/Uzhgorod",
                 "Europe/Zaporozhye",
                 "Europe/Simferopol",
@@ -1435,7 +1435,7 @@ class Provider(BaseProvider):
             alpha_3_code="UKR",
             continent="Europe",
             name="Ukraine",
-            capital="Kiev",
+            capital="Kyiv",
         ),
         Country(
             timezones=["Africa/Kampala"],
diff --git a/faker/providers/isbn/__init__.py b/faker/providers/isbn/__init__.py
index cad4e81a..5f5e0493 100644
--- a/faker/providers/isbn/__init__.py
+++ b/faker/providers/isbn/__init__.py
@@ -1,19 +1,14 @@
 from typing import List, Tuple
 
-from faker.providers.isbn.rules import RegistrantRule
-
 from .. import BaseProvider
-from .isbn import ISBN, ISBN10, ISBN13
-from .rules import RULES
+from .isbn import MAX_LENGTH, ISBN10, ISBN13
+from typing import Dict, List, Tuple
+
+localized = True
 
 
 class Provider(BaseProvider):
-    """Generates fake ISBNs. ISBN rules vary across languages/regions
-    so this class makes no attempt at replicating all of the rules. It
-    only replicates the 978 EAN prefix for the English registration
-    groups, meaning the first 4 digits of the ISBN-13 will either be
-    978-0 or 978-1. Since we are only replicating 978 prefixes, every
-    ISBN-13 will have a direct mapping to an ISBN-10.
+    """Generates fake ISBNs.
 
     See https://www.isbn-international.org/content/what-isbn for the
     format of ISBNs.
@@ -21,43 +16,46 @@ class Provider(BaseProvider):
     list of rules pertaining to each prefix/registration group.
     """
 
+    rules: Dict[str, Dict[str, List[Tuple[str, str, int]]]] = {}
+
     def _body(self) -> List[str]:
         """Generate the information required to create an ISBN-10 or
         ISBN-13.
         """
-        ean: str = self.random_element(RULES.keys())
-        reg_group: str = self.random_element(RULES[ean].keys())
+        ean: str = self.random_element(self.rules.keys())
+        reg_group: str = self.random_element(self.rules[ean].keys())
 
         # Given the chosen ean/group, decide how long the
         #   registrant/publication string may be.
         # We must allocate for the calculated check digit, so
         #   subtract 1
-        reg_pub_len: int = ISBN.MAX_LENGTH - len(ean) - len(reg_group) - 1
+        reg_pub_len: int = MAX_LENGTH - len(ean) - len(reg_group) - 1
 
         # Generate a registrant/publication combination
         reg_pub: str = self.numerify("#" * reg_pub_len)
 
         # Use rules to separate the registrant from the publication
-        rules: List[RegistrantRule] = RULES[ean][reg_group]
+        rules = self.rules[ean][reg_group]
         registrant, publication = self._registrant_publication(reg_pub, rules)
         return [ean, reg_group, registrant, publication]
 
     @staticmethod
-    def _registrant_publication(reg_pub: str, rules: List[RegistrantRule]) -> Tuple[str, str]:
+    def _registrant_publication(reg_pub: str, rules: List[Tuple[str, str, int]]) -> Tuple[str, str]:
         """Separate the registration from the publication in a given
         string.
+
         :param reg_pub: A string of digits representing a registration
             and publication.
-        :param rules: A list of RegistrantRules which designate where
+        :param rules: A list of registrant rules which designate where
             to separate the values in the string.
         :returns: A (registrant, publication) tuple of strings.
         """
         for rule in rules:
-            if rule.min <= reg_pub[:-1] <= rule.max:
-                reg_len = rule.registrant_length
+            if rule[0] <= reg_pub[:-1] <= rule[1]:
+                reg_len = rule[2]
                 break
         else:
-            raise Exception("Registrant/Publication not found in registrant " "rule list.")
+            raise Exception(f"Registrant/Publication '{reg_pub}' not found in registrant rule list.")
         registrant, publication = reg_pub[:reg_len], reg_pub[reg_len:]
         return registrant, publication
 
diff --git a/faker/providers/isbn/en_US/__init__.py b/faker/providers/isbn/en_US/__init__.py
index 3df1adc4..9c12b8d6 100644
--- a/faker/providers/isbn/en_US/__init__.py
+++ b/faker/providers/isbn/en_US/__init__.py
@@ -2,4 +2,34 @@ from .. import Provider as ISBNProvider
 
 
 class Provider(ISBNProvider):
-    pass
+    rules = {
+        # EAN prefix
+        "978": {
+            # Registration group
+            "0": [
+                # Registrant rule (min, max, registrant length)
+                ("0000000", "1999999", 2),
+                ("2000000", "2279999", 3),
+                ("2280000", "2289999", 4),
+                ("2290000", "6479999", 3),
+                ("6480000", "6489999", 7),
+                ("6490000", "6999999", 3),
+                ("7000000", "8499999", 4),
+                ("8500000", "8999999", 5),
+                ("9000000", "9499999", 6),
+                ("9500000", "9999999", 7),
+            ],
+            "1": [
+                ("0000000", "0999999", 2),
+                ("1000000", "3999999", 3),
+                ("4000000", "5499999", 4),
+                ("5500000", "7319999", 5),
+                ("7320000", "7399999", 7),
+                ("7400000", "8697999", 5),
+                ("8698000", "9729999", 6),
+                ("9730000", "9877999", 4),
+                ("9878000", "9989999", 6),
+                ("9990000", "9999999", 7),
+            ],
+        },
+    }
diff --git a/faker/providers/isbn/es_ES/__init__.py b/faker/providers/isbn/es_ES/__init__.py
new file mode 100644
index 00000000..6d188089
--- /dev/null
+++ b/faker/providers/isbn/es_ES/__init__.py
@@ -0,0 +1,36 @@
+from .. import Provider as ISBNProvider
+
+class Provider(ISBNProvider):
+    rules = {
+        "978": {
+            "84": [
+                ("0000000", "0999999", 2),
+                ("1000000", "1049999", 5),
+                ("1050000", "1199999", 4),
+                ("1200000", "1299999", 6),
+                ("1300000", "1399999", 4),
+                ("1400000", "1499999", 3),
+                ("1500000", "1999999", 5),
+                ("2000000", "6999999", 3),
+                ("7000000", "8499999", 4),
+                ("8500000", "8999999", 5),
+                ("9000000", "9199999", 4),
+                ("9200000", "9239999", 6),
+                ("9240000", "9299999", 5),
+                ("9300000", "9499999", 6),
+                ("9500000", "9699999", 5),
+                ("9700000", "9999999", 4),
+            ],
+            "13": [
+                ("0000000", "0099999", 2),
+                ("0100000", "5999999", 0),
+                ("6000000", "6049999", 3),
+                ("6050000", "6999999", 0),
+                ("7000000", "7349999", 4),
+                ("7350000", "8749999", 0),
+                ("8750000", "8999999", 5),
+                ("9000000", "9899999", 0),
+                ("9900000", "9999999", 6),
+            ]
+        },
+    }
diff --git a/faker/providers/isbn/isbn.py b/faker/providers/isbn/isbn.py
index e567864d..e593b95d 100644
--- a/faker/providers/isbn/isbn.py
+++ b/faker/providers/isbn/isbn.py
@@ -5,10 +5,10 @@ ISBN numbers.
 
 from typing import Any, Optional
 
+MAX_LENGTH = 13
 
-class ISBN:
-    MAX_LENGTH = 13
 
+class ISBN:
     def __init__(
         self,
         ean: Optional[str] = None,
diff --git a/faker/providers/isbn/rules.py b/faker/providers/isbn/rules.py
deleted file mode 100644
index db5d1260..00000000
--- a/faker/providers/isbn/rules.py
+++ /dev/null
@@ -1,45 +0,0 @@
-"""
-This module exists solely to figure how long a registrant/publication
-number may be within an ISBN. The rules change based on the prefix and
-language/region. This list of rules only encapsulates the 978 prefix
-for English books. 978 is the largest and, until recently, the only
-prefix.
-
-The complete list of prefixes and rules can be found at
-https://www.isbn-international.org/range_file_generation
-"""
-
-from collections import namedtuple
-from typing import Dict, List
-
-RegistrantRule = namedtuple("RegistrantRule", ["min", "max", "registrant_length"])
-
-# Structure: RULES[`EAN Prefix`][`Registration Group`] = [Rule1, Rule2, ...]
-RULES: Dict[str, Dict[str, List[RegistrantRule]]] = {
-    "978": {
-        "0": [
-            RegistrantRule("0000000", "1999999", 2),
-            RegistrantRule("2000000", "2279999", 3),
-            RegistrantRule("2280000", "2289999", 4),
-            RegistrantRule("2290000", "6479999", 3),
-            RegistrantRule("6480000", "6489999", 7),
-            RegistrantRule("6490000", "6999999", 3),
-            RegistrantRule("7000000", "8499999", 4),
-            RegistrantRule("8500000", "8999999", 5),
-            RegistrantRule("9000000", "9499999", 6),
-            RegistrantRule("9500000", "9999999", 7),
-        ],
-        "1": [
-            RegistrantRule("0000000", "0999999", 2),
-            RegistrantRule("1000000", "3999999", 3),
-            RegistrantRule("4000000", "5499999", 4),
-            RegistrantRule("5500000", "7319999", 5),
-            RegistrantRule("7320000", "7399999", 7),
-            RegistrantRule("7400000", "8697999", 5),
-            RegistrantRule("8698000", "9729999", 6),
-            RegistrantRule("9730000", "9877999", 4),
-            RegistrantRule("9878000", "9989999", 6),
-            RegistrantRule("9990000", "9999999", 7),
-        ],
-    },
-}
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index 669efdad..d601c8d4 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -285,7 +285,7 @@ class Provider(BaseProvider):
         self,
         left_digits: Optional[int] = None,
         right_digits: Optional[int] = None,
-        positive: bool = False,
+        positive: Optional[bool] = None,
         min_value: Optional[BasicNumber] = None,
         max_value: Optional[BasicNumber] = None,
     ) -> Decimal:
@@ -320,7 +320,10 @@ class Provider(BaseProvider):
         elif max_value is not None and max_value <= 0:
             sign = "-"
         else:
-            sign = "+" if positive else self.random_element(("+", "-"))
+            if positive is None:
+                sign = self.random_element(("+", "-"))
+            else:
+                sign = "+" if positive else "-"
 
         if sign == "+":
             if max_value is not None:
diff --git a/faker/proxy.pyi b/faker/proxy.pyi
index ccd25fe2..54347af4 100644
--- a/faker/proxy.pyi
+++ b/faker/proxy.pyi
@@ -2439,7 +2439,7 @@ class Faker:
         self,
         left_digits: Optional[int] = ...,
         right_digits: Optional[int] = ...,
-        positive: bool = ...,
+        positive: Optional[bool] = ...,
         min_value: Union[float, int, None] = ...,
         max_value: Union[float, int, None] = ...,
     ) -> Decimal: ...


Test patch:
diff --git a/tests/providers/test_isbn.py b/tests/providers/test_isbn.py
index 28eb4e0b..fda1082d 100644
--- a/tests/providers/test_isbn.py
+++ b/tests/providers/test_isbn.py
@@ -2,7 +2,6 @@ import pytest
 
 from faker.providers.isbn import ISBN10, ISBN13
 from faker.providers.isbn.en_US import Provider as ISBNProvider
-from faker.providers.isbn.rules import RegistrantRule
 
 
 class TestISBN10:
@@ -37,8 +36,8 @@ class TestProvider:
     prov = ISBNProvider(None)
 
     def test_reg_pub_separation(self):
-        r1 = RegistrantRule("0000000", "0000001", 1)
-        r2 = RegistrantRule("0000002", "0000003", 2)
+        r1 = ("0000000", "0000001", 1)
+        r2 = ("0000002", "0000003", 2)
         assert self.prov._registrant_publication("00000000", [r1, r2]) == (
             "0",
             "0000000",
@@ -70,5 +69,5 @@ class TestProvider:
 
     def test_rule_not_found(self):
         with pytest.raises(Exception):
-            r = RegistrantRule("0000000", "0000001", 1)
+            r = ("0000000", "0000001", 1)
             self.prov._registrant_publication("0000002", [r])