Repository: joke2k/faker (python)

Issue:
Update address provider for `en_IN`
* Faker version: 26.0.0
* OS: all

The address provider only have basic info on states. PIN Codes and union territories aren't added in the provider


Pull request description:
Add PIN Code range and union territories in `en_IN` address provider
### What does this change

Updates the `address` provider for `en_IN` to add union territories and PIN code ranges for states

### What was wrong

No state / union territories pin code specified

### How this fixes it

Add state / union territories pin codes with ranges

Fixes #2089 


Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index b5909b52..e1438db4 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 29.0.0
+current_version = 30.0.0
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 46cefc73..c53f7f76 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -11,3 +11,8 @@ Description of the root cause of the issue.
 Description of how the changes fix the issue.
 
 Fixes #...
+
+### Checklist
+
+- [ ] I have read the documentation about [CONTRIBUTING](https://github.com/joke2k/faker/blob/master/CONTRIBUTING.rst)
+- [ ] I have run `make lint`
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7533af92..23e9ce2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## Changelog
 
+### [v30.0.0 - 2024-09-25](https://github.com/joke2k/faker/compare/v29.0.0...v30.0.0)
+
+* Force the slug always be generated with ASCII characters. Thanks @Pandede.
+
 ### [v29.0.0 - 2024-09-19](https://github.com/joke2k/faker/compare/v28.4.1...v29.0.0)
 
 * Fix `pydecimal` distribution when called with a range across `0`. Thanks @AlexLitvino.
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 35997b89..3f939bfd 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -18,8 +18,6 @@ Making Changes
 
 -  Create a topic branch from where you want to base your work.
 -  This is usually the master branch.
--  Only target release branches if you are certain your fix must be on
-   that branch.
 -  To quickly create a topic branch based on master;
    ``git branch fix/master/my_contribution master`` then checkout
    the new branch with ``git checkout fix/master/my_contribution``.
@@ -40,6 +38,7 @@ Making Changes
 Submitting Changes
 ------------------
 
+-  Make sure there isn't already a Pull Request opened by somebody else.
 -  Push your changes to a topic branch in your fork of the repository.
 -  Submit a pull request to the repository.
 
diff --git a/VERSION b/VERSION
index f1e4903d..8dd5c17a 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-29.0.0
+30.0.0
diff --git a/docs/conf.py b/docs/conf.py
index b0c46241..08027ad1 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 = "29.0.0"
+version = "30.0.0"
 # The full version, including alpha/beta/rc tags.
-release = "29.0.0"
+release = "30.0.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 93b7b527..0c6ad45c 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 = "29.0.0"
+VERSION = "30.0.0"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/providers/address/en_IN/__init__.py b/faker/providers/address/en_IN/__init__.py
index 75e6d296..43552399 100644
--- a/faker/providers/address/en_IN/__init__.py
+++ b/faker/providers/address/en_IN/__init__.py
@@ -1,4 +1,6 @@
-from .. import Provider as AddressProvider
+import random
+from typing import Optional
+from faker.providers.address import Provider as AddressProvider
 
 
 class Provider(AddressProvider):
@@ -391,6 +393,112 @@ class Provider(AddressProvider):
         "West Bengal",
     )
 
+    states_abbr = (
+        "AP",
+        "AR",
+        "AS",
+        "BR",
+        "CG",
+        "GA",
+        "GJ",
+        "HR",
+        "HP",
+        "JH",
+        "KA",
+        "KL",
+        "MP",
+        "MH",
+        "MN",
+        "ML",
+        "MZ",
+        "NL",
+        "OD",
+        "PB",
+        "RJ",
+        "SK",
+        "TN",
+        "TG",
+        "TR",
+        "UK",
+        "UP",
+        "WB",
+    )
+
+    union_territories = (
+        ("Andaman and Nicobar Islands",),
+        ("Chandigarh",),
+        ("Dadra and Nagar Haveli, Dadra & Nagar Haveli",),
+        ("Daman and Diu",),
+        ("Delhi, National Capital Territory of Delhi",),
+        ("Jammu and Kashmir",),
+        ("Ladakh",),
+        ("Lakshadweep",),
+        ("Pondicherry",),
+        ("Puducherry",),
+    )
+
+    union_territories_abbr = (
+        "AN",
+        "CH",
+        "DN",
+        "DD",
+        "DL",
+        "JK",
+        "LA",
+        "LD",
+        "PY",
+    )
+
+    # https://en.wikipedia.org/wiki/Postal_Index_Number
+
+    # FIXME: Some states such as `BR/JH` / `UK/UP` have similar PIN code ranges
+    # FIXME: as mentioned in above link.
+
+    state_pincode = {
+        "AP": [(510_000, 539_999)],
+        "AR": [(790_000, 792_999)],
+        "AS": [(780_000, 789_999)],
+        "BR": [(800_000, 859_999)],
+        "CG": [(490_000, 499_999)],
+        "GA": [(403_000, 403_999)],
+        "GJ": [(360_000, 399_999)],
+        "HR": [(120_000, 139_999)],
+        "HP": [(170_000, 179_999)],
+        "JH": [(800_000, 859_999)],
+        "KA": [(560_000, 599_999)],
+        "KL": [(670_000, 681_999), (683_000, 699_999)],
+        "MP": [(450_000, 489_999)],
+        "MH": [(400_000, 402_999), (404_000, 449_999)],
+        "MN": [(795_000, 795_999)],
+        "ML": [(793_000, 794_999)],
+        "MZ": [(796_000, 796_999)],
+        "NL": [(797_000, 798_999)],
+        "OD": [(750_000, 779_999)],
+        "PB": [(140_000, 159_999)],
+        "RJ": [(300_000, 349_999)],
+        "SK": [(737_000, 737_999)],
+        "TN": [(600_000, 669_999)],
+        "TG": [(500_000, 509_999)],
+        "TR": [(799_000, 799_999)],
+        "UK": [(200_000, 289_999)],
+        "UP": [(200_000, 289_999)],
+        "WB": [(700_000, 736_999), (738_000, 743_999), (745_000, 749_999)],
+    }
+
+    union_territories_pincode = {
+        "AN": [(744_000, 744_999)],
+        "CH": [(160_000, 169_999)],
+        "DN": [(396_000, 396_999)],
+        "DD": [(396_000, 396_999)],
+        "DL": [(110_000, 119_999)],
+        "JK": [(180_000, 199_999)],
+        "LA": [(180_000, 199_999)],
+        "LD": [(682_000, 682_999)],
+        "PY": [(605_000, 605_999)],
+    }
+
+    army_pincode = {"APS": (900_000, 999_999)}
+
     def city_name(self) -> str:
         return self.random_element(self.cities)
 
@@ -398,3 +506,71 @@ class Provider(AddressProvider):
         return self.random_element(self.states)
 
     state = administrative_unit
+
+    def union_territory(self) -> str:
+        """Returns random union territory name"""
+
+        return self.random_element(self.union_territories)[0]
+
+    def pincode_in_state(
+        self, state_abbr: Optional[str] = None, include_union_territories=False
+    ) -> int:
+        """Random PIN Code within provided state abbreviation
+
+        :param state_abbr: State Abbr, defaults to None
+        :param include_union_territories: Include Union Territories ?, defaults to False
+        :raises ValueError: If incorrect state abbr
+        :return: PIN Code
+        """
+
+        known_abbrs = self.states_abbr
+        if include_union_territories:
+            known_abbrs += self.union_territories_abbr
+
+        if state_abbr is None:
+            state_abbr = self.random_element(known_abbrs)
+
+        if state_abbr in known_abbrs:
+            codes = self.state_pincode
+            if include_union_territories:
+                codes = codes | self.union_territories_pincode
+
+            pincode_range = self.random_element(codes[state_abbr])
+
+            return self.generator.random.randint(*pincode_range)
+
+        raise ValueError("State Abbreviation not found in list")
+
+    def pincode_in_military(self) -> int:
+        """Random PIN Code within Army Postal Service range"""
+
+        key = self.random_element(self.army_pincode.keys())
+
+        return self.generator.random.randint(*self.army_pincode[key])
+
+    # Aliases
+
+    def zipcode_in_state(
+        self, state_abbr: Optional[str] = None, include_union_territories=False
+    ) -> int:
+        return self.pincode_in_state(state_abbr, include_union_territories)
+
+    def postcode_in_state(
+        self, state_abbr: Optional[str] = None, include_union_territories=False
+    ) -> int:
+        return self.pincode_in_state(state_abbr, include_union_territories)
+
+    def pincode_in_army(self) -> int:
+        return self.pincode_in_military()
+
+    def zipcode_in_military(self) -> int:
+        return self.pincode_in_military()
+
+    def zipcode_in_army(self) -> int:
+        return self.pincode_in_military()
+
+    def postcode_in_military(self) -> int:
+        return self.pincode_in_military()
+
+    def postcode_in_army(self) -> int:
+        return self.pincode_in_military()
diff --git a/faker/providers/internet/__init__.py b/faker/providers/internet/__init__.py
index 1d84b928..d4a20a58 100644
--- a/faker/providers/internet/__init__.py
+++ b/faker/providers/internet/__init__.py
@@ -5,6 +5,7 @@ from ...decode import unidecode
 from ...utils.decorators import lowercase, slugify, slugify_unicode
 from ...utils.distribution import choices_distribution
 from .. import BaseProvider, ElementsType
+from ..lorem.en_US import Provider as USLoremProvider
 
 localized = True
 
@@ -728,7 +729,11 @@ class Provider(BaseProvider):
     def slug(self, value: Optional[str] = None) -> str:
         """Django algorithm"""
         if value is None:
-            value = self.generator.text(20)
+            # Resolve https://github.com/joke2k/faker/issues/2103
+            # Always generate slug with ASCII characters, regardless of locale
+            ext_word_list = USLoremProvider.word_list
+
+            value = self.generator.text(20, ext_word_list=ext_word_list)
         return value
 
     def image_url(


Test patch:
diff --git a/tests/providers/test_address.py b/tests/providers/test_address.py
index 6f5b3e41..1c229cdc 100644
--- a/tests/providers/test_address.py
+++ b/tests/providers/test_address.py
@@ -1865,17 +1865,58 @@ class TestEnIn:
     """Test en_IN address provider methods"""
 
     def test_city_name(self, faker, num_samples):
+        """Tests `city names` are fetched correctly"""
+
         for _ in range(num_samples):
             city_name = faker.city_name()
             assert isinstance(city_name, str)
             assert city_name in EnInAddressProvider.cities
 
     def test_state(self, faker, num_samples):
+        """Tests `states` are fetched correctly"""
+
         for _ in range(num_samples):
             state = faker.state()
             assert isinstance(state, str)
             assert state in EnInAddressProvider.states
 
+    def test_union_territories(self, faker, num_samples):
+        """Tests `union_territories` are fetched correctly"""
+
+        for _ in range(num_samples):
+            union_territory = faker.union_territory()
+            assert isinstance(union_territory, str)
+            assert (union_territory,) in EnInAddressProvider.union_territories
+
+    @pytest.mark.parametrize("pincodes", ["pincode_in_state", "zipcode_in_state", "postcode_in_state"])
+    def test_pincodes_in_state(self, faker, num_samples, pincodes):
+        """Test `pincodes` for state and union territories"""
+
+        for _ in range(num_samples):
+            include_ut = faker.random_element([True, False])
+            pincode = getattr(faker, pincodes)(include_union_territories=include_ut)
+            assert isinstance(pincode, int)
+            assert len(str(pincode)) == 6
+
+    @pytest.mark.parametrize(
+        "pincodes",
+        [
+            ("pincode_in_army"),
+            ("zipcode_in_army"),
+            ("postcode_in_army"),
+            ("postcode_in_military"),
+            ("zipcode_in_military"),
+            ("pincode_in_military"),
+        ],
+    )
+    def test_pincodes_in_military(self, faker, num_samples, pincodes):
+        """Test `pincodes` for Army"""
+
+        for _ in range(num_samples):
+            pincode = getattr(faker, pincodes)()
+            assert isinstance(pincode, int)
+            assert len(str(pincode)) == 6
+
 
 class TestSkSk:
     """Test sk_SK address provider methods"""
diff --git a/tests/providers/test_internet.py b/tests/providers/test_internet.py
index 3bd431fe..4e0ecc69 100644
--- a/tests/providers/test_internet.py
+++ b/tests/providers/test_internet.py
@@ -477,6 +477,11 @@ class TestJaJp:
         tld = faker.tld()
         assert isinstance(tld, str)
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestZhCn:
     """Test zh_CN internet provider methods"""
@@ -647,6 +652,11 @@ class TestZhCn:
         with pytest.raises(ValueError):
             faker.domain_name(levels=0)
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestZhTw:
     """Test zh_TW internet provider methods"""
@@ -655,6 +665,11 @@ class TestZhTw:
         email = faker.email()
         validate_email(email)
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestHuHu:
     """Test hu_HU internet provider methods"""
@@ -667,6 +682,11 @@ class TestHuHu:
         email = faker.email()
         assert isinstance(email, str)
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestPlPl:
     """Test pl_PL internet provider methods"""
@@ -679,6 +699,11 @@ class TestPlPl:
         tld = faker.tld()
         assert tld in PlPlInternetProvider.tlds
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestNlNl:
     """Test nl_NL internet provider methods"""
@@ -710,6 +735,11 @@ class TestNlNl:
         validate_email(email)
         assert email.split("@")[0] == "fabienne"
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestArAa:
     """Test ar_AA internet provider methods"""
@@ -741,6 +771,11 @@ class TestArAa:
         validate_email(email)
         assert email.split("@")[0] == "asyl"
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestAzAz:
     """Test az_AZ internet provider methods"""
@@ -762,6 +797,11 @@ class TestAzAz:
         tld = faker.tld()
         assert tld in AzAzInternetProvider.tlds
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestPtBr:
     """Test pt_BR internet provider methods"""
@@ -793,6 +833,11 @@ class TestPtBr:
         validate_email(email)
         assert email.split("@")[0] == "andrecaua"
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestEnPh:
     """Test en_PH internet provider methods"""
@@ -804,17 +849,28 @@ class TestEnPh:
             domain = faker.domain_name()
             validate_domain(domain)
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestFilPh(TestEnPh):
     """Test fil_PH internet provider methods"""
 
-    pass
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
 
 
 class TestTlPh(TestFilPh):
     """Test tl_PH internet provider methods"""
 
-    pass
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
 
 
 class TestEnGb:
@@ -828,6 +884,11 @@ class TestEnGb:
         tld = faker.tld()
         assert tld in EnGbInternetProvider.tlds
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestEsEs:
     """Tests for the es_ES locale."""
@@ -836,6 +897,11 @@ class TestEsEs:
         tld = faker.tld()
         assert tld in EsEsInternetProvider.tlds
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestRoRo:
     """Test ro_RO internet provider methods"""
@@ -848,6 +914,11 @@ class TestRoRo:
         tld = faker.tld()
         assert tld in PlPlInternetProvider.tlds
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestRuRu:
     """Test ru_RU internet provider methods"""
@@ -885,6 +956,11 @@ class TestRuRu:
         validate_email(email)
         assert email.split("@")[0] == "sergekuznetsov"
 
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""
+
 
 class TestThTh:
     """Test th_TH internet provider methods"""
@@ -892,3 +968,8 @@ class TestThTh:
     def test_tld(self, faker):
         tld = faker.tld()
         assert tld in ThThInternetProvider.tlds
+
+    def test_slug(self, faker):
+        num_of_samples = 100
+        for _ in range(num_of_samples):
+            assert faker.slug() != ""