Repository: joke2k/faker
Pull request #1952 (2023-11-17 19:42:29)

Add official support for python 3.12
### What does this change

Add Python 3.12 support

### What was wrong

Nothing! 
(just a remaining Python 3.7 trove classifier in setup.py)

### How this fixes it

Adding 3.12 in CI + setup.py, that's all!

Fixes #...


stefan6419846: How is dropping PyPy testing related to Python 3.12?
pfouque: It is not 😅
It generated a segmentation fault on the CI so I wanted to check if the CI passes without.
I was also wondering why is it the only pypi version tested
pfouque: Ok, it worked fine this time! 🎊 

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f747d851..017b4a0f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -138,7 +138,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python: ["3.8", "3.9", "3.10", "3.11"]
+        python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
 
     steps:
     - uses: actions/checkout@v4
@@ -168,7 +168,7 @@ jobs:
     needs: [flake8, isort, black, doc8, checkmanifest, typing]
     strategy:
       matrix:
-        python: ["3.8", "3.9", "3.10", "3.11", "pypy-3.9"]
+        python: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.9"]
 
     steps:
       - uses: actions/checkout@v4
@@ -209,7 +209,7 @@ jobs:
     needs: [flake8, isort, black, doc8, checkmanifest, typing]
     strategy:
       matrix:
-        python: ["3.8", "3.9", "3.10", "3.11"]
+        python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
         arch: ["x86", "x64"]
 
     steps:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30e85ee0..3b4600ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## Changelog
 
+### Unreleased
+
+* Add official support for Python 3.12
+
 ### [v20.0.3 - 2023-11-14](https://github.com/joke2k/faker/compare/v20.0.2...v20.0.3)
 
 * Make `unix_time` always return floats.
diff --git a/faker/providers/company/es_ES/__init__.py b/faker/providers/company/es_ES/__init__.py
new file mode 100644
index 00000000..0306b52d
--- /dev/null
+++ b/faker/providers/company/es_ES/__init__.py
@@ -0,0 +1,127 @@
+from collections import OrderedDict
+
+from .. import Provider as CompanyProvider
+
+
+class Provider(CompanyProvider):
+    """
+    Provider for company names for es_ES locale
+
+    Company naming scheme and probabilities are inspired by and/or based on existing companies in Spain.
+
+    Sources:
+    - https://en.wikipedia.org/wiki/List_of_legal_entity_types_by_country
+    - https://ranking-empresas.eleconomista.es/ranking_empresas_nacional.html
+    """
+
+    formats = (
+        "{{company_prefix}} {{last_name}} {{company_suffix}}",
+        "{{company_type}} {{random_company_acronym}} {{company_suffix}}",
+        "{{company_type}} {{last_name}} {{company_suffix}}",
+        "{{company_type}} {{random_company_adjective}} {{company_suffix}}",
+        "{{company_type}} {{last_name}} {{random_name_complements}} {{company_suffix}}",
+        "{{last_name}} {{random_name_complements}} {{company_suffix}}",
+        "{{last_name}} y {{last_name}} {{company_suffix}}",
+        "{{first_name}} {{last_name}} {{last_name}} {{company_suffix}}",
+    )
+
+    company_suffixes = OrderedDict(
+        [
+            ("S.A.", 0.19860906),
+            ("S.A.D", 0.01020618),
+            ("S.A.T.", 0.02307813),
+            ("S.A.U", 0.01506562),
+            ("S.C.P", 0.04465719),
+            ("S.Com.", 0.15636432),
+            ("S.Coop.", 0.17394866),
+            ("S.L.", 0.18325857),
+            ("S.L.L.", 0.05800693),
+            ("S.L.N.E", 0.11496705),
+            ("S.L.U.", 0.02183831),
+        ]
+    )
+
+    company_prefixes = (
+        "Familia",
+        "Grupo",
+        "Hermanos",
+        "Hnos",
+    )
+
+    company_types = (
+        "Alimentación",
+        "Banca Privada",
+        "Banco",
+        "Comercial",
+        "Comercializadora",
+        "Compañía",
+        "Construcción",
+        "Consultoría",
+        "Desarrollo",
+        "Despacho",
+        "Distribuciones",
+        "Farmaceútica",
+        "Finanzas",
+        "Fábrica",
+        "Hotel",
+        "Industrias",
+        "Infraestructuras",
+        "Inmobiliaria",
+        "Instalaciones",
+        "Inversiones",
+        "Logística",
+        "Manufacturas",
+        "Minería",
+        "Promociones",
+        "Restauración",
+        "Servicios",
+        "Soluciones",
+        "Suministros",
+        "Supermercados",
+        "Talleres",
+        "Tecnologías",
+        "Transportes",
+    )
+
+    name_complements = (
+        "& Asociados",
+        "y asociados",
+    )
+
+    company_adjectives = (
+        "Avanzadas",
+        "Castellana",
+        "Española",
+        "Españolas",
+        "Globales",
+        "Iberia",
+        "Ibérica",
+        "Ibéricos",
+        "Integrales",
+        "Inteligentes",
+        "Internacionales",
+        "del Levante",
+        "del Mediterráneo",
+        "del Noroeste",
+        "del Norte",
+        "del Sur",
+    )
+
+    def company_type(self) -> str:
+        return self.random_element(self.company_types)
+
+    def company_suffix(self) -> str:
+        return self.random_element(self.company_suffixes)
+
+    def random_name_complements(self) -> str:
+        return self.random_element(self.name_complements)
+
+    def random_company_adjective(self) -> str:
+        return self.random_element(self.company_adjectives)
+
+    def random_company_acronym(self) -> str:
+        letters = self.random_letters(self.random_int(2, 4))
+        return "".join(letters).upper()
+
+    def company_prefix(self) -> str:
+        return self.random_element(self.company_prefixes)
diff --git a/faker/providers/ssn/es_ES/__init__.py b/faker/providers/ssn/es_ES/__init__.py
index 029d26da..0d321897 100644
--- a/faker/providers/ssn/es_ES/__init__.py
+++ b/faker/providers/ssn/es_ES/__init__.py
@@ -61,6 +61,20 @@ class Provider(BaseProvider):
 
         return random.choice([self.cif, self.nie, self.nif])()
 
+    def nuss(self, company: bool = False) -> str:
+        """
+        :param company: flag to indicate if we should generate a company NUSS
+        :return: a random Spanish Social Security Number (Número de la Seguridad Social)
+        """
+        nuss_body_length = 8
+        if company:
+            nuss_body_length = 7
+        province_digits = f"{random.choice(list(range(1, 54)) + [66]):02d}"
+        nuss_body = "".join(str(random.randint(0, 9)) for _ in range(nuss_body_length))
+        control_digits = f"{int(province_digits+nuss_body) % 97:02d}"
+        nuss = f"{province_digits}{nuss_body}{control_digits}"
+        return nuss
+
     @staticmethod
     def _calculate_control_doi(doi: str) -> str:
         """
diff --git a/setup.py b/setup.py
index be4c318e..74941768 100644
--- a/setup.py
+++ b/setup.py
@@ -41,11 +41,11 @@ setup(
         "Programming Language :: Python",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3 :: Only",
-        "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
         "Programming Language :: Python :: Implementation :: CPython",
         "Programming Language :: Python :: Implementation :: PyPy",
         "Topic :: Software Development :: Libraries :: Python Modules",
diff --git a/tox.ini b/tox.ini
index 6073b179..0037915f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist=py{38,39,310,311,py3},alpine,flake8,checkmanifest,isort,mypy,doc8
+envlist=py{38,39,310,311,312,py3},alpine,flake8,checkmanifest,isort,mypy,doc8
 skip_missing_interpreters = true
 
 [testenv]


Test patch:
diff --git a/tests/providers/test_company.py b/tests/providers/test_company.py
index 13be6705..33a007b3 100644
--- a/tests/providers/test_company.py
+++ b/tests/providers/test_company.py
@@ -9,6 +9,7 @@ import pytest
 from faker.providers.company.az_AZ import Provider as AzAzCompanyProvider
 from faker.providers.company.el_GR import Provider as ElGrCompanyProvider
 from faker.providers.company.en_PH import Provider as EnPhCompanyProvider
+from faker.providers.company.es_ES import Provider as EsEsCompanyProvider
 from faker.providers.company.fil_PH import Provider as FilPhCompanyProvider
 from faker.providers.company.hu_HU import Provider as HuHuCompanyProvider
 from faker.providers.company.hy_AM import Provider as HyAmCompanyProvider
@@ -476,3 +477,18 @@ class TestNlBe:
             suffix = faker.company_suffix()
             assert isinstance(suffix, str)
             assert suffix in NlBeCompanyProvider.company_suffixes
+
+
+class TestEsEs:
+    """Test esE_ES company provider methods"""
+
+    def test_company_suffix(self, faker, num_samples):
+        for _ in range(num_samples):
+            suffix = faker.company_suffix()
+            assert isinstance(suffix, str)
+            assert suffix in list(EsEsCompanyProvider.company_suffixes.keys())
+
+    def test_company(self, faker, num_samples):
+        for _ in range(num_samples):
+            company = faker.company()
+            assert isinstance(company, str)
diff --git a/tests/providers/test_ssn.py b/tests/providers/test_ssn.py
index 0672c086..919eed86 100644
--- a/tests/providers/test_ssn.py
+++ b/tests/providers/test_ssn.py
@@ -623,6 +623,16 @@ class TestEsES(unittest.TestCase):
     def test_doi(self):
         assert len(self.fake.doi()) == 9
 
+    def test_nuss(self):
+        for _ in range(50):
+            nuss = self.fake.nuss()
+            assert isinstance(nuss, str)
+            assert 12 == len(nuss)
+        for _ in range(50):
+            nuss = self.fake.nuss(company=True)
+            assert isinstance(nuss, str)
+            assert 11 == len(nuss)
+
 
 class TestEsCA(TestEsES):
     def setUp(self):