Skip to content

Commit

Permalink
Raise our own utcnow DeprecationWarning with the right stacklevel
Browse files Browse the repository at this point in the history
Previously, when time is mocked, no DeprecationWarning would be
reported at all, and when time is not mocked, the original
datetime.utcnow would misattribute its reported DeprecationWarning to
the code of time_machine itself.  Fix both cases to attribute the
DeprecationWarning to the user code that called utcnow, by raising it
ourselves with the right stacklevel, and test that we did so
correctly.

Fixes adamchainz#445.

Signed-off-by: Anders Kaseorg <[email protected]>
  • Loading branch information
andersk committed Oct 23, 2024
1 parent 731c945 commit 8dd2a43
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 34 deletions.
23 changes: 0 additions & 23 deletions src/_time_machine.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ typedef struct {
#else
_PyCFunctionFastWithKeywords original_now;
#endif
PyCFunction original_utcnow;
PyCFunction original_clock_gettime;
PyCFunction original_clock_gettime_ns;
PyCFunction original_gmtime;
Expand Down Expand Up @@ -84,26 +83,6 @@ _time_machine_utcnow(PyObject *cls, PyObject *args)
return result;
}

static PyObject*
_time_machine_original_utcnow(PyObject *module, PyObject *args)
{
_time_machine_state *state = get_time_machine_state(module);

PyObject *datetime_module = PyImport_ImportModule("datetime");
PyObject *datetime_class = PyObject_GetAttrString(datetime_module, "datetime");

PyObject* result = state->original_utcnow(datetime_class, args);

Py_DECREF(datetime_class);
Py_DECREF(datetime_module);

return result;
}
PyDoc_STRVAR(original_utcnow_doc,
"original_utcnow() -> datetime\n\
\n\
Call datetime.datetime.utcnow() after patching.");

/* time.clock_gettime() */

static PyObject*
Expand Down Expand Up @@ -414,7 +393,6 @@ _time_machine_patch_if_needed(PyObject *module, PyObject *unused)
Py_DECREF(datetime_datetime_now);

PyCFunctionObject *datetime_datetime_utcnow = (PyCFunctionObject *) PyObject_GetAttrString(datetime_class, "utcnow");
state->original_utcnow = datetime_datetime_utcnow->m_ml->ml_meth;
datetime_datetime_utcnow->m_ml->ml_meth = _time_machine_utcnow;
Py_DECREF(datetime_datetime_utcnow);

Expand Down Expand Up @@ -497,7 +475,6 @@ PyDoc_STRVAR(module_doc, "_time_machine module");

static PyMethodDef module_functions[] = {
{"original_now", (PyCFunction)_time_machine_original_now, METH_FASTCALL|METH_KEYWORDS, original_now_doc},
{"original_utcnow", (PyCFunction)_time_machine_original_utcnow, METH_NOARGS, original_utcnow_doc},
#if PY_VERSION_HEX >= 0x030d00a2
{"original_clock_gettime", (PyCFunction)_time_machine_original_clock_gettime, METH_O, original_clock_gettime_doc},
{"original_clock_gettime_ns", (PyCFunction)_time_machine_original_clock_gettime_ns, METH_O, original_clock_gettime_ns_doc},
Expand Down
25 changes: 19 additions & 6 deletions src/time_machine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,11 +330,24 @@ def now(tz: dt.tzinfo | None = None) -> dt.datetime:
return dt.datetime.fromtimestamp(time(), tz)


def _deprecated_utcnow(timestamp: float, *, stacklevel: int) -> dt.datetime:
if sys.version_info >= (3, 12):
import warnings

warnings.warn(
"datetime.datetime.utcnow() is deprecated and scheduled for "
"removal in a future version. Use timezone-aware "
"objects to represent datetimes in UTC: "
"datetime.datetime.now(datetime.UTC).",
DeprecationWarning,
stacklevel=stacklevel + 1,
)

return dt.datetime.fromtimestamp(timestamp, dt.timezone.utc).replace(tzinfo=None)


def utcnow() -> dt.datetime:
if not coordinates_stack:
result: dt.datetime = _time_machine.original_utcnow()
return result
return dt.datetime.fromtimestamp(time(), dt.timezone.utc).replace(tzinfo=None)
return _deprecated_utcnow(time(), stacklevel=2)


# time module
Expand Down Expand Up @@ -451,8 +464,8 @@ def now(self, tz: dt.tzinfo | None = None) -> dt.datetime:
return result

def utcnow(self) -> dt.datetime:
result: dt.datetime = _time_machine.original_utcnow()
return result
timestamp: float = _time_machine.original_time()
return _deprecated_utcnow(timestamp, stacklevel=2)


class _EscapeHatchDatetime:
Expand Down
26 changes: 21 additions & 5 deletions tests/test_time_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from contextlib import contextmanager
from importlib.util import module_from_spec
from importlib.util import spec_from_file_location
from pathlib import Path
from textwrap import dedent
from unittest import SkipTest
from unittest import TestCase
Expand Down Expand Up @@ -111,7 +112,10 @@ def test_datetime_now_arg():

def test_datetime_utcnow():
with time_machine.travel(EPOCH):
now = dt.datetime.utcnow()
with pytest.warns(DeprecationWarning) as w:
now = dt.datetime.utcnow() # warns here
line = Path(w[0].filename).read_text().splitlines()[w[0].lineno - 1]
assert line.endswith("# warns here")
assert now.year == 1970
assert now.month == 1
assert now.day == 1
Expand All @@ -120,12 +124,18 @@ def test_datetime_utcnow():
assert now.second == 0
assert now.microsecond == 0
assert now.tzinfo is None
assert dt.datetime.utcnow() >= LIBRARY_EPOCH_DATETIME

with pytest.warns(DeprecationWarning) as w:
real_now = dt.datetime.utcnow() # warns here
line = Path(w[0].filename).read_text().splitlines()[w[0].lineno - 1]
assert line.endswith("# warns here")
assert real_now >= LIBRARY_EPOCH_DATETIME


def test_datetime_utcnow_no_tick():
with time_machine.travel(EPOCH, tick=False):
now = dt.datetime.utcnow()
with pytest.warns(DeprecationWarning):
now = dt.datetime.utcnow()
assert now.microsecond == 0


Expand Down Expand Up @@ -867,10 +877,16 @@ def test_datetime_now_tz(self):
assert eh_now >= real_now

def test_datetime_utcnow(self):
real_now = dt.datetime.utcnow()
with pytest.warns(DeprecationWarning):
real_now = dt.datetime.utcnow()

with time_machine.travel(EPOCH):
eh_now = time_machine.escape_hatch.datetime.datetime.utcnow()
with pytest.warns(DeprecationWarning) as w:
eh_now = (
time_machine.escape_hatch.datetime.datetime.utcnow() # warns here
)
line = Path(w[0].filename).read_text().splitlines()[w[0].lineno - 1]
assert line.endswith("# warns here")
assert eh_now >= real_now

@py_have_clock_gettime
Expand Down

0 comments on commit 8dd2a43

Please sign in to comment.