Repository: joke2k/faker
Pull request #1521 (2021-09-28 11:27:01)

added bg_BG color provider and tests
### What does this changes

Creates a bg_BG color provider and tests.



Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 46484845..c57839ca 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 8.14.0
+current_version = 8.14.2
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5924f10a..6ba39d75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 ## Changelog
 
+### [v8.14.2 - 2021-10-04](https://github.com/joke2k/faker/compare/v8.14.1...v8.14.2)
+
+* Fix bug in ``pydecimal`` when returning max/min. Thanks @wilbertom.
+
+### [v8.14.1 - 2021-10-01](https://github.com/joke2k/faker/compare/v8.14.0...v8.14.1)
+
+* Fix `pydecimal` precision. Thanks @joidegn.
+
 ### [v8.14.0 - 2021-09-24](https://github.com/joke2k/faker/compare/v8.13.1...v8.14.0)
 
 * Add datetime provider for ``bn_BD``. Thanks @jabertuhin.
diff --git a/README.rst b/README.rst
index 572a393d..559de2fc 100644
--- a/README.rst
+++ b/README.rst
@@ -230,7 +230,7 @@ Examples:
     94812 Biedenkopf
 
     $ faker profile ssn,birthdate
-    {'ssn': u'628-10-1085', 'birthdate': '2008-03-29'}
+    {'ssn': '628-10-1085', 'birthdate': '2008-03-29'}
 
     $ faker -r=3 -s=";" name
     Willam Kertzmann;
diff --git a/VERSION b/VERSION
index 2a5dd0d6..12800750 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-8.14.0
+8.14.2
diff --git a/docs/conf.py b/docs/conf.py
index 1fe65319..dc53c329 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 = '8.14.0'
+version = '8.14.2'
 # The full version, including alpha/beta/rc tags.
-release = '8.14.0'
+release = '8.14.2'
 
 # 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 11d8aee0..8dac2fd2 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 = '8.14.0'
+VERSION = '8.14.2'
 
 __all__ = ('Factory', 'Generator', 'Faker')
diff --git a/faker/providers/color/bg_BG/__init__.py b/faker/providers/color/bg_BG/__init__.py
new file mode 100644
index 00000000..325dbce0
--- /dev/null
+++ b/faker/providers/color/bg_BG/__init__.py
@@ -0,0 +1,74 @@
+from collections import OrderedDict
+
+from .. import Provider as ColorProvider
+
+localized = True
+
+
+class Provider(ColorProvider):
+    """Implement color provider for ``bg_BG`` locale."""
+
+    all_colors = OrderedDict((
+        ("Антично-бяло", "#FAEBD7"),
+        ("Аквамарин", "#7FFFD4"),
+        ("Лазурно", "#F0FFFF"),
+        ("Бежово", "#F5F5DC"),
+        ("Черно", "#000000"),
+        ("Синьо", "#0000FF"),
+        ("Синьо-виолетовo", "#8A2BE2"),
+        ("Кафявo", "#A52A2A"),
+        ("Шоколадовo", "#D2691E"),
+        ("Корал", "#FF7F50"),
+        ("Метличино синьо", "#6495ED"),
+        ("Малиново", "#DC143C"),
+        ("Тъмносиньо", "#00008B"),
+        ("Тъмноциан", "#008B8B"),
+        ("Тъмносуво", "#A9A9A9"),
+        ("Тъмнозелено", "#006400"),
+        ("Тъмно маслинено зелено", "#BDB76B"),
+        ("Тъмнооранжево", "#FF8C00"),
+        ("Тъмночервено", "#8B0000"),
+        ("Тъмнотюркоазено", "#00CED1"),
+        ("Тъмновиолетово", "#9400D3"),
+        ("Тъмнорозово", "#FF1493"),
+        ("Бледо сиво", "#696969"),
+        ("Фуксия", "#FF00FF"),
+        ("Златно", "#FFD700"),
+        ("Сребърно", "#808080"),
+        ("Зелено", "#008000"),
+        ("Жълто-зелено", "#ADFF2F"),
+        ("Ярко-розово", "#FF69B4"),
+        ("Индиго", "#4B0082"),
+        ("Слонова кост", "#FFFFF0"),
+        ("Маслинено зелено", "#F0E68C"),
+        ("Розовато-лавандулово", "#FFF0F5"),
+        ("Светлосиньо", "#ADD8E6"),
+        ("Светло-небесносино", "#E0FFFF"),
+        ("Светлосиво", "#D3D3D3"),
+        ("Светлозелено", "#90EE90"),
+        ("Светлорозово", "#FFB6C1"),
+        ("Небесносиньо", "#87CEFA"),
+        ("Светложълто", "#FFFFE0"),
+        ("Кестеняво", "#800000"),
+        ("Оранжево", "#FFA500"),
+        ("Оранжево-червено", "#FF4500"),
+        ("Бледозелено", "#98FB98"),
+        ("Бледотюркоазно", "#AFEEEE"),
+        ("Розово", "#FFC0CB"),
+        ("Сливово розово", "#DDA0DD"),
+        ("Пурпурно", "#800080"),
+        ("Червено", "#FF0000"),
+        ("Циан", "#2E8B57"),
+        ("Сребърно", "#C0C0C0"),
+        ("Тюркоазено", "#40E0D0"),
+        ("Виолетово", "#EE82EE"),
+        ("Бяло", "#FFFFFF"),
+        ("Жълто", "#FFFF00"),
+        ("Жълто-зелено", "#9ACD32"),
+    ))
+
+    safe_colors = (
+        'черно', 'бордо', 'зелено', 'маслинено зелено',
+        'пурпурно', 'циан', 'лайм', 'синьо', 'сребърно',
+        'сиво', 'жълто', 'фуксия', 'бяло',
+    )
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index ec28ddc0..b056d186 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -151,10 +151,58 @@ class Provider(BaseProvider):
 
     def pydecimal(self, left_digits=None, right_digits=None, positive=False,
                   min_value=None, max_value=None):
+        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:
+            raise ValueError('Min value cannot be greater than max value')
+        if None not in (min_value, max_value) 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')
+        if left_digits is not None and max_value and math.ceil(math.log10(abs(max_value))) > left_digits:
+            raise ValueError('Max value must fit within left digits')
+        if left_digits is not None and min_value and math.ceil(math.log10(abs(min_value))) > left_digits:
+            raise ValueError('Min value must fit within left digits')
+
+        # if either left or right digits are not specified we randomly choose a length
+        max_random_digits = 100
+        minimum_left_digits = len(str(min_value)) if min_value is not None else 1
+        if left_digits is None and right_digits is None:
+            right_digits = self.random_int(1, max_random_digits)
+            left_digits = self.random_int(minimum_left_digits, max_random_digits)
+        if left_digits is not None and right_digits is None:
+            right_digits = self.random_int(1, max_random_digits)
+        if left_digits is None and right_digits is not None:
+            left_digits = self.random_int(minimum_left_digits, max_random_digits)
 
-        float_ = self.pyfloat(
-            left_digits, right_digits, positive, min_value, max_value)
-        return Decimal(str(float_))
+        sign = ''
+        left_number = ''.join([str(self.random_digit()) for i in range(0, left_digits)]) or '0'
+        if right_digits is not None:
+            right_number = ''.join([str(self.random_digit()) for i in range(0, right_digits)])
+        else:
+            right_number = ''
+        sign = '+' if positive else self.random_element(('+', '-'))
+
+        result = Decimal(f'{sign}{left_number}.{right_number}')
+
+        # Because the random result might have the same number of decimals as max_value the random number
+        # might be above max_value or below min_value
+        if max_value is not None and result > max_value:
+            result = Decimal(max_value)
+        if min_value is not None and result < min_value:
+            result = Decimal(min_value)
+
+        return result
 
     def pytuple(self, nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types):
         return tuple(


Test patch:
diff --git a/tests/providers/test_color.py b/tests/providers/test_color.py
index 328e7217..52d15336 100644
--- a/tests/providers/test_color.py
+++ b/tests/providers/test_color.py
@@ -5,6 +5,7 @@ import re
 import pytest
 
 from faker.providers.color import RandomColor
+from faker.providers.color.bg_BG import Provider as BgBgColorProvider
 from faker.providers.color.es_ES import Provider as EsEsColorProvider
 from faker.providers.color.fa_IR import Provider as FaIrColorProvider
 from faker.providers.color.he_IL import Provider as HeILColorProvider
@@ -278,6 +279,22 @@ class TestFaIr:
             assert safe_color_name in FaIrColorProvider.safe_colors
 
 
+class TestBgBg:
+    """Test bg_BG color provider methods"""
+
+    def test_color_name(self, faker, num_samples):
+        for _ in range(num_samples):
+            color_name = faker.color_name()
+            assert isinstance(color_name, str)
+            assert color_name in BgBgColorProvider.all_colors.keys()
+
+    def test_safe_color_name(self, faker, num_samples):
+        for _ in range(num_samples):
+            safe_color_name = faker.safe_color_name()
+            assert isinstance(safe_color_name, str)
+            assert safe_color_name in BgBgColorProvider.safe_colors
+
+
 class TestEsEs:
     """Test es_ES color provider methods"""
 
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
index afe4229b..9742c9a0 100644
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -1,3 +1,4 @@
+import decimal
 import sys
 import unittest
 import warnings
@@ -215,6 +216,143 @@ class TestPyfloat(unittest.TestCase):
         self.assertGreater(result, 0)
 
 
+class TestPydecimal(unittest.TestCase):
+    def setUp(self):
+        self.fake = Faker()
+        Faker.seed(0)
+
+    def test_pydecimal(self):
+        result = self.fake.pydecimal()
+
+        self.assertIsInstance(result, decimal.Decimal)
+
+    def test_left_digits(self):
+        expected_left_digits = 10
+
+        result = self.fake.pydecimal(left_digits=expected_left_digits)
+
+        left_digits = len(str(abs(int(result))))
+        self.assertGreaterEqual(expected_left_digits, left_digits)
+
+    def test_right_digits(self):
+        expected_right_digits = 10
+
+        result = self.fake.pydecimal(right_digits=expected_right_digits)
+
+        right_digits = len(str(result).split('.')[1])
+        self.assertGreaterEqual(expected_right_digits, right_digits)
+
+    def test_positive(self):
+        result = self.fake.pydecimal(positive=True)
+
+        self.assertGreater(result, 0)
+        abs_result = -result if result < 0 else result  # abs() result returns scientific notation
+        self.assertEqual(result, abs_result)
+
+    def test_min_value(self):
+        min_values = (0, 10, -1000, 1000, 999999)
+
+        for min_value in min_values:
+            result = self.fake.pydecimal(min_value=min_value)
+            self.assertGreaterEqual(result, min_value)
+
+    def test_min_value_always_returns_a_decimal(self):
+        min_values = (0, 10, -1000, 1000, 999999)
+
+        for min_value in min_values:
+            result = self.fake.pydecimal(min_value=min_value)
+            self.assertIsInstance(result, decimal.Decimal)
+
+    def test_min_value_and_left_digits(self):
+        """
+        Combining the min_value and left_digits keyword arguments produces
+        numbers that obey both of those constraints.
+        """
+
+        result = self.fake.pydecimal(left_digits=1, min_value=0)
+        self.assertLess(result, 10)
+        self.assertGreaterEqual(result, 0)
+
+    def test_max_value(self):
+        max_values = (0, 10, -1000, 1000, 999999)
+
+        for max_value in max_values:
+            result = self.fake.pydecimal(max_value=max_value)
+            self.assertLessEqual(result, max_value)
+
+    def test_max_value_always_returns_a_decimal(self):
+        max_values = (0, 10, -1000, 1000, 999999)
+
+        for max_value in max_values:
+            result = self.fake.pydecimal(max_value=max_value)
+            self.assertIsInstance(result, decimal.Decimal)
+
+    def test_max_value_zero_and_left_digits(self):
+        """
+        Combining the max_value and left_digits keyword arguments produces
+        numbers that obey both of those constraints.
+        """
+
+        result = self.fake.pydecimal(left_digits=2, max_value=0)
+        self.assertLessEqual(result, 0)
+        self.assertGreater(result, -100)
+
+    def test_max_value_should_be_greater_than_min_value(self):
+        """
+        An exception should be raised if min_value is greater than max_value
+        """
+        expected_message = 'Min value cannot be greater than max value'
+        with self.assertRaises(ValueError) as raises:
+            self.fake.pydecimal(min_value=100, max_value=0)
+
+        message = str(raises.exception)
+        self.assertEqual(message, expected_message)
+
+    def test_max_value_and_positive(self):
+        """
+        Combining the max_value and positive keyword arguments produces
+        numbers that obey both of those constraints.
+        """
+
+        result = self.fake.pydecimal(positive=True, max_value=100)
+        self.assertLessEqual(result, 100)
+        self.assertGreater(result, 0)
+
+    def test_max_and_min_value_negative(self):
+        """
+        Combining the max_value and min_value keyword arguments with
+        negative values for each produces numbers that obey both of
+        those constraints.
+        """
+
+        result = self.fake.pydecimal(max_value=-100, min_value=-200)
+        self.assertLessEqual(result, -100)
+        self.assertGreaterEqual(result, -200)
+
+    def test_positive_and_min_value_incompatible(self):
+        """
+        An exception should be raised if positive=True is set, but
+        a negative min_value is provided.
+        """
+
+        expected_message = (
+            "Cannot combine positive=True with negative or zero min_value"
+        )
+        with self.assertRaises(ValueError) as raises:
+            self.fake.pydecimal(min_value=-100, positive=True)
+
+        message = str(raises.exception)
+        self.assertEqual(message, expected_message)
+
+    def test_positive_doesnt_return_zero(self):
+        """
+        Choose the right_digits and max_value so it's guaranteed to return zero,
+        then watch as it doesn't because positive=True
+        """
+        result = self.fake.pydecimal(positive=True, right_digits=0, max_value=1)
+        self.assertGreater(result, 0)
+
+
 class TestPystrFormat(unittest.TestCase):
 
     def setUp(self):
diff --git a/tests/test_factory.py b/tests/test_factory.py
index a8913e72..98a25ca4 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -235,6 +235,14 @@ class FactoryTestCase(unittest.TestCase):
         assert any(fake.pyfloat(left_digits=0, positive=False) < 0 for _ in range(100))
         assert any(fake.pydecimal(left_digits=0, positive=False) < 0 for _ in range(100))
 
+    def test_arbitrary_digits_pydecimal(self):
+        # tests for https://github.com/joke2k/faker/issues/1462
+        fake = Faker()
+        assert any(
+            len(str(fake.pydecimal(left_digits=sys.float_info.dig + i))) > sys.float_info.dig for i in range(100)
+        )
+        assert any(len(str(fake.pydecimal())) > sys.float_info.dig for _ in range(100))
+
     def test_pyfloat_empty_range_error(self):
         # tests for https://github.com/joke2k/faker/issues/1048
         fake = Faker()