From 0b7fd8ffc5df187edf8b5d926cee359924462df5 Mon Sep 17 00:00:00 2001 From: Paul Ganssle <1377457+pganssle@users.noreply.github.com> Date: Thu, 27 Apr 2023 11:32:30 -0600 Subject: GH-103857: Deprecate utcnow and utcfromtimestamp (#103858) Using `datetime.datetime.utcnow()` and `datetime.datetime.utcfromtimestamp()` will now raise a `DeprecationWarning`. We also have removed our internal uses of these functions and documented the change. --- Doc/library/datetime.rst | 8 +++ Lib/datetime.py | 16 ++++- Lib/email/utils.py | 8 +-- Lib/http/cookiejar.py | 8 +-- Lib/test/datetimetester.py | 68 +++++++++++++++------- Lib/test/support/testresult.py | 7 ++- Lib/test/test_plistlib.py | 2 +- Lib/test/test_sqlite3/test_types.py | 2 +- .../2023-04-25-17-03-18.gh-issue-103857.Mr2Cak.rst | 2 + Modules/_datetimemodule.c | 14 +++++ 10 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-04-25-17-03-18.gh-issue-103857.Mr2Cak.rst diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 7889dd7..bed19ad 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -896,6 +896,10 @@ Other constructors, all class methods: in UTC. As such, the recommended way to create an object representing the current time in UTC is by calling ``datetime.now(timezone.utc)``. + .. deprecated:: 3.12 + + Use :meth:`datetime.now` with :attr:`UTC` instead. + .. classmethod:: datetime.fromtimestamp(timestamp, tz=None) @@ -964,6 +968,10 @@ Other constructors, all class methods: :c:func:`gmtime` function. Raise :exc:`OSError` instead of :exc:`ValueError` on :c:func:`gmtime` failure. + .. deprecated:: 3.12 + + Use :meth:`datetime.fromtimestamp` with :attr:`UTC` instead. + .. classmethod:: datetime.fromordinal(ordinal) diff --git a/Lib/datetime.py b/Lib/datetime.py index 09a2d2d..b0eb1c2 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1801,6 +1801,13 @@ class datetime(date): @classmethod def utcfromtimestamp(cls, t): """Construct a naive UTC datetime from a POSIX timestamp.""" + import warnings + warnings.warn("datetime.utcfromtimestamp() is deprecated and scheduled " + "for removal in a future version. Use timezone-aware " + "objects to represent datetimes in UTC: " + "datetime.fromtimestamp(t, datetime.UTC).", + DeprecationWarning, + stacklevel=2) return cls._fromtimestamp(t, True, None) @classmethod @@ -1812,8 +1819,15 @@ class datetime(date): @classmethod def utcnow(cls): "Construct a UTC datetime from time.time()." + import warnings + warnings.warn("datetime.utcnow() is deprecated and scheduled for " + "removal in a future version. Instead, Use timezone-aware " + "objects to represent datetimes in UTC: " + "datetime.now(datetime.UTC).", + DeprecationWarning, + stacklevel=2) t = _time.time() - return cls.utcfromtimestamp(t) + return cls._fromtimestamp(t, True, None) @classmethod def combine(cls, date, time, tzinfo=True): diff --git a/Lib/email/utils.py b/Lib/email/utils.py index 4d014ba..81da539 100644 --- a/Lib/email/utils.py +++ b/Lib/email/utils.py @@ -143,13 +143,13 @@ def formatdate(timeval=None, localtime=False, usegmt=False): # 2822 requires that day and month names be the English abbreviations. if timeval is None: timeval = time.time() - if localtime or usegmt: - dt = datetime.datetime.fromtimestamp(timeval, datetime.timezone.utc) - else: - dt = datetime.datetime.utcfromtimestamp(timeval) + dt = datetime.datetime.fromtimestamp(timeval, datetime.timezone.utc) + if localtime: dt = dt.astimezone() usegmt = False + elif not usegmt: + dt = dt.replace(tzinfo=None) return format_datetime(dt, usegmt) def format_datetime(dt, usegmt=False): diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py index 93b10d2..bd89370 100644 --- a/Lib/http/cookiejar.py +++ b/Lib/http/cookiejar.py @@ -104,9 +104,9 @@ def time2isoz(t=None): """ if t is None: - dt = datetime.datetime.utcnow() + dt = datetime.datetime.now(tz=datetime.UTC) else: - dt = datetime.datetime.utcfromtimestamp(t) + dt = datetime.datetime.fromtimestamp(t, tz=datetime.UTC) return "%04d-%02d-%02d %02d:%02d:%02dZ" % ( dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) @@ -122,9 +122,9 @@ def time2netscape(t=None): """ if t is None: - dt = datetime.datetime.utcnow() + dt = datetime.datetime.now(tz=datetime.UTC) else: - dt = datetime.datetime.utcfromtimestamp(t) + dt = datetime.datetime.fromtimestamp(t, tz=datetime.UTC) return "%s, %02d-%s-%04d %02d:%02d:%02d GMT" % ( DAYS[dt.weekday()], dt.day, MONTHS[dt.month-1], dt.year, dt.hour, dt.minute, dt.second) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 477f16f..c5eb6e7 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2437,7 +2437,8 @@ class TestDateTime(TestDate): ts = time.time() expected = time.gmtime(ts) - got = self.theclass.utcfromtimestamp(ts) + with self.assertWarns(DeprecationWarning): + got = self.theclass.utcfromtimestamp(ts) self.verify_field_equality(expected, got) # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in @@ -2483,8 +2484,12 @@ class TestDateTime(TestDate): @support.run_with_tz('MSK-03') # Something east of Greenwich def test_microsecond_rounding(self): + def utcfromtimestamp(*args, **kwargs): + with self.assertWarns(DeprecationWarning): + return self.theclass.utcfromtimestamp(*args, **kwargs) + for fts in [self.theclass.fromtimestamp, - self.theclass.utcfromtimestamp]: + utcfromtimestamp]: zero = fts(0) self.assertEqual(zero.second, 0) self.assertEqual(zero.microsecond, 0) @@ -2581,10 +2586,11 @@ class TestDateTime(TestDate): self.theclass.fromtimestamp(ts) def test_utcfromtimestamp_limits(self): - try: - self.theclass.utcfromtimestamp(-2**32 - 1) - except (OSError, OverflowError): - self.skipTest("Test not valid on this platform") + with self.assertWarns(DeprecationWarning): + try: + self.theclass.utcfromtimestamp(-2**32 - 1) + except (OSError, OverflowError): + self.skipTest("Test not valid on this platform") min_dt = self.theclass.min.replace(tzinfo=timezone.utc) min_ts = min_dt.timestamp() @@ -2597,10 +2603,11 @@ class TestDateTime(TestDate): ("maximum", max_ts, max_dt.replace(tzinfo=None)), ]: with self.subTest(test_name, ts=ts, expected=expected): - try: - actual = self.theclass.utcfromtimestamp(ts) - except (OSError, OverflowError) as exc: - self.skipTest(str(exc)) + with self.assertWarns(DeprecationWarning): + try: + actual = self.theclass.utcfromtimestamp(ts) + except (OSError, OverflowError) as exc: + self.skipTest(str(exc)) self.assertEqual(actual, expected) @@ -2645,7 +2652,8 @@ class TestDateTime(TestDate): @unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps") def test_negative_float_utcfromtimestamp(self): - d = self.theclass.utcfromtimestamp(-1.05) + with self.assertWarns(DeprecationWarning): + d = self.theclass.utcfromtimestamp(-1.05) self.assertEqual(d, self.theclass(1969, 12, 31, 23, 59, 58, 950000)) def test_utcnow(self): @@ -2655,8 +2663,11 @@ class TestDateTime(TestDate): # a second of each other. tolerance = timedelta(seconds=1) for dummy in range(3): - from_now = self.theclass.utcnow() - from_timestamp = self.theclass.utcfromtimestamp(time.time()) + with self.assertWarns(DeprecationWarning): + from_now = self.theclass.utcnow() + + with self.assertWarns(DeprecationWarning): + from_timestamp = self.theclass.utcfromtimestamp(time.time()) if abs(from_timestamp - from_now) <= tolerance: break # Else try again a few times. @@ -2956,7 +2967,11 @@ class TestDateTime(TestDate): constr_name=constr_name): constructor = getattr(base_obj, constr_name) - dt = constructor(*constr_args) + if constr_name == "utcfromtimestamp": + with self.assertWarns(DeprecationWarning): + dt = constructor(*constr_args) + else: + dt = constructor(*constr_args) # Test that it creates the right subclass self.assertIsInstance(dt, DateTimeSubclass) @@ -2986,7 +3001,11 @@ class TestDateTime(TestDate): for name, meth_name, kwargs in test_cases: with self.subTest(name): constr = getattr(DateTimeSubclass, meth_name) - dt = constr(**kwargs) + if constr == "utcnow": + with self.assertWarns(DeprecationWarning): + dt = constr(**kwargs) + else: + dt = constr(**kwargs) self.assertIsInstance(dt, DateTimeSubclass) self.assertEqual(dt.extra, 7) @@ -4642,7 +4661,8 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase, unittest.TestCase): for dummy in range(3): now = datetime.now(weirdtz) self.assertIs(now.tzinfo, weirdtz) - utcnow = datetime.utcnow().replace(tzinfo=utc) + with self.assertWarns(DeprecationWarning): + utcnow = datetime.utcnow().replace(tzinfo=utc) now2 = utcnow.astimezone(weirdtz) if abs(now - now2) < timedelta(seconds=30): break @@ -4676,7 +4696,8 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase, unittest.TestCase): # Try to make sure tz= actually does some conversion. timestamp = 1000000000 - utcdatetime = datetime.utcfromtimestamp(timestamp) + with self.assertWarns(DeprecationWarning): + utcdatetime = datetime.utcfromtimestamp(timestamp) # In POSIX (epoch 1970), that's 2001-09-09 01:46:40 UTC, give or take. # But on some flavor of Mac, it's nowhere near that. So we can't have # any idea here what time that actually is, we can only test that @@ -4690,7 +4711,8 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase, unittest.TestCase): def test_tzinfo_utcnow(self): meth = self.theclass.utcnow # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). - base = meth() + with self.assertWarns(DeprecationWarning): + base = meth() # Try with and without naming the keyword; for whatever reason, # utcnow() doesn't accept a tzinfo argument. off42 = FixedOffset(42, "42") @@ -4702,7 +4724,8 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase, unittest.TestCase): meth = self.theclass.utcfromtimestamp ts = time.time() # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). - base = meth(ts) + with self.assertWarns(DeprecationWarning): + base = meth(ts) # Try with and without naming the keyword; for whatever reason, # utcfromtimestamp() doesn't accept a tzinfo argument. off42 = FixedOffset(42, "42") @@ -5309,7 +5332,7 @@ class TestTimezoneConversions(unittest.TestCase): def test_fromutc(self): self.assertRaises(TypeError, Eastern.fromutc) # not enough args - now = datetime.utcnow().replace(tzinfo=utc_real) + now = datetime.now(tz=utc_real) self.assertRaises(ValueError, Eastern.fromutc, now) # wrong tzinfo now = now.replace(tzinfo=Eastern) # insert correct tzinfo enow = Eastern.fromutc(now) # doesn't blow up @@ -5411,9 +5434,11 @@ class Oddballs(unittest.TestCase): self.assertEqual(datetime_sc, as_datetime) def test_extra_attributes(self): + with self.assertWarns(DeprecationWarning): + utcnow = datetime.utcnow() for x in [date.today(), time(), - datetime.utcnow(), + utcnow, timedelta(), tzinfo(), timezone(timedelta())]: @@ -6073,6 +6098,7 @@ class ZoneInfo(tzinfo): def transitions(self): for (_, prev_ti), (t, ti) in pairs(zip(self.ut, self.ti)): shift = ti[0] - prev_ti[0] + # TODO: Remove this use of utcfromtimestamp yield datetime.utcfromtimestamp(t), shift def nondst_folds(self): diff --git a/Lib/test/support/testresult.py b/Lib/test/support/testresult.py index 2cd1366..14474be 100644 --- a/Lib/test/support/testresult.py +++ b/Lib/test/support/testresult.py @@ -18,10 +18,13 @@ class RegressionTestResult(unittest.TextTestResult): self.buffer = True if self.USE_XML: from xml.etree import ElementTree as ET - from datetime import datetime + from datetime import datetime, UTC self.__ET = ET self.__suite = ET.Element('testsuite') - self.__suite.set('start', datetime.utcnow().isoformat(' ')) + self.__suite.set('start', + datetime.now(UTC) + .replace(tzinfo=None) + .isoformat(' ')) self.__e = None self.__start_time = None diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py index 6b45744..b08abab 100644 --- a/Lib/test/test_plistlib.py +++ b/Lib/test/test_plistlib.py @@ -925,7 +925,7 @@ class TestBinaryPlistlib(unittest.TestCase): # Issue #26709: 32-bit timestamp out of range for ts in -2**31-1, 2**31: with self.subTest(ts=ts): - d = (datetime.datetime.utcfromtimestamp(0) + + d = (datetime.datetime(1970, 1, 1, 0, 0) + datetime.timedelta(seconds=ts)) data = plistlib.dumps(d, fmt=plistlib.FMT_BINARY) self.assertEqual(plistlib.loads(data), d) diff --git a/Lib/test/test_sqlite3/test_types.py b/Lib/test/test_sqlite3/test_types.py index 5e0ff35..fde5f88 100644 --- a/Lib/test/test_sqlite3/test_types.py +++ b/Lib/test/test_sqlite3/test_types.py @@ -517,7 +517,7 @@ class DateTimeTests(unittest.TestCase): self.assertEqual(ts, ts2) def test_sql_timestamp(self): - now = datetime.datetime.utcnow() + now = datetime.datetime.now(tz=datetime.UTC) self.cur.execute("insert into test(ts) values (current_timestamp)") self.cur.execute("select ts from test") with self.assertWarnsRegex(DeprecationWarning, "converter"): diff --git a/Misc/NEWS.d/next/Library/2023-04-25-17-03-18.gh-issue-103857.Mr2Cak.rst b/Misc/NEWS.d/next/Library/2023-04-25-17-03-18.gh-issue-103857.Mr2Cak.rst new file mode 100644 index 0000000..3bd370d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-25-17-03-18.gh-issue-103857.Mr2Cak.rst @@ -0,0 +1,2 @@ +Deprecated :meth:`datetime.datetime.utcnow` and +:meth:`datetime.datetime.utcfromtimestamp`. (Patch by Paul Ganssle) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index f317dc1..d392d38 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -5144,6 +5144,13 @@ datetime_datetime_now_impl(PyTypeObject *type, PyObject *tz) static PyObject * datetime_utcnow(PyObject *cls, PyObject *dummy) { + PyErr_WarnEx( + PyExc_DeprecationWarning, + "datetime.utcnow() is deprecated and scheduled for removal in a future " + "version. Use timezone-aware objects to represent datetimes in UTC: " + "datetime.now(datetime.UTC).", + 2 + ); return datetime_best_possible(cls, _PyTime_gmtime, Py_None); } @@ -5180,6 +5187,13 @@ datetime_fromtimestamp(PyObject *cls, PyObject *args, PyObject *kw) static PyObject * datetime_utcfromtimestamp(PyObject *cls, PyObject *args) { + PyErr_WarnEx( + PyExc_DeprecationWarning, + "datetime.utcfromtimestamp() is deprecated and scheduled for removal " + "in a future version. Use timezone-aware objects to represent " + "datetimes in UTC: datetime.now(datetime.UTC).", + 2 + ); PyObject *timestamp; PyObject *result = NULL; -- cgit v0.12