Repository: joke2k/faker
Pull request #1773 (2022-12-29 15:23:15)

Fix siret and siren generation for fr_FR locale company information
Hi !

This is a new attempt to fix the French SIRET/SIREN generation.

The correction was first submitted in MR #1465 in 2021 but never merged

I simply did a rebase from the master branch and I added basic tests



daamien: > can you run `make lint` to fix the formatting issues?

done !

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 666bf7bb..8ff7e584 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 16.3.0
+current_version = 16.6.0
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f83145a1..0a112e0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 ## Changelog
 
+### [v16.6.0 - 2023-01-17](https://github.com/joke2k/faker/compare/v16.5.0...v16.6.0)
+
+* Add `fr_CA` address provider. Thanks @davebulaval.
+
+### [v16.5.0 - 2023-01-17](https://github.com/joke2k/faker/compare/v16.4.0...v16.5.0)
+
+* Add `zipcode`, `postcode` and `postalcode` to `pl_PL`. Thanks @bukowa.
+
+### [v16.4.0 - 2023-01-12](https://github.com/joke2k/faker/compare/v16.3.0...v16.4.0)
+
+* Update phone number formats for `es_ES` locale. Thanks @GuillemCalidae.
+
 ### [v16.3.0 - 2023-01-10](https://github.com/joke2k/faker/compare/v16.2.0...v16.3.0)
 
 * Add new `fr_BE` and `nl_BE` `Person` providers. Thanks @Dutcho.
diff --git a/VERSION b/VERSION
index d9a8a357..bd015b90 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-16.3.0
+16.6.0
diff --git a/docs/communityproviders.rst b/docs/communityproviders.rst
index fc211029..e458f1ed 100644
--- a/docs/communityproviders.rst
+++ b/docs/communityproviders.rst
@@ -18,6 +18,9 @@ Here's a list of Providers written by the community:
 | Credit Score  | Fake credit score data    | `faker_credit_score`_            |
 |               | for testing purposes      |                                  |
 +---------------+---------------------------+----------------------------------+
+| Datasets      | Build providers based     | `faker-datasets`_                |
+|               | on datasets               |                                  |
++---------------+---------------------------+----------------------------------+
 | Education     | Public school name and    | `faker_education`_               |
 |               | info for testing purposes |                                  |
 +---------------+---------------------------+----------------------------------+
@@ -71,6 +74,7 @@ In order to be included, your provider must satisfy these requirement:
 .. _faker_airtravel: https://pypi.org/project/faker_airtravel/
 .. _faker_biology: https://pypi.org/project/faker_biology/
 .. _faker_credit_score: https://pypi.org/project/faker-credit-score/
+.. _faker-datasets: https://pypi.org/project/faker-datasets/
 .. _faker_education: https://pypi.org/project/faker_education/
 .. _faker-file: https://pypi.org/project/faker-file/
 .. _faker_geoscience: https://pypi.org/project/faker-geoscience/
diff --git a/docs/conf.py b/docs/conf.py
index 4c708316..d9675462 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 = "16.3.0"
+version = "16.6.0"
 # The full version, including alpha/beta/rc tags.
-release = "16.3.0"
+release = "16.6.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 7e38dd51..1c9e8234 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 = "16.3.0"
+VERSION = "16.6.0"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/providers/address/en_CA/__init__.py b/faker/providers/address/en_CA/__init__.py
index 4602bb0e..f2ac4caf 100644
--- a/faker/providers/address/en_CA/__init__.py
+++ b/faker/providers/address/en_CA/__init__.py
@@ -2,6 +2,8 @@ import re
 
 from typing import Optional
 
+from faker.providers import ElementsType
+
 from ..en import Provider as AddressProvider
 
 
@@ -33,9 +35,9 @@ class Provider(AddressProvider):
         "Y",
     )
 
-    city_prefixes = ("North", "East", "West", "South", "New", "Lake", "Port")
+    city_prefixes: ElementsType[str] = ("North", "East", "West", "South", "New", "Lake", "Port")
 
-    city_suffixes = (
+    city_suffixes: ElementsType[str] = (
         "town",
         "ton",
         "land",
@@ -337,7 +339,7 @@ class Provider(AddressProvider):
         "YT": ["Y"],
     }
 
-    city_formats = (
+    city_formats: ElementsType[str] = (
         "{{city_prefix}} {{first_name}}{{city_suffix}}",
         "{{city_prefix}} {{first_name}}",
         "{{first_name}}{{city_suffix}}",
diff --git a/faker/providers/address/fr_CA/__init__.py b/faker/providers/address/fr_CA/__init__.py
new file mode 100644
index 00000000..40a5ba13
--- /dev/null
+++ b/faker/providers/address/fr_CA/__init__.py
@@ -0,0 +1,81 @@
+from typing import Any
+
+from ..en_CA import Provider as EnCaProvider
+
+
+class Provider(EnCaProvider):
+    #  Most of the parts are identical to en_CA, we simply override those who are not shared between the two.
+
+    city_prefixes = (
+        "Ville",
+        "Baie",
+        "Saint-",
+        "Sainte-",
+        "Mont-",
+        "La",
+        "Lac-",
+        "L'",
+        "L'Île-",
+    )
+
+    city_suffixes = (
+        "Est",
+        "Ouest",
+        "-sur-Mer",
+    )
+
+    street_prefixes = (
+        "rue",
+        "rue",
+        "chemin",
+        "avenue",
+        "boulevard",
+        "route",
+        "rang",
+        "allé",
+        "montée",
+    )
+
+    provinces = (
+        "Alberta",
+        "Colombie-Britannique",
+        "Manitoba",
+        "Nouveau-Brunswick",
+        "Terre-Neuve-et-Labrador",
+        "Territoires du Nord-Ouest",
+        "Nouvelle-Écosse",
+        "Nunavut",
+        "Ontario",
+        "Île-du-Prince-Édouard",
+        "Québec",
+        "Saskatchewan",
+        "Yukon",
+    )
+
+    street_name_formats = (
+        "{{street_prefix}} {{first_name}}",
+        "{{street_prefix}} {{last_name}}",
+    )
+
+    city_formats = (
+        "{{city_prefix}} {{last_name}}",
+        "{{city_prefix}} {{last_name}}",
+        "{{city_prefix}}-{{city_prefix}}-{{last_name}}",
+        "{{city_prefix}} {{first_name}} {{city_suffix}}",
+        "{{city_prefix}} {{first_name}}",
+        "{{city_prefix}} {{first_name}}",
+        "{{city_prefix}} {{first_name}}",
+        "{{last_name}}",
+        "{{last_name}}",
+        "{{first_name}} {{city_suffix}}",
+        "{{last_name}} {{city_suffix}}",
+    )
+
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        super().__init__(*args, **kwargs)
+
+    def street_prefix(self) -> str:
+        """
+        :example: 'rue'
+        """
+        return self.random_element(self.street_prefixes)
diff --git a/faker/providers/address/pl_PL/__init__.py b/faker/providers/address/pl_PL/__init__.py
index e993e5d5..84ac931a 100644
--- a/faker/providers/address/pl_PL/__init__.py
+++ b/faker/providers/address/pl_PL/__init__.py
@@ -675,4 +675,22 @@ class Provider(AddressProvider):
         """
         return self.random_element(self.regions)
 
+    def postcode(self) -> str:
+        """
+        :example: '62-200'
+        """
+        return "%02d-%03d" % (self.generator.random.randint(1, 99), self.generator.random.randint(1, 999))
+
+    def zipcode(self) -> str:
+        """
+        :example: '62-200'
+        """
+        return self.postcode()
+
+    def postalcode(self) -> str:
+        """
+        :example: '62-200'
+        """
+        return self.postcode()
+
     region = administrative_unit
diff --git a/faker/providers/company/fr_FR/__init__.py b/faker/providers/company/fr_FR/__init__.py
index b8115cf4..8cafce67 100644
--- a/faker/providers/company/fr_FR/__init__.py
+++ b/faker/providers/company/fr_FR/__init__.py
@@ -1,5 +1,7 @@
 from typing import Tuple
 
+from faker.utils.checksums import calculate_luhn
+
 from .. import Provider as CompanyProvider
 
 
@@ -121,9 +123,11 @@ class Provider(CompanyProvider):
 
     def siren(self) -> str:
         """
-        Generates a siren number (9 digits).
+        Generates a siren number (9 digits). Formatted as '### ### ###'.
         """
-        return self.numerify(self.siren_format)
+        code = self.numerify("########")
+        luhn_checksum = str(calculate_luhn(float(code)))
+        return f"{code[:3]} {code[3:6]} {code[6:]}{luhn_checksum}"
 
     def siret(self, max_sequential_digits: int = 2) -> str:
         """
@@ -131,10 +135,15 @@ class Provider(CompanyProvider):
         It is in fact the result of the concatenation of a siren number (9 digits),
         a sequential number (4 digits) and a control number (1 digit) concatenation.
         If $max_sequential_digits is invalid, it is set to 2.
-        :param max_sequential_digits: The maximum number of digits for the sequential number (> 0 && <= 4).
+
+        The siret number is formatted as '### ### ### #####'.
+        :param max_sequential_digits The maximum number of digits for the sequential number (> 0 && <= 4).
         """
         if max_sequential_digits > 4 or max_sequential_digits <= 0:
             max_sequential_digits = 2
 
         sequential_number = str(self.random_number(max_sequential_digits)).zfill(4)
-        return self.numerify(self.siren() + " " + sequential_number + "#")
+
+        code = self.siren().replace(" ", "") + sequential_number
+        luhn_checksum = str(calculate_luhn(float(code)))
+        return f"{code[:3]} {code[3:6]} {code[6:9]} {code[9:]}{luhn_checksum}"


Test patch:
diff --git a/tests/providers/test_address.py b/tests/providers/test_address.py
index 120dee66..d9cf2e8b 100644
--- a/tests/providers/test_address.py
+++ b/tests/providers/test_address.py
@@ -26,6 +26,7 @@ from faker.providers.address.es_ES import Provider as EsEsAddressProvider
 from faker.providers.address.es_MX import Provider as EsMxAddressProvider
 from faker.providers.address.fa_IR import Provider as FaIrAddressProvider
 from faker.providers.address.fi_FI import Provider as FiFiAddressProvider
+from faker.providers.address.fr_CA import Provider as FrCaAddressProvider
 from faker.providers.address.fr_CH import Provider as FrChAddressProvider
 from faker.providers.address.fr_FR import Provider as FrFrAddressProvider
 from faker.providers.address.he_IL import Provider as HeIlAddressProvider
@@ -2220,3 +2221,65 @@ class TestUkUa:
             region = faker.region()
             assert isinstance(region, str)
             assert region in UkUaAddressProvider.region_names
+
+
+class TestFrCa:
+    """Test fr_CA address provider methods"""
+
+    def test_province(self, faker, num_samples):
+        for _ in range(num_samples):
+            province = faker.province()
+            assert isinstance(province, str)
+            assert province in FrCaAddressProvider.provinces
+
+    def test_province_abbr(self, faker, num_samples):
+        for _ in range(num_samples):
+            province_abbr = faker.province_abbr()
+            assert isinstance(province_abbr, str)
+            assert province_abbr in FrCaAddressProvider.provinces_abbr
+
+    def test_city_prefixes(self, faker, num_samples):
+        for _ in range(num_samples):
+            city_prefix = faker.city_prefix()
+            assert isinstance(city_prefix, str)
+            assert city_prefix in FrCaAddressProvider.city_prefixes
+
+    def test_city_suffixes(self, faker, num_samples):
+        for _ in range(num_samples):
+            city_suffixes = faker.city_suffix()
+            assert isinstance(city_suffixes, str)
+            assert city_suffixes in FrCaAddressProvider.city_suffixes
+
+    def test_street_prefixes(self, faker, num_samples):
+        for _ in range(num_samples):
+            street_prefix = faker.street_prefix()
+            assert isinstance(street_prefix, str)
+            assert street_prefix in FrCaAddressProvider.street_prefixes
+
+    def test_administrative_unit(self, faker, num_samples):
+        for _ in range(num_samples):
+            province = faker.administrative_unit()
+            assert isinstance(province, str)
+            assert province in FrCaAddressProvider.provinces
+
+
+class TestPlPl:
+    """Test pl_PL address provider methods"""
+
+    def test_postcode(self, faker, num_samples):
+        for _ in range(num_samples):
+            postcode = faker.postcode()
+            match = re.findall(r"^\d{2}-\d{3}$", postcode)
+            assert match
+
+    def test_zipcode(self, faker, num_samples):
+        for _ in range(num_samples):
+            zipcode = faker.zipcode()
+            match = re.findall(r"^\d{2}-\d{3}$", zipcode)
+            assert match
+
+    def test_postalcode(self, faker, num_samples):
+        for _ in range(num_samples):
+            postalcode = faker.postalcode()
+            match = re.findall(r"^^\d{2}-\d{3}$$", postalcode)
+            assert match
diff --git a/tests/providers/test_company.py b/tests/providers/test_company.py
index 6df034b1..85dd1ec3 100644
--- a/tests/providers/test_company.py
+++ b/tests/providers/test_company.py
@@ -23,6 +23,7 @@ from faker.providers.company.ru_RU import Provider as RuRuCompanyProvider
 from faker.providers.company.ru_RU import calculate_checksum
 from faker.providers.company.th_TH import Provider as ThThCompanyProvider
 from faker.providers.company.tr_TR import Provider as TrTrCompanyProvider
+from faker.utils.checksums import luhn_checksum
 
 
 class TestAzAz:
@@ -60,6 +61,22 @@ class TestFiFi:
             assert self._has_valid_checksum(company_id)
 
 
+class TestFrFr:
+    """Test fr_FR company provider methods"""
+
+    def test_siren(self, faker, num_samples):
+        for _ in range(num_samples):
+            siren = faker.siren()
+            assert len(siren) == 11
+            assert luhn_checksum(siren.replace(" ", "")) == 0
+
+    def test_siret(self, faker, num_samples):
+        for _ in range(num_samples):
+            siret = faker.siret()
+            assert len(siret) == 17
+            assert luhn_checksum(siret.replace(" ", "")) == 0
+
+
 class TestHyAm:
     """Test hy_AM company provider methods"""