Repository: joke2k/faker

Issue:
pydecimal left_digits ignored if min_value or max_value is specified
`max_value` should be set to `10 ** (left_digits -1) - epsilon` before this:

https://github.com/joke2k/faker/blob/d9f4b00b9134e6dfbb09cc1caa81c912b79c3c7c/faker/providers/python/__init__.py#L92-L102

Use cases for using both include:

- `min_value=0`, since `positive=True` disallows `0` (a bug in itself IMO, but that's an age old debate!)
- `min_value` at all actually? As in, 4 left digits, but no less than 42 in value, for example.
- a `max_value` that has a semantically different reason for existing, so it's convenient to specify in addition to `left_digits` [^]

Work around is to specify a `max_value` (per above) instead of `left_digits` if `min_value` or `max_value` are needed too.

(I will have a PR for this shortly.)

[^] - e.g. `left_digits` could be a database requirement (`NUMERIC(left + right, right)`), but `max_value` something to do with the logic the fake is for.


Discussion:
Oops, #1410 looks like the same.

Resolving patch:
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -1,3 +1,4 @@
+import math
 import string
 import sys
 import warnings
@@ -69,10 +70,15 @@ def pyfloat(self, left_digits=None, right_digits=None, positive=False,
         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')
 
         # Make sure at least either left or right is set
         if left_digits is None and right_digits is None:
-            left_digits = self.random_int(1, sys.float_info.dig - 1)
+            needed_left_digits = max(1, math.ceil(math.log10(max(abs(max_value or 1), abs(min_value or 1)))))
+            right_digits = self.random_int(1, sys.float_info.dig - needed_left_digits)
 
         # If only one side is set, choose #digits for other side
         if (left_digits is None) ^ (right_digits is None):
@@ -90,6 +96,13 @@ def pyfloat(self, left_digits=None, right_digits=None, positive=False,
 
         sign = ''
         if (min_value is not None) or (max_value is not None):
+            # Make sure left_digits still respected
+            if left_digits is not None:
+                if max_value is None:
+                    max_value = 10 ** left_digits  # minus smallest representable, adjusted later
+                if min_value is None:
+                    min_value = -(10 ** left_digits)  # plus smallest representable, adjusted later
+
             if max_value is not None and max_value < 0:
                 max_value += 1  # as the random_int will be generated up to max_value - 1
             if min_value is not None and min_value < 0:
@@ -107,6 +120,14 @@ def pyfloat(self, left_digits=None, right_digits=None, positive=False,
                 result = float('0.' + '0' * (right_digits - 1) + '1')
             else:
                 result += sys.float_info.epsilon
+
+        if right_digits:
+            result = min(result, 10 ** left_digits - float(f'0.{"0" * (right_digits - 1)}1'))
+            result = max(result, -(10 ** left_digits + float(f'0.{"0" * (right_digits - 1)}1')))
+        else:
+            result = min(result, 10 ** left_digits - 1)
+            result = max(result, -(10 ** left_digits + 1))
+
         return result
 
     def _safe_random_int(self, min_value, max_value, positive):


Test patch:
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -132,6 +132,16 @@ def test_min_value(self):
             result = self.fake.pyfloat(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.pyfloat(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)
 
@@ -139,6 +149,16 @@ def test_max_value(self):
             result = self.fake.pyfloat(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.pyfloat(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
@@ -160,6 +180,17 @@ def test_max_value_and_positive(self):
         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.pyfloat(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