diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 71f619024e570d..0eb50bc5ec77a5 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -8,7 +8,7 @@ import time as _time import math as _math -import sys +import os from operator import index as _index def _cmp(x, y): @@ -1037,6 +1037,9 @@ def fromtimestamp(cls, t): "Construct a date from a POSIX timestamp (like time.time())." if t is None: raise TypeError("'NoneType' object cannot be interpreted as an integer") + if t < 0 and os.name == 'nt': + # Windows converters throw an OSError for negative values. + return cls.fromtimestamp(0) + timedelta(seconds=t) y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t) return cls(y, m, d) @@ -1854,6 +1857,10 @@ def _fromtimestamp(cls, t, utc, tz): A timezone info object may be passed in as well. """ + if t < 0 and os.name == 'nt': + # Windows converters throw an OSError for negative values. + return cls._fromtimestamp(0, utc, tz) + timedelta(seconds=t) + frac, t = _math.modf(t) us = round(frac * 1e6) if us >= 1000000: @@ -1877,7 +1884,7 @@ def _fromtimestamp(cls, t, utc, tz): # thus we can't perform fold detection for values of time less # than the max time fold. See comments in _datetimemodule's # version of this method for more details. - if t < max_fold_seconds and sys.platform.startswith("win"): + if t < max_fold_seconds and os.name == 'nt': return result y, m, d, hh, mm, ss = converter(t - max_fold_seconds)[:6] diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 93b3382b9c654e..69f78a3aed74b0 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2683,24 +2683,19 @@ def utcfromtimestamp(*args, **kwargs): self.assertEqual(zero.second, 0) self.assertEqual(zero.microsecond, 0) one = fts(1e-6) - try: - minus_one = fts(-1e-6) - except OSError: - # localtime(-1) and gmtime(-1) is not supported on Windows - pass - else: - self.assertEqual(minus_one.second, 59) - self.assertEqual(minus_one.microsecond, 999999) - - t = fts(-1e-8) - self.assertEqual(t, zero) - t = fts(-9e-7) - self.assertEqual(t, minus_one) - t = fts(-1e-7) - self.assertEqual(t, zero) - t = fts(-1/2**7) - self.assertEqual(t.second, 59) - self.assertEqual(t.microsecond, 992188) + minus_one = fts(-1e-6) + self.assertEqual(minus_one.second, 59) + self.assertEqual(minus_one.microsecond, 999999) + + t = fts(-1e-8) + self.assertEqual(t, zero) + t = fts(-9e-7) + self.assertEqual(t, minus_one) + t = fts(-1e-7) + self.assertEqual(t, zero) + t = fts(-1/2**7) + self.assertEqual(t.second, 59) + self.assertEqual(t.microsecond, 992188) t = fts(1e-7) self.assertEqual(t, zero) @@ -2735,6 +2730,7 @@ def test_timestamp_limits(self): # If that assumption changes, this value can change as well self.assertEqual(max_ts, 253402300799.0) + @unittest.skipIf(sys.platform == "win32", "Windows can't generate negative timestamps") def test_fromtimestamp_limits(self): try: self.theclass.fromtimestamp(-2**32 - 1) @@ -2774,6 +2770,7 @@ def test_fromtimestamp_limits(self): # OverflowError, especially on 32-bit platforms. self.theclass.fromtimestamp(ts) + @unittest.skipIf(sys.platform == "win32", "Windows can't generate negative timestamps") def test_utcfromtimestamp_limits(self): with self.assertWarns(DeprecationWarning): try: @@ -2835,13 +2832,11 @@ def test_insane_utcfromtimestamp(self): self.assertRaises(OverflowError, self.theclass.utcfromtimestamp, insane) - @unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps") def test_negative_float_fromtimestamp(self): # The result is tz-dependent; at least test that this doesn't # fail (like it did before bug 1646728 was fixed). self.theclass.fromtimestamp(-1.05) - @unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps") def test_negative_float_utcfromtimestamp(self): with self.assertWarns(DeprecationWarning): d = self.theclass.utcfromtimestamp(-1.05) diff --git a/Misc/NEWS.d/next/Windows/2025-05-21-12-29-59.gh-issue-80620.WKel4J.rst b/Misc/NEWS.d/next/Windows/2025-05-21-12-29-59.gh-issue-80620.WKel4J.rst new file mode 100644 index 00000000000000..15da2968f8fc5e --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2025-05-21-12-29-59.gh-issue-80620.WKel4J.rst @@ -0,0 +1,5 @@ +Support negative values (dates before the UNIX epoch of 1970-01-01) in +:meth:`datetime.date.fromtimestamp` and +:meth:`datetime.datetime.fromtimestamp` on Windows, by substituting 0 +and using :class:`datetime.timedelta` to go back in time. Patch by +John Keith Hohm. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index eb90be81c8d34c..0dd1c6ace2e3da 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3241,6 +3241,13 @@ date_new(PyTypeObject *type, PyObject *args, PyObject *kw) return self; } +static PyObject *add_datetime_timedelta(PyDateTime_DateTime *date, + PyDateTime_Delta *delta, + int factor); +static PyObject * +add_date_timedelta(PyDateTime_Date *date, PyDateTime_Delta *delta, int negate); + + static PyObject * date_fromtimestamp(PyObject *cls, PyObject *obj) { @@ -3250,6 +3257,29 @@ date_fromtimestamp(PyObject *cls, PyObject *obj) if (_PyTime_ObjectToTime_t(obj, &t, _PyTime_ROUND_FLOOR) == -1) return NULL; +#ifdef MS_WINDOWS + if (t < 0) { + if (_PyTime_localtime(0, &tm) != 0) + return NULL; + + int negate = 0; + PyObject *date = date_fromtimestamp(cls, _PyLong_GetZero()); + if (date == NULL) { + return NULL; + } + PyObject *result = NULL; + PyObject *delta = PyObject_CallFunction((PyObject*)&DELTA_TYPE(NO_STATE), "iO", 0, obj); + if (delta == NULL) { + Py_DECREF(date); + return NULL; + } + result = add_date_timedelta(PyDate_CAST(date), PyDelta_CAST(delta), negate); + Py_DECREF(delta); + Py_DECREF(date); + return result; + } +#endif + if (_PyTime_localtime(t, &tm) != 0) return NULL; @@ -4070,9 +4100,6 @@ tzinfo_dst(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(dt)) } -static PyObject *add_datetime_timedelta(PyDateTime_DateTime *date, - PyDateTime_Delta *delta, - int factor); static PyObject *datetime_utcoffset(PyObject *self, PyObject *); static PyObject *datetime_dst(PyObject *self, PyObject *); @@ -5533,6 +5560,26 @@ datetime_from_timestamp(PyObject *cls, TM_FUNC f, PyObject *timestamp, &timet, &us, _PyTime_ROUND_HALF_EVEN) == -1) return NULL; +#ifdef MS_WINDOWS + if (timet < 0) { + int factor = 1; + PyObject *dt = datetime_from_timet_and_us(cls, f, 0, 0, tzinfo); + if (dt == NULL) { + return NULL; + } + PyObject *result = NULL; + PyObject *delta = PyObject_CallFunction((PyObject*)&DELTA_TYPE(NO_STATE), "iO", 0, timestamp); + if (delta == NULL) { + Py_DECREF(dt); + return NULL; + } + result = add_datetime_timedelta(PyDateTime_CAST(dt), PyDelta_CAST(delta), factor); + Py_DECREF(delta); + Py_DECREF(dt); + return result; + } +#endif + return datetime_from_timet_and_us(cls, f, timet, (int)us, tzinfo); }