Repository: joke2k/faker
Pull request #1988 (2024-02-06 19:50:33)

Consistent error handling for mis-ordered start-dates and end-dates
### What does this change

In the past, Faker would throw an error if the start of a daterange was after the end. This is still the behaviour on Windows but not Unix.

### What was wrong

Python's random module is itself inconsistent about mis-ordered arguments to random.randint versus random.uniform. This bubbled up to a different behaviour on Windows and Unix because those different functions are used depending on the platform.

### How this fixes it

This PR ensures that on both Windows and Unix, an exception is thrown. This is "backwards compatible" to older versions of Faker and compatible across platforms. It is not backwards compatible to the very most recent versions of Faker, when running on Unix, because it re-introduces the exception.

On all platforms, the exception is a ValueError that contains the string "empty range" and this can be used by callers to detect the problem. Snowfakery, for example, has code to detect this problem and report it nicely to users.

Fixes #...



Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 046cb1b3..c9882870 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -168,7 +168,7 @@ jobs:
     needs: [flake8, isort, black, doc8, checkmanifest, typing]
     strategy:
       matrix:
-        python: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.9"]
+        python: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.10"]
 
     steps:
       - uses: actions/checkout@v4
diff --git a/faker/providers/date_time/__init__.py b/faker/providers/date_time/__init__.py
index 14997342..ebba9c95 100644
--- a/faker/providers/date_time/__init__.py
+++ b/faker/providers/date_time/__init__.py
@@ -79,15 +79,17 @@ class Provider(BaseProvider):
     #       appropriate to generate random seconds with.
     if platform.system() == "Windows":
 
-        @property
-        def _rand_seconds(self) -> Callable[[int, int], int]:
-            return self.generator.random.randint
+        def _rand_seconds(self, start_datetime: int, end_datetime: int) -> float:
+            return self.generator.random.randint(start_datetime, end_datetime)
 
     else:
 
-        @property
-        def _rand_seconds(self) -> Callable[[float, float], float]:
-            return self.generator.random.uniform
+        def _rand_seconds(self, start_datetime: int, end_datetime: int) -> float:
+            if start_datetime > end_datetime:
+                raise ValueError(
+                    "empty range for _rand_seconds: start datetime must be before than end datetime"
+                )
+            return self.generator.random.uniform(start_datetime, end_datetime)
 
     centuries: ElementsType[str] = [
         "I",
diff --git a/readthedocs.yml b/readthedocs.yml
index 8bca54c9..1b80a853 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -1,5 +1,14 @@
-formats:
-  - none
+version: "2"
+
+build:
+  os: "ubuntu-22.04"
+  tools:
+    python: "3.10"
+
 python:
-  pip_install: true
-  version: 3
+  install:
+    - method: pip
+      path: .
+
+sphinx:
+  configuration: docs/conf.py


Test patch:
diff --git a/tests/providers/test_date_time.py b/tests/providers/test_date_time.py
index 797a3694..39256a4b 100644
--- a/tests/providers/test_date_time.py
+++ b/tests/providers/test_date_time.py
@@ -130,9 +130,13 @@ class TestDateTime(unittest.TestCase):
         from_delta = DatetimeProvider._parse_date_time(delta)
         from_int = DatetimeProvider._parse_date_time(timestamp)
 
-        assert datetime.fromtimestamp(from_delta).date() == (datetime.fromtimestamp(timestamp).date())
+        assert datetime.fromtimestamp(from_delta).date() == (
+            datetime.fromtimestamp(timestamp).date()
+        )
 
-        assert datetime.fromtimestamp(from_int).date() == (datetime.fromtimestamp(timestamp).date())
+        assert datetime.fromtimestamp(from_int).date() == (
+            datetime.fromtimestamp(timestamp).date()
+        )
 
     def test_parse_date(self):
         parsed = DatetimeProvider._parse_date("+30d")
@@ -193,11 +197,17 @@ class TestDateTime(unittest.TestCase):
         assert self.fake.iso8601(tzinfo=utc, sep=" ")[10] == " "
         assert self.fake.iso8601(tzinfo=utc, sep="_")[10] == "_"
 
-    @pytest.mark.skipif(not sys.platform.startswith("win"), reason="windows does not support sub second precision")
+    @pytest.mark.skipif(
+        not sys.platform.startswith("win"),
+        reason="windows does not support sub second precision",
+    )
     def test_iso8601_fractional_seconds_win(self):
         assert len(self.fake.iso8601()) == 19
 
-    @pytest.mark.skipif(sys.platform.startswith("win"), reason="non windows does support sub second precision")
+    @pytest.mark.skipif(
+        sys.platform.startswith("win"),
+        reason="non windows does support sub second precision",
+    )
     def test_iso8601_fractional_seconds_non_win(self):
         assert len(self.fake.iso8601()) == 26
 
@@ -234,6 +244,10 @@ class TestDateTime(unittest.TestCase):
         assert datetime_start <= random_date
         assert datetime_end >= random_date
 
+    def test_date_time_between_dates_with_no_date_overlap(self):
+        with pytest.raises(ValueError):
+            self.fake.date_time_between_dates("-1y", "-2y")
+
     def test_date_time_between_dates_with_tzinfo(self):
         timestamp_start = random.randint(0, 2000000000)
         timestamp_end = timestamp_start + 1
@@ -241,11 +255,15 @@ class TestDateTime(unittest.TestCase):
         datetime_start = datetime.fromtimestamp(timestamp_start, utc)
         datetime_end = datetime.fromtimestamp(timestamp_end, utc)
 
-        random_date_naive = self.fake.date_time_between_dates(datetime_start, datetime_end)
+        random_date_naive = self.fake.date_time_between_dates(
+            datetime_start, datetime_end
+        )
         with pytest.raises(TypeError):
             datetime_start <= random_date_naive
 
-        random_date = self.fake.date_time_between_dates(datetime_start, datetime_end, utc)
+        random_date = self.fake.date_time_between_dates(
+            datetime_start, datetime_end, utc
+        )
         assert datetime_start <= random_date
         assert datetime_end >= random_date
 
@@ -275,14 +293,16 @@ class TestDateTime(unittest.TestCase):
             datetime(datetime.now().year - (datetime.now().year % 100), 1, 1),
         )
 
-        assert self._datetime_to_time(self.fake.date_time_this_century(after_now=False)) <= self._datetime_to_time(
-            datetime.now()
-        )
+        assert self._datetime_to_time(
+            self.fake.date_time_this_century(after_now=False)
+        ) <= self._datetime_to_time(datetime.now())
         assert self._datetime_to_time(
             self.fake.date_time_this_century(before_now=False, after_now=True)
         ) >= self._datetime_to_time(datetime.now())
         assert (
-            self._datetime_to_time(self.fake.date_time_this_century(before_now=True, after_now=True))
+            self._datetime_to_time(
+                self.fake.date_time_this_century(before_now=True, after_now=True)
+            )
             >= this_century_start
         )
 
@@ -291,9 +311,9 @@ class TestDateTime(unittest.TestCase):
             datetime(datetime.now().year - (datetime.now().year % 10), 1, 1),
         )
 
-        assert self._datetime_to_time(self.fake.date_time_this_decade(after_now=False)) <= self._datetime_to_time(
-            datetime.now()
-        )
+        assert self._datetime_to_time(
+            self.fake.date_time_this_decade(after_now=False)
+        ) <= self._datetime_to_time(datetime.now())
         assert self._datetime_to_time(
             self.fake.date_time_this_decade(before_now=False, after_now=True)
         ) >= self._datetime_to_time(datetime.now())
@@ -301,13 +321,15 @@ class TestDateTime(unittest.TestCase):
             self.fake.date_time_this_decade(before_now=False, after_now=False)
         ) == self._datetime_to_time(datetime.now())
         assert (
-            self._datetime_to_time(self.fake.date_time_this_decade(before_now=True, after_now=True))
+            self._datetime_to_time(
+                self.fake.date_time_this_decade(before_now=True, after_now=True)
+            )
             >= this_decade_start
         )
         # test year
-        assert self._datetime_to_time(self.fake.date_time_this_year(after_now=False)) <= self._datetime_to_time(
-            datetime.now()
-        )
+        assert self._datetime_to_time(
+            self.fake.date_time_this_year(after_now=False)
+        ) <= self._datetime_to_time(datetime.now())
         assert self._datetime_to_time(
             self.fake.date_time_this_year(before_now=False, after_now=True)
         ) >= self._datetime_to_time(datetime.now())
@@ -315,9 +337,9 @@ class TestDateTime(unittest.TestCase):
             self.fake.date_time_this_year(before_now=False, after_now=False)
         ) == self._datetime_to_time(datetime.now())
         # test month
-        assert self._datetime_to_time(self.fake.date_time_this_month(after_now=False)) <= self._datetime_to_time(
-            datetime.now()
-        )
+        assert self._datetime_to_time(
+            self.fake.date_time_this_month(after_now=False)
+        ) <= self._datetime_to_time(datetime.now())
         assert self._datetime_to_time(
             self.fake.date_time_this_month(before_now=False, after_now=True)
         ) >= self._datetime_to_time(datetime.now())
@@ -329,83 +351,161 @@ class TestDateTime(unittest.TestCase):
     def test_date_time_this_period_with_tzinfo(self):
         # ensure all methods provide timezone aware datetimes
         with pytest.raises(TypeError):
-            self.fake.date_time_this_century(before_now=False, after_now=True, tzinfo=utc) >= datetime.now()
+            self.fake.date_time_this_century(
+                before_now=False, after_now=True, tzinfo=utc
+            ) >= datetime.now()
         with pytest.raises(TypeError):
-            self.fake.date_time_this_decade(after_now=False, tzinfo=utc) <= datetime.now()
+            self.fake.date_time_this_decade(
+                after_now=False, tzinfo=utc
+            ) <= datetime.now()
         with pytest.raises(TypeError):
             self.fake.date_time_this_year(after_now=False, tzinfo=utc) <= datetime.now()
         with pytest.raises(TypeError):
-            self.fake.date_time_this_month(after_now=False, tzinfo=utc) <= datetime.now()
+            self.fake.date_time_this_month(
+                after_now=False, tzinfo=utc
+            ) <= datetime.now()
 
         # test century
-        assert self.fake.date_time_this_century(after_now=False, tzinfo=utc) <= datetime.now(utc)
-        assert self.fake.date_time_this_century(before_now=False, after_now=True, tzinfo=utc) >= datetime.now(utc)
-        assert self.fake.date_time_this_century(before_now=False, after_now=False, tzinfo=utc).replace(
+        assert self.fake.date_time_this_century(
+            after_now=False, tzinfo=utc
+        ) <= datetime.now(utc)
+        assert self.fake.date_time_this_century(
+            before_now=False, after_now=True, tzinfo=utc
+        ) >= datetime.now(utc)
+        assert self.fake.date_time_this_century(
+            before_now=False, after_now=False, tzinfo=utc
+        ).replace(second=0, microsecond=0) == datetime.now(utc).replace(
             second=0, microsecond=0
-        ) == datetime.now(utc).replace(second=0, microsecond=0)
+        )
         # test decade
-        assert self.fake.date_time_this_decade(after_now=False, tzinfo=utc) <= datetime.now(utc)
-        assert self.fake.date_time_this_decade(before_now=False, after_now=True, tzinfo=utc) >= datetime.now(utc)
-
-        assert self.fake.date_time_this_decade(before_now=False, after_now=False, tzinfo=utc).replace(
+        assert self.fake.date_time_this_decade(
+            after_now=False, tzinfo=utc
+        ) <= datetime.now(utc)
+        assert self.fake.date_time_this_decade(
+            before_now=False, after_now=True, tzinfo=utc
+        ) >= datetime.now(utc)
+
+        assert self.fake.date_time_this_decade(
+            before_now=False, after_now=False, tzinfo=utc
+        ).replace(second=0, microsecond=0) == datetime.now(utc).replace(
             second=0, microsecond=0
-        ) == datetime.now(utc).replace(second=0, microsecond=0)
+        )
         # test year
-        assert self.fake.date_time_this_year(after_now=False, tzinfo=utc) <= datetime.now(utc)
-        assert self.fake.date_time_this_year(before_now=False, after_now=True, tzinfo=utc) >= datetime.now(utc)
-        assert self.fake.date_time_this_year(before_now=False, after_now=False, tzinfo=utc).replace(
+        assert self.fake.date_time_this_year(
+            after_now=False, tzinfo=utc
+        ) <= datetime.now(utc)
+        assert self.fake.date_time_this_year(
+            before_now=False, after_now=True, tzinfo=utc
+        ) >= datetime.now(utc)
+        assert self.fake.date_time_this_year(
+            before_now=False, after_now=False, tzinfo=utc
+        ).replace(second=0, microsecond=0) == datetime.now(utc).replace(
             second=0, microsecond=0
-        ) == datetime.now(utc).replace(second=0, microsecond=0)
-        assert self.fake.date_time_this_year(before_now=True, after_now=True, tzinfo=utc).year == datetime.now(utc).year
+        )
+        assert (
+            self.fake.date_time_this_year(
+                before_now=True, after_now=True, tzinfo=utc
+            ).year
+            == datetime.now(utc).year
+        )
         # test month
-        assert self.fake.date_time_this_month(after_now=False, tzinfo=utc) <= datetime.now(utc)
-        assert self.fake.date_time_this_month(before_now=False, after_now=True, tzinfo=utc) >= datetime.now(utc)
-        assert self.fake.date_time_this_month(before_now=False, after_now=False, tzinfo=utc).replace(
+        assert self.fake.date_time_this_month(
+            after_now=False, tzinfo=utc
+        ) <= datetime.now(utc)
+        assert self.fake.date_time_this_month(
+            before_now=False, after_now=True, tzinfo=utc
+        ) >= datetime.now(utc)
+        assert self.fake.date_time_this_month(
+            before_now=False, after_now=False, tzinfo=utc
+        ).replace(second=0, microsecond=0) == datetime.now(utc).replace(
             second=0, microsecond=0
-        ) == datetime.now(utc).replace(second=0, microsecond=0)
+        )
         assert (
-            self.fake.date_time_this_month(before_now=True, after_now=True, tzinfo=utc).month == datetime.now(utc).month
+            self.fake.date_time_this_month(
+                before_now=True, after_now=True, tzinfo=utc
+            ).month
+            == datetime.now(utc).month
         ) and (
-            self.fake.date_time_this_month(before_now=True, after_now=True, tzinfo=utc).year == datetime.now(utc).year
+            self.fake.date_time_this_month(
+                before_now=True, after_now=True, tzinfo=utc
+            ).year
+            == datetime.now(utc).year
         )
 
     @unittest.skipUnless(is64bit(), "requires 64bit")
     def test_date_this_period(self):
         # test century
         assert (
-            self.fake.date_this_century(before_today=True, after_today=True).strftime("%G")[:2]
+            self.fake.date_this_century(before_today=True, after_today=True).strftime(
+                "%G"
+            )[:2]
             == datetime.now().strftime("%G")[:2]
         )
         assert self.fake.date_this_century(after_today=False) <= date.today()
-        assert self.fake.date_this_century(before_today=False, after_today=True) >= date.today()
-        assert self.fake.date_this_century(before_today=False, after_today=False) == date.today()
+        assert (
+            self.fake.date_this_century(before_today=False, after_today=True)
+            >= date.today()
+        )
+        assert (
+            self.fake.date_this_century(before_today=False, after_today=False)
+            == date.today()
+        )
         # test decade
         assert (
-            self.fake.date_this_decade(before_today=True, after_today=True).strftime("%G")[:3]
+            self.fake.date_this_decade(before_today=True, after_today=True).strftime(
+                "%G"
+            )[:3]
             == datetime.now().strftime("%G")[:3]
         )
         assert self.fake.date_this_decade(after_today=False) <= date.today()
-        assert self.fake.date_this_decade(before_today=False, after_today=True) >= date.today()
-        assert self.fake.date_this_decade(before_today=False, after_today=False) == date.today()
+        assert (
+            self.fake.date_this_decade(before_today=False, after_today=True)
+            >= date.today()
+        )
+        assert (
+            self.fake.date_this_decade(before_today=False, after_today=False)
+            == date.today()
+        )
         # test year
-        assert self.fake.date_this_year(before_today=True, after_today=True).year == datetime.now().year
+        assert (
+            self.fake.date_this_year(before_today=True, after_today=True).year
+            == datetime.now().year
+        )
         assert self.fake.date_this_year(after_today=False) <= date.today()
-        assert self.fake.date_this_year(before_today=False, after_today=True) >= date.today()
-        assert self.fake.date_this_year(before_today=False, after_today=False) == date.today()
+        assert (
+            self.fake.date_this_year(before_today=False, after_today=True)
+            >= date.today()
+        )
+        assert (
+            self.fake.date_this_year(before_today=False, after_today=False)
+            == date.today()
+        )
         # test month
-        assert (self.fake.date_this_month(before_today=True, after_today=True).month == datetime.now().month) and (
-            self.fake.date_this_month(before_today=True, after_today=True).year == datetime.now().year
+        assert (
+            self.fake.date_this_month(before_today=True, after_today=True).month
+            == datetime.now().month
+        ) and (
+            self.fake.date_this_month(before_today=True, after_today=True).year
+            == datetime.now().year
         )
         assert self.fake.date_this_month(after_today=False) <= date.today()
-        assert self.fake.date_this_month(before_today=False, after_today=True) >= date.today()
-        assert self.fake.date_this_month(before_today=False, after_today=False) == date.today()
+        assert (
+            self.fake.date_this_month(before_today=False, after_today=True)
+            >= date.today()
+        )
+        assert (
+            self.fake.date_this_month(before_today=False, after_today=False)
+            == date.today()
+        )
 
     def test_date_time_between(self):
         now = datetime.now()
         _30_years_ago = change_year(now, -30)
         _20_years_ago = change_year(now, -20)
 
-        random_datetime = self.fake.date_time_between(start_date="-30y", end_date="-20y")
+        random_datetime = self.fake.date_time_between(
+            start_date="-30y", end_date="-20y"
+        )
         assert isinstance(random_datetime, datetime)
         self.assertBetween(random_datetime, _30_years_ago, _20_years_ago)
 
@@ -413,7 +513,9 @@ class TestDateTime(unittest.TestCase):
         _30_years_ago = change_year(now, -30)
         _20_years_ago = change_year(now, -20)
 
-        random_datetime = self.fake.date_time_between(start_date="-30y", end_date="-20y", tzinfo=utc)
+        random_datetime = self.fake.date_time_between(
+            start_date="-30y", end_date="-20y", tzinfo=utc
+        )
         assert isinstance(random_datetime, datetime)
         self.assertBetween(random_datetime, _30_years_ago, _20_years_ago)
 
@@ -484,7 +586,9 @@ class TestDateTime(unittest.TestCase):
         end = datetime.now(utc).replace(microsecond=0)
         start = end - timedelta(days=15)
 
-        series = list(self.fake.time_series(start_date=start, end_date=end, tzinfo=start.tzinfo))
+        series = list(
+            self.fake.time_series(start_date=start, end_date=end, tzinfo=start.tzinfo)
+        )
         assert series[0][0] == start
 
     def test_unix_time(self):
@@ -498,7 +602,9 @@ class TestDateTime(unittest.TestCase):
             start_datetime = datetime(2001, 1, 1, tzinfo=utc)
             end_datetime = datetime(2001, 1, 2, tzinfo=utc)
 
-            constrained_unix_time = self.fake.unix_time(end_datetime=end_datetime, start_datetime=start_datetime)
+            constrained_unix_time = self.fake.unix_time(
+                end_datetime=end_datetime, start_datetime=start_datetime
+            )
 
             self.assertIsInstance(constrained_unix_time, (int, float))
             self.assertBetween(
@@ -522,7 +628,9 @@ class TestDateTime(unittest.TestCase):
             # Ensure relative unix_times partially-constrained by an end time are generated correctly
             one_day_after_epoch_start = datetime(1970, 1, 2, tzinfo=utc)
 
-            distant_unix_time = self.fake.unix_time(end_datetime=one_day_after_epoch_start)
+            distant_unix_time = self.fake.unix_time(
+                end_datetime=one_day_after_epoch_start
+            )
 
             self.assertIsInstance(distant_unix_time, (int, float))
             self.assertBetween(
@@ -545,13 +653,19 @@ class TestDateTime(unittest.TestCase):
             if platform.system() != "Windows":
                 del os.environ["TZ"]
 
-    @pytest.mark.skipif(not sys.platform.startswith("win"), reason="windows does not support sub second precision")
+    @pytest.mark.skipif(
+        not sys.platform.startswith("win"),
+        reason="windows does not support sub second precision",
+    )
     def test_unix_time_win(self):
         unix_time = self.fake.unix_time()
         assert isinstance(unix_time, float)
         assert unix_time % 1 == 0.0
 
-    @pytest.mark.skipif(sys.platform.startswith("win"), reason="non windows does support sub second precision")
+    @pytest.mark.skipif(
+        sys.platform.startswith("win"),
+        reason="non windows does support sub second precision",
+    )
     def test_unix_time_non_win(self):
         unix_time = self.fake.unix_time()
         assert isinstance(unix_time, float)
@@ -730,7 +844,9 @@ class DatesOfBirth(unittest.TestCase):
             days_since_dob = now - dob
 
             assert isinstance(dob, date)
-            assert days_since_six_years_ago > days_since_dob >= days_since_five_years_ago
+            assert (
+                days_since_six_years_ago > days_since_dob >= days_since_five_years_ago
+            )
 
     def test_distant_age_range(self):
         for _ in range(100):
@@ -743,7 +859,11 @@ class DatesOfBirth(unittest.TestCase):
             days_since_dob = now - dob
 
             assert isinstance(dob, date)
-            assert days_since_one_hundred_eleven_years_ago > days_since_dob >= days_since_one_hundred_years_ago
+            assert (
+                days_since_one_hundred_eleven_years_ago
+                > days_since_dob
+                >= days_since_one_hundred_years_ago
+            )
 
 
 class TestFilPh(unittest.TestCase):