Skip to content

Commit

Permalink
Support platform specific second precision (#1946)
Browse files Browse the repository at this point in the history
This inspects the platform and defines a platform specific property for
generating random seconds.
For windows this uses `random.randint` as used before and for everything
else `random.uniform`, which returns floats instead of ints.

Then later on when trying to generate a random number of seconds this
proper is then used to get the function that can return values with the
correct behaviour.

I tried setting the function directly on `self` in an `__init__`, but
that worked poorly when seeding the faker after creation, as seeding
recreates the internal random instance, so it requires some lazy
evaluation to be able to point to the correct random instance in all
cases.

Internally the points in time is still just ints. Changing those to
floats is not strictly important, and would just add noise to diff -
making it harder to review.
  • Loading branch information
cknv authored Nov 10, 2023
1 parent 6e258ee commit 0946c37
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 10 deletions.
26 changes: 21 additions & 5 deletions faker/providers/date_time/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import platform
import re

from calendar import timegm
Expand Down Expand Up @@ -73,6 +74,21 @@ class ParseError(ValueError):


class Provider(BaseProvider):
# NOTE: Windows only guarantee second precision, in order to emulate that
# we need to inspect the platform to determine which function is most
# appropriate to generate random seconds with.
if platform.system() == "Windows":

@property
def _rand_seconds(self):
return self.generator.random.randint

else:

@property
def _rand_seconds(self):
return self.generator.random.uniform

centuries: ElementsType[str] = [
"I",
"II",
Expand Down Expand Up @@ -1814,7 +1830,7 @@ def unix_time(
"""
start_datetime = self._parse_start_datetime(start_datetime)
end_datetime = self._parse_end_datetime(end_datetime)
return self.generator.random.randint(start_datetime, end_datetime)
return self._rand_seconds(start_datetime, end_datetime)

def time_delta(self, end_datetime: Optional[DateParseType] = None) -> timedelta:
"""
Expand All @@ -1824,7 +1840,7 @@ def time_delta(self, end_datetime: Optional[DateParseType] = None) -> timedelta:
end_datetime = self._parse_end_datetime(end_datetime)
seconds = end_datetime - start_datetime

ts = self.generator.random.randint(*sorted([0, seconds]))
ts = self._rand_seconds(*sorted([0, seconds]))
return timedelta(seconds=ts)

def date_time(
Expand Down Expand Up @@ -1867,7 +1883,7 @@ def date_time_ad(
start_time = -62135596800 if start_datetime is None else self._parse_start_datetime(start_datetime)
end_datetime = self._parse_end_datetime(end_datetime)

ts = self.generator.random.randint(start_time, end_datetime)
ts = self._rand_seconds(start_time, end_datetime)
# NOTE: using datetime.fromtimestamp(ts) directly will raise
# a "ValueError: timestamp out of range for platform time_t"
# on some platforms due to system C functions;
Expand Down Expand Up @@ -2033,7 +2049,7 @@ def date_time_between(
if end_date - start_date <= 1:
ts = start_date + self.generator.random.random()
else:
ts = self.generator.random.randint(start_date, end_date)
ts = self._rand_seconds(start_date, end_date)
if tzinfo is None:
return datetime(1970, 1, 1, tzinfo=tzinfo) + timedelta(seconds=ts)
else:
Expand Down Expand Up @@ -2132,7 +2148,7 @@ def date_time_between_dates(
datetime_to_timestamp(datetime.now(tzinfo)) if datetime_end is None else self._parse_date_time(datetime_end)
)

timestamp = self.generator.random.randint(datetime_start_, datetime_end_)
timestamp = self._rand_seconds(datetime_start_, datetime_end_)
try:
if tzinfo is None:
pick = convert_timestamp_to_datetime(timestamp, tzlocal())
Expand Down
27 changes: 22 additions & 5 deletions tests/providers/test_date_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ def test_datetimes_with_and_without_tzinfo(self):
assert not self.fake.iso8601().endswith("+00:00")
assert self.fake.iso8601(utc).endswith("+00:00")
assert self.fake.iso8601()[10] == "T"
assert len(self.fake.iso8601()) == 19
assert len(self.fake.iso8601(timespec="hours")) == 13
assert len(self.fake.iso8601(timespec="minutes")) == 16
assert len(self.fake.iso8601(timespec="seconds")) == 19
Expand All @@ -194,6 +193,14 @@ def test_datetimes_with_and_without_tzinfo(self):
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")
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")
def test_iso8601_fractional_seconds_non_win(self):
assert len(self.fake.iso8601()) == 26

def test_date_object(self):
assert isinstance(self.fake.date_object(), date)

Expand Down Expand Up @@ -493,7 +500,7 @@ def test_unix_time(self):

constrained_unix_time = self.fake.unix_time(end_datetime=end_datetime, start_datetime=start_datetime)

self.assertIsInstance(constrained_unix_time, int)
self.assertIsInstance(constrained_unix_time, (int, float))
self.assertBetween(
constrained_unix_time,
datetime_to_timestamp(start_datetime),
Expand All @@ -505,7 +512,7 @@ def test_unix_time(self):

recent_unix_time = self.fake.unix_time(start_datetime=one_day_ago)

self.assertIsInstance(recent_unix_time, int)
self.assertIsInstance(recent_unix_time, (int, float))
self.assertBetween(
recent_unix_time,
datetime_to_timestamp(one_day_ago),
Expand All @@ -517,7 +524,7 @@ def test_unix_time(self):

distant_unix_time = self.fake.unix_time(end_datetime=one_day_after_epoch_start)

self.assertIsInstance(distant_unix_time, int)
self.assertIsInstance(distant_unix_time, (int, float))
self.assertBetween(
distant_unix_time,
datetime_to_timestamp(epoch_start),
Expand All @@ -527,7 +534,7 @@ def test_unix_time(self):
# Ensure wide-open unix_times are generated correctly
self.fake.unix_time()

self.assertIsInstance(constrained_unix_time, int)
self.assertIsInstance(constrained_unix_time, (int, float))
self.assertBetween(constrained_unix_time, 0, datetime_to_timestamp(now))

# Ensure it does not throw error with startdate='now' for machines with negative offset
Expand All @@ -538,6 +545,16 @@ def test_unix_time(self):
if platform.system() != "Windows":
del os.environ["TZ"]

@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, int)

@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)

def test_change_year(self):
_2020_06_01 = datetime.strptime("2020-06-01", "%Y-%m-%d")
_20_years_ago = change_year(_2020_06_01, -20)
Expand Down

0 comments on commit 0946c37

Please sign in to comment.