Repository: joke2k/faker (python)

Issue:
slug() - returns an empty string in some locales
* Faker version: 28.4.1
* OS: Mac

When using certain locales (e.g. `jp_JP`), the `slug` fake returns an empty string. I had a quick look and this is probably because the `slugify` decorator disallows unicode characters (which makes sense) and the fake is using the `text` fake under the hood - not anticipating that it is going to return unicode characters.

### Steps to reproduce

1. `fake = Faker("jp_JP")`
2. `fake.slug()`

### Expected behavior

The `slug` fake should never return an empty string

### Actual behavior

The `slug` fake returns an empty string in some locales.


Pull request description:
Force the slug always be generated with ASCII characters
### What does this change

Compose the slug with Latin words only, regardless the assigned locale.

### What was wrong

The slug may be empty if there is no Latin characters in the locale language, such as Japanese, Korean and Chinese.

### How this fixes it

Compose the slug with Latin words only, regardless the assigned locale.

Fixes #2103 


Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 46a4bbcb..b5909b52 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 28.4.1
+current_version = 29.0.0
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 016e9c43..7533af92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## Changelog
 
+### [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.
+
 ### [v28.4.1 - 2024-09-04](https://github.com/joke2k/faker/compare/v28.4.0...v28.4.1)
 
 * Fix issue where Faker does not properly convert min/max float values to `Decimal`. Thanks @bdjellabaldebaran.
diff --git a/VERSION b/VERSION
index ba1028e7..f1e4903d 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-28.4.1
+29.0.0
diff --git a/docs/conf.py b/docs/conf.py
index 08aeaea0..b0c46241 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 = "28.4.1"
+version = "29.0.0"
 # The full version, including alpha/beta/rc tags.
-release = "28.4.1"
+release = "29.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 ffe2782a..93b7b527 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 = "28.4.1"
+VERSION = "29.0.0"
 
 __all__ = ("Factory", "Generator", "Faker")
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(
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index 374a5793..28b43562 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -335,7 +335,7 @@ class Provider(BaseProvider):
                 left_number = str(self._random_int_of_length(left_digits))
         else:
             if min_value is not None:
-                left_number = str(self.random_int(int(max(max_value or 0, 0)), int(abs(min_value))))
+                left_number = str(self.random_int(int(abs(min(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:
@@ -345,7 +345,7 @@ class Provider(BaseProvider):
         if right_digits is None:
             right_digits = self.random_int(0, max_random_digits)
 
-        right_number = "".join([str(self.random_digit()) for i in range(0, right_digits)])
+        right_number = "".join([str(self.random_digit()) for _ in range(0, right_digits)])
 
         result = Decimal(f"{sign}{left_number}.{right_number}")
 
diff --git a/faker/proxy.pyi b/faker/proxy.pyi
index 54347af4..792ad536 100644
--- a/faker/proxy.pyi
+++ b/faker/proxy.pyi
@@ -1,3 +1,6 @@
+# This file is auto-generated by generate_stubs.py.
+# Please do not edit this file directly.
+
 import datetime
 
 from collections import OrderedDict
diff --git a/generate_stubs.py b/generate_stubs.py
index 1f8bc67a..4823b79a 100644
--- a/generate_stubs.py
+++ b/generate_stubs.py
@@ -186,7 +186,9 @@ member_signatures_block = "    " + "\n    ".join(
     [sig.replace("\n", "\n    ") for sig in signatures_with_comments_as_str]
 )
 
-body = f"""
+body = f"""# This file is auto-generated by generate_stubs.py.
+# Please do not edit this file directly.
+
 {imports_block}
 
 class Faker:


Test patch:
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() != ""
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
index 5af45bf2..17502b86 100644
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -3,6 +3,7 @@ import sys
 import unittest
 import warnings
 
+from collections import Counter
 from typing import Iterable, Optional, Type, Union
 from unittest.mock import patch
 
@@ -506,6 +507,29 @@ class TestPydecimal(unittest.TestCase):
         result = self.fake.pydecimal(min_value=10**1000)
         self.assertGreater(result, 10**1000)
 
+    def test_min_value_and_max_value_have_different_signs_return_evenly_distributed_values(self):
+        result = []
+        boundary_value = 10
+        for _ in range(1000):
+            result.append(self.fake.pydecimal(min_value=-boundary_value, max_value=boundary_value, right_digits=0))
+        self.assertEqual(len(Counter(result)), 2 * boundary_value + 1)
+
+    def test_min_value_and_max_value_negative_return_evenly_distributed_values(self):
+        result = []
+        min_value = -60
+        max_value = -50
+        for _ in range(1000):
+            result.append(self.fake.pydecimal(min_value=min_value, max_value=max_value, right_digits=0))
+        self.assertGreater(len(Counter(result)), max_value - min_value)
+
+    def test_min_value_and_max_value_positive_return_evenly_distributed_values(self):
+        result = []
+        min_value = 50
+        max_value = 60
+        for _ in range(1000):
+            result.append(self.fake.pydecimal(min_value=min_value, max_value=max_value, right_digits=0))
+        self.assertGreater(len(Counter(result)), max_value - min_value)
+
     def test_min_value_float_returns_correct_digit_number(self):
         Faker.seed("6")
         result = self.fake.pydecimal(left_digits=1, right_digits=1, min_value=0.2, max_value=0.3)