Skip to content

Commit bf430ff

Browse files
committed
Review changes
1 parent 4178b1d commit bf430ff

File tree

4 files changed

+125
-35
lines changed

4 files changed

+125
-35
lines changed

docs/changelog.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
Changelog
33
=========
44

5+
* Add marker support to :doc:`the pytest plugin <pytest_plugin>`.
6+
Decorate tests with ``@pytest.mark.time_machine(<destination>)`` to set time during a test, affecting function-level fixtures as well.
7+
8+
Thanks to Javier Buzzi in `PR #499 <https://github.com/adamchainz/time-machine/pull/499>`__.
9+
510
* Import date and time functions once in the C extension.
611

712
This should improve speed a little bit, and avoid segmentation faults when the functions have been swapped out, such as when freezegun is in effect.
@@ -10,10 +15,6 @@ Changelog
1015
2.18.0 (2025-08-18)
1116
-------------------
1217

13-
* Add a pytest marker fixture supporting initial time setting and in-test time shifting.
14-
15-
Thanks to Javier Buzzi in `PR #499 <https://github.com/adamchainz/time-machine/pull/499>`__.
16-
1718
* Update the :ref:`migration CLI <migration-cli>` to detect unittest classes based on whether they use ``self.assert*`` methods like ``self.assertEqual()``.
1819

1920
* Fix free-threaded Python warning: ``RuntimeWarning: The global interpreter lock (GIL) has been enabled...`` as seen on Python 3.13+.

docs/pytest_plugin.rst

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
pytest plugin
33
=============
44

5-
time-machine also works as a pytest plugin.
6-
It provides a marker and function-scoped fixture called ``time_machine`` with methods ``move_to()`` and ``shift()``, which have the same signature as their equivalents in ``Coordinates``.
7-
This can be used to mock your test at different points in time and will automatically be un-mock when the test is torn down.
5+
time-machine works as a pytest plugin, which pytest will detect automatically.
6+
The plugin supplies both a fixture and a marker to control the time during tests.
7+
8+
``time_machine`` marker
9+
-----------------------
10+
11+
Use the ``time_machine`` `marker <https://docs.pytest.org/en/stable/how-to/mark.html>`__ with a valid destination for :class:`~.travel` to mock the time while a test function runs.
12+
It applies for function-scoped fixtures too, meaning the time will be mocked for any setup or teardown code done in the test function.
813

914
For example:
1015

@@ -14,10 +19,38 @@ For example:
1419
1520
1621
@pytest.mark.time_machine(dt.datetime(1985, 10, 26))
17-
def test_delorean_marker(time_machine):
22+
def test_delorean_marker():
1823
assert dt.date.today().isoformat() == "1985-10-26"
19-
time_machine.move_to(dt.datetime(2015, 10, 21))
20-
assert dt.date.today().isoformat() == "2015-10-21"
24+
25+
Or for a class:
26+
27+
.. code-block:: python
28+
29+
import datetime as dt
30+
31+
import pytest
32+
33+
34+
@pytest.mark.time_machine(dt.datetime(1985, 10, 26))
35+
class TestSomething:
36+
def test_one(self):
37+
assert dt.date.today().isoformat() == "1985-10-26"
38+
39+
def test_two(self):
40+
assert dt.date.today().isoformat() == "1985-10-26"
41+
42+
``time_machine`` fixture
43+
------------------------
44+
45+
Use the function-scoped `fixture <https://docs.pytest.org/en/stable/explanation/fixtures.html#about-fixtures>`__ ``time_machine`` to control time in your tests.
46+
It provides an object with two methods, ``move_to()`` and ``shift()``, which work the same as their equivalents in the :class:`time_machine.Coordinates` class.
47+
Until you call ``move_to()``, time is not mocked.
48+
49+
For example:
50+
51+
.. code-block:: python
52+
53+
import datetime as dt
2154
2255
2356
def test_delorean(time_machine):
@@ -54,3 +87,18 @@ If you are using pytest test classes, you can apply the fixture to all test meth
5487
assert int(time.time()) == 1000.0
5588
time_machine.move_to(2000.0)
5689
assert int(time.time()) == 2000.0
90+
91+
It’s possible to combine the marker and fixture in the same test:
92+
93+
.. code-block:: python
94+
95+
import datetime as dt
96+
97+
import pytest
98+
99+
100+
@pytest.mark.time_machine(dt.datetime(1985, 10, 26))
101+
def test_delorean_marker_and_fixture(time_machine):
102+
assert dt.date.today().isoformat() == "1985-10-26"
103+
time_machine.move_to(dt.datetime(2015, 10, 21))
104+
assert dt.date.today().isoformat() == "2015-10-21"

src/time_machine/__init__.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -378,23 +378,21 @@ def time_ns() -> int:
378378
# pytest plugin
379379

380380
if HAVE_PYTEST: # pragma: no branch
381-
MARKER_NAME = "time_machine"
382-
FIXTURE_NAME = "time_machine"
383381

384382
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
385383
"""
386-
Inject our fixture into any tests with our marker
384+
Add the fixture to any tests with the marker.
387385
"""
388386
for item in items:
389-
if item.get_closest_marker(MARKER_NAME):
390-
item.fixturenames.insert(0, FIXTURE_NAME) # type: ignore[attr-defined]
387+
if item.get_closest_marker("time_machine"):
388+
item.fixturenames.insert(0, "time_machine") # type: ignore[attr-defined]
391389

392390
def pytest_configure(config: pytest.Config) -> None:
393391
"""
394-
Register our marker
392+
Register the marker.
395393
"""
396394
config.addinivalue_line(
397-
"markers", f"{MARKER_NAME}(...): use time machine to set time"
395+
"markers", "time_machine(...): set the time with time-machine"
398396
)
399397

400398
class TimeMachineFixture:
@@ -431,12 +429,12 @@ def stop(self) -> None:
431429
if self.traveller is not None:
432430
self.traveller.stop()
433431

434-
@pytest.fixture(name=FIXTURE_NAME)
432+
@pytest.fixture(name="time_machine")
435433
def time_machine_fixture(
436434
request: pytest.FixtureRequest,
437435
) -> TypingGenerator[TimeMachineFixture, None, None]:
438436
fixture = TimeMachineFixture()
439-
marker = request.node.get_closest_marker(MARKER_NAME)
437+
marker = request.node.get_closest_marker("time_machine")
440438
if marker:
441439
fixture.move_to(*marker.args, **marker.kwargs)
442440

tests/test_time_machine.py

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -948,34 +948,77 @@ def test_fixture_shift_without_move_to(time_machine):
948948
)
949949

950950

951-
def test_standalone_marker(testdir):
951+
def test_marker_function(testdir):
952952
testdir.makepyfile(
953953
"""
954954
import pytest
955955
import time
956956
957957
@pytest.fixture
958958
def current_time():
959-
return time.time()
959+
return time.time()
960960
961-
@pytest.fixture
962-
def set_time(time_machine):
963-
time_machine.move_to("2000-01-01")
964-
965-
def test_normal(current_time):
966-
assert current_time > 1742943111.0
967-
968-
@pytest.mark.time_machine("2000-01-01")
969-
def test_mod(current_time, time_machine):
970-
assert current_time == 946684800.0
971-
time_machine.shift(1)
972-
assert current_time == 946684800.0
973-
assert int(time.time()) == 946684801
961+
@pytest.mark.time_machine(0)
962+
def test(current_time):
963+
assert current_time < 10.0
964+
"""
965+
)
966+
967+
result = testdir.runpytest("-v", "-s")
968+
result.assert_outcomes(passed=1)
969+
970+
971+
def test_marker_and_fixture(testdir):
972+
testdir.makepyfile(
973+
"""
974+
import pytest
975+
import time
976+
977+
@pytest.mark.time_machine(0)
978+
def test(time_machine):
979+
assert time.time() < 10.0
980+
time_machine.shift(100)
981+
assert 100.0 <= time.time() < 110.0
982+
time_machine.move_to(0)
983+
assert time.time() < 10.0
984+
"""
985+
)
986+
result = testdir.runpytest("-v", "-s")
987+
result.assert_outcomes(passed=1)
988+
989+
990+
def test_marker_class(testdir):
991+
testdir.makepyfile(
992+
"""
993+
import pytest
994+
import time
995+
996+
@pytest.mark.time_machine(0)
997+
class TestTimeMachine:
998+
def test(self):
999+
assert time.time() < 10.0
1000+
"""
1001+
)
1002+
1003+
result = testdir.runpytest("-v", "-s")
1004+
result.assert_outcomes(passed=1)
1005+
1006+
1007+
def test_marker_module(testdir):
1008+
testdir.makepyfile(
1009+
"""
1010+
import pytest
1011+
import time
1012+
1013+
pytestmark = pytest.mark.time_machine(0)
1014+
1015+
def test_module():
1016+
assert time.time() < 10.0
9741017
"""
9751018
)
9761019

9771020
result = testdir.runpytest("-v", "-s")
978-
assert result.ret == 0
1021+
result.assert_outcomes(passed=1)
9791022

9801023

9811024
# escape hatch tests

0 commit comments

Comments
 (0)