Repository: joke2k/faker
Pull request #1954 (2023-11-18 09:20:58)

fix: random sign when +ve is false(re)
### What does this change

When using print(f.pyfloat(positive=False)) , the number could be positive or negative.
Now when positive is false, only negative numbers would be returned.

### What was wrong

Both negative and positive being returned.

### How this fixes it

Removed randomly picking a sign if positive is false

Fixes: #1928 

Now this could be intended, if it was, maintainers, please let me know, I would love to implement the random sign functionality but that would require changing the function arguments and tests.

I did not want to ruin the commit history by making changes to the messed up pr.
Apologies to the maintainers. heres the actual pr.

fcurella: I think the correct behavior should be:

`positive=True` always return a positive number
`positive=False` always return a negative
`positive=None` (or not specified at all) return random sign

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index dc61d014..988e36ff 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 20.0.3
+current_version = 20.1.0
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/.github/workflows/autobump.yml b/.github/workflows/autobump.yml
deleted file mode 100644
index fbf5a590..00000000
--- a/.github/workflows/autobump.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-name: Bump Version
-on:
-  pull_request:
-    types: [closed]
-
-jobs:
-  bumpversion:
-    runs-on: ubuntu-latest
-    if: github.event.pull_request.merged_at != null
-    strategy:
-      max-parallel: 1
-    steps:
-      - name: Wait for status checks
-        id: waitforstatuschecks
-        uses: "fcurella/gh-status-check@main"
-        with:
-          ignoreContexts: coverage/coveralls,continuous-integration/appveyor/branch
-          ignoreActions: autobump
-          checkInterval: 12
-          githubToken: ${{ secrets.GITHUB_TOKEN }}
-      - name: Get Next Part
-        id: nextpart
-        if: steps.waitforstatuschecks.outputs.status == 'success'
-        uses: "fcurella/gh-action-label-to-semver@main"
-        with:
-          major: "bump-version:major"
-          minor: "bump-version:minor"
-          defaultPart: "patch"
-          githubToken: ${{ secrets.GITHUB_TOKEN }}
-      - uses: actions/checkout@v4
-        with:
-         fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
-      - name: Get Next Version
-        uses: "fcurella/gh-action-next-version@main"
-        id: nextversion
-        with:
-          part: ${{ steps.nextpart.outputs.part }}
-      - name: Update Changelog
-        uses: "fcurella/gh-action-update-changelog@main"
-        with:
-          changelogPath: "CHANGELOG.md"
-          changelogLine : 3
-          currentVersion: ${{ steps.nextversion.outputs.currentVersion }}
-          nextVersion: ${{ steps.nextversion.outputs.nextVersion }}
-      - name: Bump version
-        uses: "fcurella/gh-action-bump2version@main"
-        with:
-          part: ${{ steps.nextpart.outputs.part }}
-      - name: Push changes
-        uses: ad-m/github-push-action@master
-        with:
-          tags: true
-          github_token: ${{ secrets.GITHUB_TOKEN }}
-      - name: Set up Python
-        uses: actions/setup-python@v4
-        with:
-          python-version: '3.x'
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip
-          pip install setuptools wheel
-      - name: Build package
-        run: |
-          python setup.py sdist bdist_wheel
-      - name: Publish package
-        uses: pypa/gh-action-pypi-publish@master
-        with:
-          user: __token__
-          password: ${{ secrets.pypi_password }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f747d851..d6c5e210 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,14 +25,14 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-flake8
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'
     - name: Install dependencies
       run: |
         python -m pip install flake8
     - name: Lint with flake8
-      uses: liskin/gh-problem-matcher-wrap@v2
+      uses: liskin/gh-problem-matcher-wrap@v3
       with:
         linters: flake8
         run: flake8 --extend-ignore=E203 faker tests
@@ -50,7 +50,7 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-checkmanifest
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'
     - name: Install dependencies
@@ -74,14 +74,14 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-isort
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'
     - name: Install dependencies
       run: |
         python -m pip install isort
     - name: Import order checking with isort
-      uses: liskin/gh-problem-matcher-wrap@v2
+      uses: liskin/gh-problem-matcher-wrap@v3
       with:
         linters: isort
         run: isort --check --diff .
@@ -99,7 +99,7 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-black
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'
     - name: Install dependencies
@@ -123,7 +123,7 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-pep8
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'
     - name: Install dependencies
@@ -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
@@ -151,14 +151,14 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-typing
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python }}
     - name: Install dependencies
       run: |
         python -m pip install mypy
     - name: Static type checking with mypy
-      uses: liskin/gh-problem-matcher-wrap@v2
+      uses: liskin/gh-problem-matcher-wrap@v3
       with:
         linters: mypy
         run: mypy --install-types --non-interactive --config mypy.ini faker
@@ -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
@@ -181,13 +181,13 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-pip-test_ubuntu
       - name: Setup Python
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python }}
       - name: Install Tox and any other packages
         run: python -m pip install tox coveralls
       - name: pytest
-        uses: liskin/gh-problem-matcher-wrap@v2
+        uses: liskin/gh-problem-matcher-wrap@v3
         with:
           linters: pytest
           run: tox -e py
@@ -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:
@@ -223,7 +223,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-pip-test_windows
       - name: Setup Python
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python }}
       - name: Install Tox and any other packages
@@ -254,7 +254,7 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-test_alpine
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'
     - name: Install dependencies
diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml
index 9dde6fff..d3c0119f 100644
--- a/.github/workflows/issues.yml
+++ b/.github/workflows/issues.yml
@@ -10,7 +10,7 @@ jobs:
       issues: write
       pull-requests: write
     steps:
-      - uses: actions/stale@v8
+      - uses: actions/stale@v9
         with:
           days-before-issue-stale: 90
           days-before-issue-close: 14
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30e85ee0..4b87944b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 ## Changelog
 
+### [v20.1.0 - 2023-11-20](https://github.com/joke2k/faker/compare/v20.0.3...v20.1.0)
+
+* Add company provider and NUSS for `es_ES` locale. Thanks @fgsalomon.
+* Add official support for Python 3.12. Thanks @pfouque.
+
 ### [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/VERSION b/VERSION
index a9ac8dac..e43bba47 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-20.0.3
+20.1.0
diff --git a/docs/conf.py b/docs/conf.py
index 21b49f92..842e313d 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.3"
+version = "20.1.0"
 # The full version, including alpha/beta/rc tags.
-release = "20.0.3"
+release = "20.1.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 1886fc22..99ae0f8a 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.3"
+VERSION = "20.1.0"
 
 __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..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/python/__init__.py b/faker/providers/python/__init__.py
index 32f918a6..8e3da264 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -141,7 +141,7 @@ class Provider(BaseProvider):
         self,
         left_digits=None,
         right_digits=None,
-        positive=False,
+        positive=None,
         min_value=None,
         max_value=None,
     ):
@@ -203,7 +203,14 @@ class Provider(BaseProvider):
                 positive,
             )
         else:
-            sign = "+" if positive else self.random_element(("+", "-"))
+            if positive is None:
+                sign = self.random_element(("+", "-"))
+            elif positive is True:
+                sign = "+"
+            else:
+                sign = "-"
+            
+            
             left_number = self.random_number(left_digits)
 
         result = float(f"{sign}{left_number}.{self.random_number(right_digits)}")
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..83ec0595 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):
@@ -793,8 +803,9 @@ class TestFiFI(unittest.TestCase):
         for _ in range(100):
             assert re.search(r"^FI\d{8}$", self.fake.vat_id())
 
+    @freezegun.freeze_time("2023-10-23")
     def test_ssn_without_age_range(self):
-        current_year = datetime.now().year
+        current_year = 2023
         age = current_year - 1995
         ssn = self.fake.ssn(min_age=age, max_age=age, artificial=True)
         assert "95-" in ssn