diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4648484532..f6df28d0c6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 8.14.0 +current_version = 8.14.1 files = VERSION faker/__init__.py docs/conf.py commit = True tag = True diff --git a/CHANGELOG.md b/CHANGELOG.md index 5924f10a18..0806599b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog +### [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 572a393d4c..559de2fc00 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 2a5dd0d638..6092827e64 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.14.0 +8.14.1 diff --git a/docs/conf.py b/docs/conf.py index 1fe653199a..dfec42affd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '8.14.0' +version = '8.14.1' # The full version, including alpha/beta/rc tags. -release = '8.14.0' +release = '8.14.1' # 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 11d8aee0c9..a122980a9c 100644 --- a/faker/__init__.py +++ b/faker/__init__.py @@ -2,6 +2,6 @@ from faker.generator import Generator from faker.proxy import Faker -VERSION = '8.14.0' +VERSION = '8.14.1' __all__ = ('Factory', 'Generator', 'Faker') diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py index ec28ddc091..595e69fb7a 100644 --- a/faker/providers/python/__init__.py +++ b/faker/providers/python/__init__.py @@ -151,10 +151,58 @@ def pyint(self, min_value=0, max_value=9999, step=1): 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 = max_value + if min_value is not None and result < min_value: + result = min_value + + return result def pytuple(self, nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types): return tuple( diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py index afe4229b17..6da60260b9 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,129 @@ def test_positive_doesnt_return_zero(self): 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_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_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 a8913e72bc..98a25ca4aa 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -235,6 +235,14 @@ def test_negative_pyfloat(self): 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()