Repository: joke2k/faker
Pull request #1828 (2023-03-21 20:32:11)

Separate freely-associated states & US territories
There are three sovereign states that are members of the Compact of Free Association. These states are not US territories, and having `state_abbr()` include them by default can cause problems with other tooling.

For the purposes of a valid deliverable address, it's useful to include these freely-associated states by default. But it's also helpful to be able to dynamically exclude them. (Furthermore, there's good reason to simply be correct about how these sovereign states are referred to).

-----

### What does this change

Exposes a custom boolean (no change in default behavior) which allows
exclusion of freely-associated states from US states & territories.

### What was wrong

We'd categorized these sovereign states as US territories.

### How this fixes it

An optional param to `state_abbr()` allows exclusion of these freely-associated
states while still preserving existing functionality.


Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 6a709ad6..896ab836 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 18.2.0
+current_version = 18.2.1
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6f97f96..31158cb2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## Changelog
 
+### [v18.2.1 - 2023-03-21](https://github.com/joke2k/faker/compare/v18.2.0...v18.2.1)
+
+* Fix value error formatting. Thanks @dariocurr.
+
 ### [v18.2.0 - 2023-03-20](https://github.com/joke2k/faker/compare/v18.1.0...v18.2.0)
 
 * Add `pyobject` generator. Thanks @dariocurr.
diff --git a/VERSION b/VERSION
index 08e3ca9b..c0aa6d4a 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-18.2.0
+18.2.1
diff --git a/docs/conf.py b/docs/conf.py
index 1fec621d..dae4a058 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -51,9 +51,9 @@ copyright = "2014, Daniele Faraglia"
 # built documents.
 #
 # The short X.Y version.
-version = "18.2.0"
+version = "18.2.1"
 # The full version, including alpha/beta/rc tags.
-release = "18.2.0"
+release = "18.2.1"
 
 # 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 2600ad63..697bb5a5 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 = "18.2.0"
+VERSION = "18.2.1"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/providers/address/en_US/__init__.py b/faker/providers/address/en_US/__init__.py
index 4bf14695..728ecab3 100644
--- a/faker/providers/address/en_US/__init__.py
+++ b/faker/providers/address/en_US/__init__.py
@@ -1,5 +1,5 @@
 from collections import OrderedDict
-from typing import Optional
+from typing import Optional, Set
 
 from ..en import Provider as AddressProvider
 
@@ -419,7 +419,8 @@ class Provider(AddressProvider):
         "WV": (24701, 26886),
         "WI": (53001, 54990),
         "WY": (82001, 83128),
-        # Territories - incomplete ranges with accurate subsets - https://www.geonames.org/postalcode-search.html
+        # Territories & freely-associated states
+        # incomplete ranges with accurate subsets - https://www.geonames.org/postalcode-search.html
         "AS": (96799, 96799),
         "FM": (96941, 96944),
         "GU": (96910, 96932),
@@ -432,16 +433,21 @@ class Provider(AddressProvider):
 
     territories_abbr = (
         "AS",
-        "FM",
         "GU",
-        "MH",
         "MP",
-        "PW",
         "PR",
         "VI",
     )
 
-    states_and_territories_abbr = states_abbr + territories_abbr
+    # Freely-associated states (sovereign states; members of COFA)
+    # https://en.wikipedia.org/wiki/Compact_of_Free_Association
+    freely_associated_states_abbr = (
+        "FM",
+        "MH",
+        "PW",
+    )
+
+    known_usps_abbr = states_abbr + territories_abbr + freely_associated_states_abbr
 
     military_state_abbr = ("AE", "AA", "AP")
 
@@ -494,16 +500,28 @@ class Provider(AddressProvider):
 
     state = administrative_unit
 
-    def state_abbr(self, include_territories: bool = True) -> str:
+    def state_abbr(
+        self,
+        include_territories: bool = True,
+        include_freely_associated_states: bool = True,
+    ) -> str:
         """
-        :returns: A random state or territory abbreviation.
+        :returns: A random two-letter USPS postal code
+
+        By default, the resulting code may abbreviate any of the fity states,
+        five US territories, or three freely-associating sovereign states.
 
         :param include_territories: If True, territories will be included.
-            If False, only states will be returned.
+            If False, US territories will be excluded.
+        :param include_freely_associated_states: If True, freely-associated states will be included.
+            If False, sovereign states in free association with the US will be excluded.
         """
+        abbreviations: Set[str] = set(self.states_abbr)
         if include_territories:
-            return self.random_element(self.states_and_territories_abbr)
-        return self.random_element(self.states_abbr)
+            abbreviations.update(self.territories_abbr)
+        if include_freely_associated_states:
+            abbreviations.update(self.freely_associated_states_abbr)
+        return self.random_element(abbreviations)
 
     def postcode(self) -> str:
         return "%05d" % self.generator.random.randint(501, 99950)
@@ -520,7 +538,7 @@ class Provider(AddressProvider):
         if state_abbr is None:
             state_abbr = self.random_element(self.states_abbr)
 
-        if state_abbr in self.states_and_territories_abbr:
+        if state_abbr in self.known_usps_abbr:
             postcode = "%d" % (
                 self.generator.random.randint(
                     self.states_postcode[state_abbr][0],
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index 0d7070d0..15aa8257 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -87,7 +87,7 @@ class Provider(BaseProvider):
         elif object_type == dict:
             return self.pydict()
         else:
-            raise ValueError("Object type `{object_type}` is not supported by `pyobject` function")
+            raise ValueError(f"Object type `{object_type}` is not supported by `pyobject` function")
 
     def pybool(self, truth_probability: int = 50) -> bool:
         """


Test patch:
diff --git a/tests/providers/test_address.py b/tests/providers/test_address.py
index 0576484e..baa34fdc 100644
--- a/tests/providers/test_address.py
+++ b/tests/providers/test_address.py
@@ -519,14 +519,29 @@ class TestEnUS:
         for _ in range(num_samples):
             state_abbr = faker.state_abbr()
             assert isinstance(state_abbr, str)
-            states_and_territories = EnUsAddressProvider.states_and_territories_abbr
+            states_and_territories = EnUsAddressProvider.known_usps_abbr
             assert state_abbr in states_and_territories
 
+    def test_state_abbr_states_only(self, faker, num_samples):
+        for _ in range(num_samples):
+            state_abbr = faker.state_abbr(include_territories=False, include_freely_associated_states=False)
+            assert isinstance(state_abbr, str)
+            assert state_abbr in EnUsAddressProvider.states_abbr
+
     def test_state_abbr_no_territories(self, faker, num_samples):
         for _ in range(num_samples):
             state_abbr = faker.state_abbr(include_territories=False)
             assert isinstance(state_abbr, str)
-            assert state_abbr in EnUsAddressProvider.states_abbr
+            assert (
+                state_abbr in EnUsAddressProvider.states_abbr
+                or state_abbr in EnUsAddressProvider.freely_associated_states_abbr
+            )
+
+    def test_state_abbr_no_freely_associated_states(self, faker, num_samples):
+        for _ in range(num_samples):
+            state_abbr = faker.state_abbr(include_freely_associated_states=False)
+            assert isinstance(state_abbr, str)
+            assert state_abbr in EnUsAddressProvider.states_abbr or state_abbr in EnUsAddressProvider.territories_abbr
 
     def test_postcode(self, faker, num_samples):
         for _ in range(num_samples):
@@ -536,7 +551,7 @@ class TestEnUS:
 
     def test_postcode_in_state(self, faker, num_samples):
         for _ in range(num_samples):
-            for state_abbr in EnUsAddressProvider.states_and_territories_abbr:
+            for state_abbr in EnUsAddressProvider.known_usps_abbr:
                 code = faker.postcode_in_state(state_abbr)
                 assert re.fullmatch(r"\d{5}", code)
                 assert int(code) >= EnUsAddressProvider.states_postcode[state_abbr][0]
@@ -553,7 +568,7 @@ class TestEnUS:
 
     def test_zipcode_in_state(self, faker, num_samples):
         for _ in range(num_samples):
-            for state_abbr in EnUsAddressProvider.states_and_territories_abbr:
+            for state_abbr in EnUsAddressProvider.known_usps_abbr:
                 code = faker.zipcode_in_state(state_abbr)
                 assert re.fullmatch(r"\d{5}", code)
                 assert int(code) >= EnUsAddressProvider.states_postcode[state_abbr][0]
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
index 6b79715c..ff30a279 100644
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -24,7 +24,7 @@ def test_pyobject(
 
 @pytest.mark.parametrize("object_type", (object, type, callable))
 def test_pyobject_with_unknown_object_type(object_type):
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match=f"Object type `{object_type}` is not supported by `pyobject` function"):
         assert Faker().pyobject(object_type=object_type)