Repository: joke2k/faker

Issue:
pyfloat produces values outside of the allowed range.
* Faker version: 17.6.0
* OS: Windows 10

Brief summary of the issue goes here.

`pyfloat` produces values outside of the allowed range.
It appears this is related to the fact that the provided `min_value` and `max_value` are overwritten see [line 150](https://github.com/joke2k/faker/blob/master/faker/providers/python/__init__.py#L150). So even the final check to see if the result exceeds the `max_value` will not catch these scenarios. 

### Steps to reproduce

```python
for i in range(10000):
    min_x, max_x = (-120.123, -115.321)
    min_y, max_y = (20.123, 30.321)
    x= fake.pyfloat(min_value=min_x, max_value=max_x)
    y= fake.pyfloat(min_value=min_y, max_value=max_y)
    if x < min_x:
        print(f'Exceeds min {x} < {min_x}')
    if x > max_x:
        print(f'Exceeds max {x} > {min_x}')
    if y < min_y:
        print(f'Exceeds min {y} < {min_y}')
    if y > max_y:
        print(f'Exceeds max {y} > {min_y}')
```

Partial output:
```
Exceeds max -115.0 > -115.321
Exceeds max -115.23 > -115.321
Exceeds max -115.1989873 > -115.321
Exceeds max -115.282293711644 > -115.321
Exceeds min 20.10361238 < 20.123
Exceeds max -115.127203600243 > -115.321
Exceeds max -115.1 > -115.321
Exceeds min 20.0 < 20.123
Exceeds max -115.19968337045 > -115.321
Exceeds max -115.198 > -115.321
Exceeds max -115.12414928 > -115.321
Exceeds min 20.0 < 20.123
```

### Expected behavior

Values should be bound by the `min_value` and `max_value`

### Actual behavior

Values are exceeding the allowed boundary

pyfloat produces values outside of the allowed range.
* Faker version: 17.6.0
* OS: Windows 10

Brief summary of the issue goes here.

`pyfloat` produces values outside of the allowed range.
It appears this is related to the fact that the provided `min_value` and `max_value` are overwritten see [line 150](https://github.com/joke2k/faker/blob/master/faker/providers/python/__init__.py#L150). So even the final check to see if the result exceeds the `max_value` will not catch these scenarios. 

### Steps to reproduce

```python
for i in range(10000):
    min_x, max_x = (-120.123, -115.321)
    min_y, max_y = (20.123, 30.321)
    x= fake.pyfloat(min_value=min_x, max_value=max_x)
    y= fake.pyfloat(min_value=min_y, max_value=max_y)
    if x < min_x:
        print(f'Exceeds min {x} < {min_x}')
    if x > max_x:
        print(f'Exceeds max {x} > {min_x}')
    if y < min_y:
        print(f'Exceeds min {y} < {min_y}')
    if y > max_y:
        print(f'Exceeds max {y} > {min_y}')
```

Partial output:
```
Exceeds max -115.0 > -115.321
Exceeds max -115.23 > -115.321
Exceeds max -115.1989873 > -115.321
Exceeds max -115.282293711644 > -115.321
Exceeds min 20.10361238 < 20.123
Exceeds max -115.127203600243 > -115.321
Exceeds max -115.1 > -115.321
Exceeds min 20.0 < 20.123
Exceeds max -115.19968337045 > -115.321
Exceeds max -115.198 > -115.321
Exceeds max -115.12414928 > -115.321
Exceeds min 20.0 < 20.123
```

### Expected behavior

Values should be bound by the `min_value` and `max_value`

### Actual behavior

Values are exceeding the allowed boundary



Discussion:

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
@@ -144,20 +144,23 @@ def pyfloat(
 
         sign = ""
         if (min_value is not None) or (max_value is not None):
+            # Copy values to ensure we're not modifying the original values and thus going out of bounds
+            left_min_value = min_value
+            left_max_value = max_value
             # 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
+                    left_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
+                    left_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
+                left_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:
-                min_value += 1  # as we then append digits after the left_number
+                left_min_value += 1  # as we then append digits after the left_number
             left_number = self._safe_random_int(
-                min_value,
-                max_value,
+                left_min_value,
+                left_max_value,
                 positive,
             )
         else:
@@ -178,11 +181,17 @@ def pyfloat(
             result = min(result, 10**left_digits - 1)
             result = max(result, -(10**left_digits + 1))
 
-        # It's possible for the result to end up > than max_value
-        # This is a quick hack to ensure result is always smaller.
+        # It's possible for the result to end up > than max_value or < than min_value
+        # When this happens we introduce some variance so we're not always the exactly the min_value or max_value.
+        # Which can happen a lot depending on the difference of the values.
+        # Ensure the variance is bound by the difference between the max and min
         if max_value is not None:
             if result > max_value:
-                result = result - (result - max_value)
+                result = result - (result - max_value + self.generator.random.uniform(0, max_value - min_value))
+        if min_value is not None:
+            if result < min_value:
+                result = result + (min_value - result + self.generator.random.uniform(0, max_value - min_value))
+
         return result
 
     def _safe_random_int(self, min_value: float, max_value: float, positive: bool) -> int:


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
@@ -181,6 +181,17 @@ def test_max_value_and_positive(self):
         self.assertLessEqual(result, 100)
         self.assertGreater(result, 0)
 
+    def test_max_and_min_value_positive_with_decimals(self):
+        """
+        Combining the max_value and min_value keyword arguments with
+        positive values for each produces numbers that obey both of
+        those constraints.
+        """
+        for _ in range(1000):
+            result = self.fake.pyfloat(min_value=100.123, max_value=200.321)
+            self.assertLessEqual(result, 200.321)
+            self.assertGreaterEqual(result, 100.123)
+
     def test_max_and_min_value_negative(self):
         """
         Combining the max_value and min_value keyword arguments with
@@ -192,6 +203,17 @@ def test_max_and_min_value_negative(self):
         self.assertLessEqual(result, -100)
         self.assertGreaterEqual(result, -200)
 
+    def test_max_and_min_value_negative_with_decimals(self):
+        """
+        Combining the max_value and min_value keyword arguments with
+        negative values for each produces numbers that obey both of
+        those constraints.
+        """
+        for _ in range(1000):
+            result = self.fake.pyfloat(max_value=-100.123, min_value=-200.321)
+            self.assertLessEqual(result, -100.123)
+            self.assertGreaterEqual(result, -200.321)
+
     def test_positive_and_min_value_incompatible(self):
         """
         An exception should be raised if positive=True is set, but