Repository: joke2k/faker
Pull request #1949 (2023-11-14 09:38:59)

add company provider for es_ES locale
### What does this change

Adds company provider for es_ES locale and also adds a function to generate NUSS (social security numbers) in ssn provider for es_ES locale

### What was wrong

There wasn't a company provider for es_ES locale



Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index cfbbdfee..dc61d014 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 20.0.0
+current_version = 20.0.3
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76e24f31..30e85ee0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,20 @@
 ## Changelog
 
+### [v20.0.3 - 2023-11-14](https://github.com/joke2k/faker/compare/v20.0.2...v20.0.3)
+
+* Make `unix_time` always return floats.
+
+### [v20.0.2 - 2023-11-14](https://github.com/joke2k/faker/compare/v20.0.1...v20.0.2)
+
+* Fix `pydecimal` crash on float `min_value` or `max_value`. Thanks @s-weigand.
+
+### [v20.0.1 - 2023-11-14](https://github.com/joke2k/faker/compare/v20.0.0...v20.0.1)
+
+* Fix type hint for `unix_time`.
+
 ### [v20.0.0 - 2023-11-10](https://github.com/joke2k/faker/compare/v19.13.0...v20.0.0)
 
-* Support platform-specific second precision. Thanks @@cknv.
+* Support platform-specific second precision. Thanks @cknv.
 
 ### [v19.13.0 - 2023-11-01](https://github.com/joke2k/faker/compare/v19.12.1...v19.13.0)
 
diff --git a/README.rst b/README.rst
index 9fa5472e..8765eee0 100644
--- a/README.rst
+++ b/README.rst
@@ -478,9 +478,9 @@ Credits
     :target: https://coveralls.io/r/joke2k/faker?branch=master
     :alt: Test coverage
 
-.. |build| image:: https://github.com/joke2k/faker/workflows/Python%20Tests/badge.svg?branch=master&event=push
-    :target: https://github.com/joke2k/faker/actions?query=workflow%3A%22Python+Tests%22+branch%3Amaster+event%3Apush
-    :alt: Build status of the master branch on Mac/Linux
+.. |build| image:: https://github.com/joke2k/faker/actions/workflows/ci.yml/badge.svg
+    :target: https://github.com/joke2k/faker/actions/workflows/ci.yml
+    :alt: Build status of the master branch
 
 .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
     :target: https://raw.githubusercontent.com/joke2k/faker/master/LICENSE.txt
diff --git a/VERSION b/VERSION
index e88320d7..a9ac8dac 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-20.0.0
+20.0.3
diff --git a/docs/conf.py b/docs/conf.py
index 0c67eb9a..21b49f92 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 = "20.0.0"
+version = "20.0.3"
 # The full version, including alpha/beta/rc tags.
-release = "20.0.0"
+release = "20.0.3"
 
 # 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 204c05dd..1886fc22 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 = "20.0.0"
+VERSION = "20.0.3"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/providers/company/es_ES/__init__.py b/faker/providers/company/es_ES/__init__.py
new file mode 100644
index 00000000..ab3cbde3
--- /dev/null
+++ b/faker/providers/company/es_ES/__init__.py
@@ -0,0 +1,128 @@
+import random
+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/date_time/__init__.py b/faker/providers/date_time/__init__.py
index b3a2197f..14997342 100644
--- a/faker/providers/date_time/__init__.py
+++ b/faker/providers/date_time/__init__.py
@@ -80,13 +80,13 @@ class Provider(BaseProvider):
     if platform.system() == "Windows":
 
         @property
-        def _rand_seconds(self):
+        def _rand_seconds(self) -> Callable[[int, int], int]:
             return self.generator.random.randint
 
     else:
 
         @property
-        def _rand_seconds(self):
+        def _rand_seconds(self) -> Callable[[float, float], float]:
             return self.generator.random.uniform
 
     centuries: ElementsType[str] = [
@@ -1821,16 +1821,18 @@ class Provider(BaseProvider):
         self,
         end_datetime: Optional[DateParseType] = None,
         start_datetime: Optional[DateParseType] = None,
-    ) -> int:
+    ) -> float:
         """
         Get a timestamp between January 1, 1970 and now, unless passed
         explicit start_datetime or end_datetime values.
 
-        :example: 1061306726
+        On Windows, the decimal part is always 0.
+
+        :example: 1061306726.6
         """
         start_datetime = self._parse_start_datetime(start_datetime)
         end_datetime = self._parse_end_datetime(end_datetime)
-        return self._rand_seconds(start_datetime, end_datetime)
+        return float(self._rand_seconds(start_datetime, end_datetime))
 
     def time_delta(self, end_datetime: Optional[DateParseType] = None) -> timedelta:
         """
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index 3118fb89..32f918a6 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -258,21 +258,21 @@ class Provider(BaseProvider):
 
     def pydecimal(
         self,
-        left_digits=None,
-        right_digits=None,
-        positive=False,
-        min_value=None,
-        max_value=None,
-    ):
+        left_digits: Optional[int] = None,
+        right_digits: Optional[int] = None,
+        positive: bool = False,
+        min_value: Optional[float] = None,
+        max_value: Optional[float] = None,
+    ) -> Decimal:
         if left_digits is not None and left_digits < 0:
             raise ValueError("A decimal number cannot have less than 0 digits in its " "integer part")
         if right_digits is not None and right_digits < 0:
             raise ValueError("A decimal number cannot have less than 0 digits in its " "fractional part")
         if (left_digits is not None and left_digits == 0) and (right_digits is not None and right_digits == 0):
             raise ValueError("A decimal number cannot have 0 digits in total")
-        if None not in (min_value, max_value) and min_value > max_value:
+        if min_value is not None and max_value is not None and min_value > max_value:
             raise ValueError("Min value cannot be greater than max value")
-        if None not in (min_value, max_value) and min_value == max_value:
+        if min_value is not None and max_value is not None and min_value == max_value:
             raise ValueError("Min and max value cannot be the same")
         if positive and min_value is not None and min_value <= 0:
             raise ValueError("Cannot combine positive=True with negative or zero min_value")
@@ -299,7 +299,7 @@ class Provider(BaseProvider):
 
         if sign == "+":
             if max_value is not None:
-                left_number = str(self.random_int(max(min_value or 0, 0), max_value))
+                left_number = str(self.random_int(int(max(min_value or 0, 0)), int(max_value)))
             else:
                 min_left_digits = math.ceil(math.log10(max(min_value or 1, 1)))
                 if left_digits is None:
@@ -307,7 +307,7 @@ class Provider(BaseProvider):
                 left_number = "".join([str(self.random_digit()) for i in range(0, left_digits)]) or "0"
         else:
             if min_value is not None:
-                left_number = str(self.random_int(max(max_value or 0, 0), abs(min_value)))
+                left_number = str(self.random_int(int(max(max_value or 0, 0)), int(abs(min_value))))
             else:
                 min_left_digits = math.ceil(math.log10(abs(min(max_value or 1, 1))))
                 if left_digits is None:
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:
         """


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_date_time.py b/tests/providers/test_date_time.py
index bb8cb69a..797a3694 100644
--- a/tests/providers/test_date_time.py
+++ b/tests/providers/test_date_time.py
@@ -548,7 +548,8 @@ class TestDateTime(unittest.TestCase):
     @pytest.mark.skipif(not sys.platform.startswith("win"), reason="windows does not support sub second precision")
     def test_unix_time_win(self):
         unix_time = self.fake.unix_time()
-        assert isinstance(unix_time, int)
+        assert isinstance(unix_time, float)
+        assert unix_time % 1 == 0.0
 
     @pytest.mark.skipif(sys.platform.startswith("win"), reason="non windows does support sub second precision")
     def test_unix_time_non_win(self):
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
index 27a2a69e..6342bfb9 100644
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -84,6 +84,36 @@ def test_pyfloat_right_or_left_digit_overflow():
             assert str(abs(result)) == "1.12345678901234"
 
 
+@pytest.mark.skipif(sys.version_info < (3, 10), reason="Only relevant for Python 3.10 and later.")
+@pytest.mark.parametrize(
+    ("min_value", "max_value"),
+    [
+        (1.5, None),
+        (-1.5, None),
+        (None, -1.5),
+        (None, 1.5),
+        (-1.5, 1.5),
+    ],
+)
+@pytest.mark.parametrize(("left_digits"), [None, 5])
+@pytest.mark.parametrize(("right_digits"), [None, 5])
+@pytest.mark.filterwarnings(
+    # Convert the warning to an error for this test
+    r"error:non-integer arguments to randrange\(\):DeprecationWarning"
+)
+def test_float_min_and_max_value_does_not_crash(
+    left_digits: Optional[int],
+    right_digits: Optional[int],
+    min_value: Optional[float],
+    max_value: Optional[float],
+):
+    """
+    Float arguments to randrange are deprecated from Python 3.10. This is a regression
+    test to check that `pydecimal` does not cause a crash on any code path.
+    """
+    Faker().pydecimal(left_digits, right_digits, min_value=min_value, max_value=max_value)
+
+
 class TestPyint(unittest.TestCase):
     def setUp(self):
         self.fake = Faker()
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):