././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1728396564.596161 time_machine-2.16.0/0000755000175100001660000000000014701236425013631 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396560.0 time_machine-2.16.0/CHANGELOG.rst0000644000175100001660000001577014701236420015657 0ustar00runnerdocker========= Changelog ========= 2.16.0 (2024-10-08) ------------------- * Drop Python 3.8 support. 2.15.0 (2024-08-06) ------------------- * Include wheels for Python 3.13. 2.14.2 (2024-06-29) ------------------- * Fix ``SystemError`` on Python 3.13 and Windows when starting time travelling. Thanks to Bernát Gábor for the report in `Issue #456 `__. 2.14.1 (2024-03-22) ------------------- * Fix segmentation fault when the first ``travel()`` call in a process uses a ``timedelta``. Thanks to Marcin Sulikowski for the report in `Issue #431 `__. 2.14.0 (2024-03-03) ------------------- * Fix ``utcfromtimestamp()`` warning on Python 3.12+. Thanks to Konstantin Baikov in `PR #424 `__. * Fix class decorator for classmethod overrides. Thanks to Pavel Bitiukov for the reproducer in `PR #404 `__. * Avoid calling deprecated ``uuid._load_system_functions()`` on Python 3.9+. Thanks to Nikita Sobolev for the ping in `CPython Issue #113308 `__. * Support Python 3.13 alpha 4. Thanks to Miro Hrončok in `PR #409 `__. 2.13.0 (2023-09-19) ------------------- * Add support for ``datetime.timedelta`` to ``time_machine.travel()``. Thanks to Nate Dudenhoeffer in `PR #298 `__. * Fix documentation about using local time for naive date(time) strings. Thanks to Stefaan Lippens in `PR #306 `__. * Add ``shift()`` method to the ``time_machine`` pytest fixture. Thanks to Stefaan Lippens in `PR #312 `__. * Mock ``time.monotonic()`` and ``time.monotonic_ns()``. They return the values of ``time.time()`` and ``time.time_ns()`` respectively, rather than real monotonic clocks. Thanks to Anthony Sottile in `PR #382 `__. 2.12.0 (2023-08-14) ------------------- * Include wheels for Python 3.12. 2.11.0 (2023-07-10) ------------------- * Drop Python 3.7 support. 2.10.0 (2023-06-16) ------------------- * Support Python 3.12. 2.9.0 (2022-12-31) ------------------ * Build Windows ARM64 wheels. * Explicitly error when attempting to install on PyPy. Thanks to Michał Górny in `PR #315 `__. 2.8.2 (2022-09-29) ------------------ * Improve type hints for ``time_machine.travel()`` to preserve the types of the wrapped function/coroutine/class. 2.8.1 (2022-08-16) ------------------ * Actually build Python 3.11 wheels. 2.8.0 (2022-08-15) ------------------ * Build Python 3.11 wheels. 2.7.1 (2022-06-24) ------------------ * Fix usage of ``ZoneInfo`` from the ``backports.zoneinfo`` package. This makes ``ZoneInfo`` support work for Python < 3.9. 2.7.0 (2022-05-11) ------------------ * Support Python 3.11 (no wheels yet, they will only be available when Python 3.11 is RC when the ABI is stable). 2.6.0 (2022-01-10) ------------------ * Drop Python 3.6 support. 2.5.0 (2021-12-14) ------------------ * Add ``time_machine.escape_hatch``, which provides functions to bypass time-machine. Thanks to Matt Pegler for the feature request in `Issue #206 `__. 2.4.1 (2021-11-27) ------------------ * Build musllinux wheels. 2.4.0 (2021-09-01) ------------------ * Support Python 3.10. 2.3.1 (2021-07-13) ------------------ * Build universal2 wheels for Python 3.8 on macOS. 2.3.0 (2021-07-05) ------------------ * Allow passing ``tick`` to ``Coordinates.move_to()`` and the pytest fixture’s ``time_machine.move_to()``. This allows freezing or unfreezing of time when travelling. 2.2.0 (2021-07-02) ------------------ * Include type hints. * Convert C module to use PEP 489 multi-phase extension module initialization. This makes the module ready for Python sub-interpreters. * Release now includes a universal2 wheel for Python 3.9 on macOS, to work on Apple Silicon. * Stop distributing tests to reduce package size. Tests are not intended to be run outside of the tox setup in the repository. Repackagers can use GitHub's tarballs per tag. 2.1.0 (2021-02-19) ------------------ * Release now includes wheels for ARM on Linux. 2.0.1 (2021-01-18) ------------------ * Prevent ``ImportError`` on Windows where ``time.tzset()`` is unavailable. 2.0.0 (2021-01-17) ------------------ * Release now includes wheels for Windows and macOS. * Move internal calculations to use nanoseconds, avoiding a loss of precision. * After a call to ``move_to()``, the first function call to retrieve the current time will return exactly the destination time, copying the behaviour of the first call to ``travel()``. * Add the ability to shift timezone by passing in a ``ZoneInfo`` timezone. * Remove ``tz_offset`` argument. This was incorrectly copied from ``freezegun``. Use the new timezone mocking with ``ZoneInfo`` instead. * Add pytest plugin and fixture ``time_machine``. * Work with Windows’ different epoch. 1.3.0 (2020-12-12) ------------------ * Support Python 3.9. * Move license from ISC to MIT License. 1.2.1 (2020-08-29) ------------------ * Correctly return naive datetimes from ``datetime.utcnow()`` whilst time travelling. Thanks to Søren Pilgård and Bart Van Loon for the report in `Issue #52 `__. 1.2.0 (2020-07-08) ------------------ * Add ``move_to()`` method to move to a different time whilst travelling. This is based on freezegun's ``move_to()`` method. 1.1.1 (2020-06-22) ------------------ * Move C-level ``clock_gettime()`` and ``clock_gettime_ns()`` checks to runtime to allow distribution of macOS wheels. 1.1.0 (2020-06-08) ------------------ * Add ``shift()`` method to move forward in time by a delta whilst travelling. This is based on freezegun's ``tick()`` method. Thanks to Alex Subbotin for the feature in `PR #27 `__. * Fix to work when either ``clock_gettime()`` or ``CLOCK_REALTIME`` is not present. This happens on some Unix platforms, for example on macOS with the official Python.org installer, which is compiled against macOS 10.9. Thanks to Daniel Crowe for the fix in `PR #30 `__. 1.0.1 (2020-05-29) ------------------ * Fix ``datetime.now()`` behaviour with the ``tz`` argument when not time-travelling. 1.0.0 (2020-05-29) ------------------ * First non-beta release. * Added support for ``tz_offset`` argument. * ``tick=True`` will only start time ticking after the first method return that retrieves the current time. * Added nestability of ``travel()``. * Support for ``time.time_ns()`` and ``time.clock_gettime_ns()``. 1.0.0b1 (2020-05-04) -------------------- * First release on PyPI. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396560.0 time_machine-2.16.0/LICENSE0000644000175100001660000000205514701236420014633 0ustar00runnerdockerMIT License Copyright (c) 2020 Adam Johnson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396560.0 time_machine-2.16.0/MANIFEST.in0000644000175100001660000000016314701236420015362 0ustar00runnerdockerprune tests include CHANGELOG.rst include LICENSE include pyproject.toml include README.rst include src/*/py.typed ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1728396564.595161 time_machine-2.16.0/PKG-INFO0000644000175100001660000005104614701236425014734 0ustar00runnerdockerMetadata-Version: 2.1 Name: time-machine Version: 2.16.0 Summary: Travel through time in your tests. Author-email: Adam Johnson Project-URL: Changelog, https://github.com/adamchainz/time-machine/blob/main/CHANGELOG.rst Project-URL: Funding, https://adamj.eu/books/ Project-URL: Repository, https://github.com/adamchainz/time-machine Keywords: date,datetime,mock,test,testing,tests,time,warp Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Typing :: Typed Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: python-dateutil ============ time-machine ============ .. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/time-machine/main.yml.svg?branch=main&style=for-the-badge :target: https://github.com/adamchainz/time-machine/actions?workflow=CI .. image:: https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge :target: https://github.com/adamchainz/time-machine/actions?workflow=CI .. image:: https://img.shields.io/pypi/v/time-machine.svg?style=for-the-badge :target: https://pypi.org/project/time-machine/ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge :target: https://github.com/psf/black .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge :target: https://github.com/pre-commit/pre-commit :alt: pre-commit Travel through time in your tests. A quick example: .. code-block:: python import datetime as dt from zoneinfo import ZoneInfo import time_machine hill_valley_tz = ZoneInfo("America/Los_Angeles") @time_machine.travel(dt.datetime(1985, 10, 26, 1, 24, tzinfo=hill_valley_tz)) def test_delorean(): assert dt.date.today().isoformat() == "1985-10-26" For a bit of background, see `the introductory blog post `__ and `the benchmark blog post `__. ---- **Testing a Django project?** Check out my book `Speed Up Your Django Tests `__ which covers loads of ways to write faster, more accurate tests. I created time-machine whilst writing the book. ---- Installation ============ Use **pip**: .. code-block:: sh python -m pip install time-machine Python 3.9 to 3.13 supported. Only CPython is supported at this time because time-machine directly hooks into the C-level API. Usage ===== If you’re coming from freezegun or libfaketime, see also the below section on migrating. ``travel(destination, *, tick=True)`` ------------------------------------- ``travel()`` is a class that allows time travel, to the datetime specified by ``destination``. It does so by mocking all functions from Python's standard library that return the current date or datetime. It can be used independently, as a function decorator, or as a context manager. ``destination`` specifies the datetime to move to. It may be: * A ``datetime.datetime``. If it is naive, it will be assumed to have the UTC timezone. If it has ``tzinfo`` set to a |zoneinfo-instance|_, the current timezone will also be mocked. * A ``datetime.date``. This will be converted to a UTC datetime with the time 00:00:00. * A ``datetime.timedelta``. This will be interpreted relative to the current time. If already within a ``travel()`` block, the ``shift()`` method is easier to use (documented below). * A ``float`` or ``int`` specifying a `Unix timestamp `__ * A string, which will be parsed with `dateutil.parse `__ and converted to a timestamp. If the result is naive, it will be assumed to be local time. .. |zoneinfo-instance| replace:: ``zoneinfo.ZoneInfo`` instance .. _zoneinfo-instance: https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo Additionally, you can provide some more complex types: * A generator, in which case ``next()`` will be called on it, with the result treated as above. * A callable, in which case it will be called with no parameters, with the result treated as above. ``tick`` defines whether time continues to "tick" after travelling, or is frozen. If ``True``, the default, successive calls to mocked functions return values increasing by the elapsed real time *since the first call.* So after starting travel to ``0.0`` (the UNIX epoch), the first call to any datetime function will return its representation of ``1970-01-01 00:00:00.000000`` exactly. The following calls "tick," so if a call was made exactly half a second later, it would return ``1970-01-01 00:00:00.500000``. Mocked Functions ^^^^^^^^^^^^^^^^ All datetime functions in the standard library are mocked to move to the destination current datetime: * ``datetime.datetime.now()`` * ``datetime.datetime.utcnow()`` * ``time.clock_gettime()`` (only for ``CLOCK_REALTIME``) * ``time.clock_gettime_ns()`` (only for ``CLOCK_REALTIME``) * ``time.gmtime()`` * ``time.localtime()`` * ``time.monotonic()`` (not a real monotonic clock, returns ``time.time()``) * ``time.monotonic_ns()`` (not a real monotonic clock, returns ``time.time_ns()``) * ``time.strftime()`` * ``time.time()`` * ``time.time_ns()`` The mocking is done at the C layer, replacing the function pointers for these built-ins. Therefore, it automatically affects everywhere those functions have been imported, unlike use of ``unittest.mock.patch()``. Usage with ``start()`` / ``stop()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To use independently, create an instance, use ``start()`` to move to the destination time, and ``stop()`` to move back. For example: .. code-block:: python import datetime as dt import time_machine traveller = time_machine.travel(dt.datetime(1985, 10, 26)) traveller.start() # It's the past! assert dt.date.today() == dt.date(1985, 10, 26) traveller.stop() # We've gone back to the future! assert dt.date.today() > dt.date(2020, 4, 29) ``travel()`` instances are nestable, but you'll need to be careful when manually managing to call their ``stop()`` methods in the correct order, even when exceptions occur. It's recommended to use the decorator or context manager forms instead, to take advantage of Python features to do this. Function Decorator ^^^^^^^^^^^^^^^^^^ When used as a function decorator, time is mocked during the wrapped function's duration: .. code-block:: python import time import time_machine @time_machine.travel("1970-01-01 00:00 +0000") def test_in_the_deep_past(): assert 0.0 < time.time() < 1.0 You can also decorate asynchronous functions (coroutines): .. code-block:: python import time import time_machine @time_machine.travel("1970-01-01 00:00 +0000") async def test_in_the_deep_past(): assert 0.0 < time.time() < 1.0 Beware: time is a *global* state - `see below <#caveats>`__. Context Manager ^^^^^^^^^^^^^^^ When used as a context manager, time is mocked during the ``with`` block: .. code-block:: python import time import time_machine def test_in_the_deep_past(): with time_machine.travel(0.0): assert 0.0 < time.time() < 1.0 Class Decorator ^^^^^^^^^^^^^^^ Only ``unittest.TestCase`` subclasses are supported. When applied as a class decorator to such classes, time is mocked from the start of ``setUpClass()`` to the end of ``tearDownClass()``: .. code-block:: python import time import time_machine import unittest @time_machine.travel(0.0) class DeepPastTests(TestCase): def test_in_the_deep_past(self): assert 0.0 < time.time() < 1.0 Note this is different to ``unittest.mock.patch()``\'s behaviour, which is to mock only during the test methods. For pytest-style test classes, see the pattern `documented below <#pytest-plugin>`__. Timezone mocking ^^^^^^^^^^^^^^^^ If the ``destination`` passed to ``time_machine.travel()`` or ``Coordinates.move_to()`` has its ``tzinfo`` set to a |zoneinfo-instance2|_, the current timezone will be mocked. This will be done by calling |time-tzset|_, so it is only available on Unix. .. |zoneinfo-instance2| replace:: ``zoneinfo.ZoneInfo`` instance .. _zoneinfo-instance2: https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo .. |time-tzset| replace:: ``time.tzset()`` .. _time-tzset: https://docs.python.org/3/library/time.html#time.tzset ``time.tzset()`` changes the ``time`` module’s `timezone constants `__ and features that rely on those, such as ``time.localtime()``. It won’t affect other concepts of “the current timezone”, such as Django’s (which can be changed with its |timezone-override|_). .. |timezone-override| replace:: ``timezone.override()`` .. _timezone-override: https://docs.djangoproject.com/en/stable/ref/utils/#django.utils.timezone.override Here’s a worked example changing the current timezone: .. code-block:: python import datetime as dt import time from zoneinfo import ZoneInfo import time_machine hill_valley_tz = ZoneInfo("America/Los_Angeles") @time_machine.travel(dt.datetime(2015, 10, 21, 16, 29, tzinfo=hill_valley_tz)) def test_hoverboard_era(): assert time.tzname == ("PST", "PDT") now = dt.datetime.now() assert (now.hour, now.minute) == (16, 29) ``Coordinates`` --------------- The ``start()`` method and entry of the context manager both return a ``Coordinates`` object that corresponds to the given "trip" in time. This has a couple methods that can be used to travel to other times. ``move_to(destination, tick=None)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``move_to()`` moves the current time to a new destination. ``destination`` may be any of the types supported by ``travel``. ``tick`` may be set to a boolean, to change the ``tick`` flag of ``travel``. For example: .. code-block:: python import datetime as dt import time import time_machine with time_machine.travel(0, tick=False) as traveller: assert time.time() == 0 traveller.move_to(234) assert time.time() == 234 ``shift(delta)`` ^^^^^^^^^^^^^^^^ ``shift()`` takes one argument, ``delta``, which moves the current time by the given offset. ``delta`` may be a ``timedelta`` or a number of seconds, which will be added to destination. It may be negative, in which case time will move to an earlier point. For example: .. code-block:: python import datetime as dt import time import time_machine with time_machine.travel(0, tick=False) as traveller: assert time.time() == 0 traveller.shift(dt.timedelta(seconds=100)) assert time.time() == 100 traveller.shift(-dt.timedelta(seconds=10)) assert time.time() == 90 pytest plugin ------------- time-machine also works as a pytest plugin. It provides a function-scoped fixture called ``time_machine`` with methods ``move_to()`` and ``shift()``, which have the same signature as their equivalents in ``Coordinates``. 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. For example: .. code-block:: python import datetime as dt def test_delorean(time_machine): time_machine.move_to(dt.datetime(1985, 10, 26)) assert dt.date.today().isoformat() == "1985-10-26" time_machine.move_to(dt.datetime(2015, 10, 21)) assert dt.date.today().isoformat() == "2015-10-21" time_machine.shift(dt.timedelta(days=1)) assert dt.date.today().isoformat() == "2015-10-22" If you are using pytest test classes, you can apply the fixture to all test methods in a class by adding an autouse fixture: .. code-block:: python import time import pytest class TestSomething: @pytest.fixture(autouse=True) def set_time(self, time_machine): time_machine.move_to(1000.0) def test_one(self): assert int(time.time()) == 1000.0 def test_two(self, time_machine): assert int(time.time()) == 1000.0 time_machine.move_to(2000.0) assert int(time.time()) == 2000.0 ``escape_hatch`` ---------------- The ``escape_hatch`` object provides functions to bypass time-machine. These allow you to call the real datetime functions, without any mocking. It also provides a way to check if time-machine is currently time travelling. These capabilities are useful in rare circumstances. For example, if you need to authenticate with an external service during time travel, you may need the real value of ``datetime.now()``. The functions are: * ``escape_hatch.is_travelling() -> bool`` - returns ``True`` if ``time_machine.travel()`` is active, ``False`` otherwise. * ``escape_hatch.datetime.datetime.now()`` - wraps the real ``datetime.datetime.now()``. * ``escape_hatch.datetime.datetime.utcnow()`` - wraps the real ``datetime.datetime.utcnow()``. * ``escape_hatch.time.clock_gettime()`` - wraps the real ``time.clock_gettime()``. * ``escape_hatch.time.clock_gettime_ns()`` - wraps the real ``time.clock_gettime_ns()``. * ``escape_hatch.time.gmtime()`` - wraps the real ``time.gmtime()``. * ``escape_hatch.time.localtime()`` - wraps the real ``time.localtime()``. * ``escape_hatch.time.strftime()`` - wraps the real ``time.strftime()``. * ``escape_hatch.time.time()`` - wraps the real ``time.time()``. * ``escape_hatch.time.time_ns()`` - wraps the real ``time.time_ns()``. For example: .. code-block:: python import time_machine with time_machine.travel(...): if time_machine.escape_hatch.is_travelling(): print("We need to go back to the future!") real_now = time_machine.escape_hatch.datetime.datetime.now() external_authenticate(now=real_now) Caveats ======= Time is a global state. Any concurrent threads or asynchronous functions are also be affected. Some aren't ready for time to move so rapidly or backwards, and may crash or produce unexpected results. Also beware that other processes are not affected. For example, if you use SQL datetime functions on a database server, they will return the real time. Comparison ========== There are some prior libraries that try to achieve the same thing. They have their own strengths and weaknesses. Here's a quick comparison. unittest.mock ------------- The standard library's `unittest.mock `__ can be used to target imports of ``datetime`` and ``time`` to change the returned value for current time. Unfortunately, this is fragile as it only affects the import location the mock targets. Therefore, if you have several modules in a call tree requesting the date/time, you need several mocks. This is a general problem with unittest.mock - see `Why Your Mock Doesn't Work `__. It's also impossible to mock certain references, such as function default arguments: .. code-block:: python def update_books(_now=time.time): # set as default argument so faster lookup for book in books: ... Although such references are rare, they are occasionally used to optimize highly repeated loops. freezegun --------- Steve Pulec's `freezegun `__ library is a popular solution. It provides a clear API which was much of the inspiration for time-machine. The main drawback is its slow implementation. It essentially does a find-and-replace mock of all the places that the ``datetime`` and ``time`` modules have been imported. This gets around the problems with using unittest.mock, but it means the time it takes to do the mocking is proportional to the number of loaded modules. In large projects, this can take several seconds, an impractical overhead for an individual test. It's also not a perfect search, since it searches only module-level imports. Such imports are definitely the most common way projects use date and time functions, but they're not the only way. freezegun won’t find functions that have been “hidden” inside arbitrary objects, such as class-level attributes. It also can't affect C extensions that call the standard library functions, including (I believe) Cython-ized Python code. python-libfaketime ------------------ Simon Weber's `python-libfaketime `__ wraps the `libfaketime `__ library. libfaketime replaces all the C-level system calls for the current time with its own wrappers. It's therefore a "perfect" mock for the current process, affecting every single point the current time might be fetched, and performs much faster than freezegun. Unfortunately python-libfaketime comes with the limitations of ``LD_PRELOAD``. This is a mechanism to replace system libraries for a program as it loads (`explanation `__). This causes two issues in particular when you use python-libfaketime. First, ``LD_PRELOAD`` is only available on Unix platforms, which prevents you from using it on Windows. Second, you have to help manage ``LD_PRELOAD``. You either use python-libfaketime's ``reexec_if_needed()`` function, which restarts (*re-execs*) your test process while loading, or manually manage the ``LD_PRELOAD`` environment variable. Neither is ideal. Re-execing breaks anything that might wrap your test process, such as profilers, debuggers, and IDE test runners. Manually managing the environment variable is a bit of overhead, and must be done for each environment you run your tests in, including each developer's machine. time-machine ------------ time-machine is intended to combine the advantages of freezegun and libfaketime. It works without ``LD_PRELOAD`` but still mocks the standard library functions everywhere they may be referenced. Its weak point is that other libraries using date/time system calls won't be mocked. Thankfully this is rare. It's also possible such python libraries can be added to the set mocked by time-machine. One drawback is that it only works with CPython, so can't be used with other Python interpreters like PyPy. However it may possible to extend it to support other interpreters through different mocking mechanisms. Migrating from libfaketime or freezegun ======================================= freezegun has a useful API, and python-libfaketime copies some of it, with a different function name. time-machine also copies some of freezegun's API, in ``travel()``\'s ``destination``, and ``tick`` arguments, and the ``shift()`` method. There are a few differences: * time-machine's ``tick`` argument defaults to ``True``, because code tends to make the (reasonable) assumption that time progresses whilst running, and should normally be tested as such. Testing with time frozen can make it easy to write complete assertions, but it's quite artificial. Write assertions against time ranges, rather than against exact values. * freezegun interprets dates and naive datetimes in the local time zone (including those parsed from strings with ``dateutil``). This means tests can pass when run in one time zone and fail in another. time-machine instead interprets dates and naive datetimes in UTC so they are fixed points in time. Provide time zones where required. * freezegun's ``tick()`` method has been implemented as ``shift()``, to avoid confusion with the ``tick`` argument. It also requires an explicit delta rather than defaulting to 1 second. * freezegun's ``tz_offset`` argument is not supported, since it only partially mocks the current time zone. Time zones are more complicated than a single offset from UTC, and freezegun only uses the offset in ``time.localtime()``. Instead, time-machine will mock the current time zone if you give it a ``datetime`` with a ``ZoneInfo`` timezone. Some features aren't supported like the ``auto_tick_seconds`` argument. These may be added in a future release. If you are only fairly simple function calls, you should be able to migrate by replacing calls to ``freezegun.freeze_time()`` and ``libfaketime.fake_time()`` with ``time_machine.travel()``. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396560.0 time_machine-2.16.0/README.rst0000644000175100001660000004667714701236420015337 0ustar00runnerdocker============ time-machine ============ .. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/time-machine/main.yml.svg?branch=main&style=for-the-badge :target: https://github.com/adamchainz/time-machine/actions?workflow=CI .. image:: https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge :target: https://github.com/adamchainz/time-machine/actions?workflow=CI .. image:: https://img.shields.io/pypi/v/time-machine.svg?style=for-the-badge :target: https://pypi.org/project/time-machine/ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge :target: https://github.com/psf/black .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge :target: https://github.com/pre-commit/pre-commit :alt: pre-commit Travel through time in your tests. A quick example: .. code-block:: python import datetime as dt from zoneinfo import ZoneInfo import time_machine hill_valley_tz = ZoneInfo("America/Los_Angeles") @time_machine.travel(dt.datetime(1985, 10, 26, 1, 24, tzinfo=hill_valley_tz)) def test_delorean(): assert dt.date.today().isoformat() == "1985-10-26" For a bit of background, see `the introductory blog post `__ and `the benchmark blog post `__. ---- **Testing a Django project?** Check out my book `Speed Up Your Django Tests `__ which covers loads of ways to write faster, more accurate tests. I created time-machine whilst writing the book. ---- Installation ============ Use **pip**: .. code-block:: sh python -m pip install time-machine Python 3.9 to 3.13 supported. Only CPython is supported at this time because time-machine directly hooks into the C-level API. Usage ===== If you’re coming from freezegun or libfaketime, see also the below section on migrating. ``travel(destination, *, tick=True)`` ------------------------------------- ``travel()`` is a class that allows time travel, to the datetime specified by ``destination``. It does so by mocking all functions from Python's standard library that return the current date or datetime. It can be used independently, as a function decorator, or as a context manager. ``destination`` specifies the datetime to move to. It may be: * A ``datetime.datetime``. If it is naive, it will be assumed to have the UTC timezone. If it has ``tzinfo`` set to a |zoneinfo-instance|_, the current timezone will also be mocked. * A ``datetime.date``. This will be converted to a UTC datetime with the time 00:00:00. * A ``datetime.timedelta``. This will be interpreted relative to the current time. If already within a ``travel()`` block, the ``shift()`` method is easier to use (documented below). * A ``float`` or ``int`` specifying a `Unix timestamp `__ * A string, which will be parsed with `dateutil.parse `__ and converted to a timestamp. If the result is naive, it will be assumed to be local time. .. |zoneinfo-instance| replace:: ``zoneinfo.ZoneInfo`` instance .. _zoneinfo-instance: https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo Additionally, you can provide some more complex types: * A generator, in which case ``next()`` will be called on it, with the result treated as above. * A callable, in which case it will be called with no parameters, with the result treated as above. ``tick`` defines whether time continues to "tick" after travelling, or is frozen. If ``True``, the default, successive calls to mocked functions return values increasing by the elapsed real time *since the first call.* So after starting travel to ``0.0`` (the UNIX epoch), the first call to any datetime function will return its representation of ``1970-01-01 00:00:00.000000`` exactly. The following calls "tick," so if a call was made exactly half a second later, it would return ``1970-01-01 00:00:00.500000``. Mocked Functions ^^^^^^^^^^^^^^^^ All datetime functions in the standard library are mocked to move to the destination current datetime: * ``datetime.datetime.now()`` * ``datetime.datetime.utcnow()`` * ``time.clock_gettime()`` (only for ``CLOCK_REALTIME``) * ``time.clock_gettime_ns()`` (only for ``CLOCK_REALTIME``) * ``time.gmtime()`` * ``time.localtime()`` * ``time.monotonic()`` (not a real monotonic clock, returns ``time.time()``) * ``time.monotonic_ns()`` (not a real monotonic clock, returns ``time.time_ns()``) * ``time.strftime()`` * ``time.time()`` * ``time.time_ns()`` The mocking is done at the C layer, replacing the function pointers for these built-ins. Therefore, it automatically affects everywhere those functions have been imported, unlike use of ``unittest.mock.patch()``. Usage with ``start()`` / ``stop()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To use independently, create an instance, use ``start()`` to move to the destination time, and ``stop()`` to move back. For example: .. code-block:: python import datetime as dt import time_machine traveller = time_machine.travel(dt.datetime(1985, 10, 26)) traveller.start() # It's the past! assert dt.date.today() == dt.date(1985, 10, 26) traveller.stop() # We've gone back to the future! assert dt.date.today() > dt.date(2020, 4, 29) ``travel()`` instances are nestable, but you'll need to be careful when manually managing to call their ``stop()`` methods in the correct order, even when exceptions occur. It's recommended to use the decorator or context manager forms instead, to take advantage of Python features to do this. Function Decorator ^^^^^^^^^^^^^^^^^^ When used as a function decorator, time is mocked during the wrapped function's duration: .. code-block:: python import time import time_machine @time_machine.travel("1970-01-01 00:00 +0000") def test_in_the_deep_past(): assert 0.0 < time.time() < 1.0 You can also decorate asynchronous functions (coroutines): .. code-block:: python import time import time_machine @time_machine.travel("1970-01-01 00:00 +0000") async def test_in_the_deep_past(): assert 0.0 < time.time() < 1.0 Beware: time is a *global* state - `see below <#caveats>`__. Context Manager ^^^^^^^^^^^^^^^ When used as a context manager, time is mocked during the ``with`` block: .. code-block:: python import time import time_machine def test_in_the_deep_past(): with time_machine.travel(0.0): assert 0.0 < time.time() < 1.0 Class Decorator ^^^^^^^^^^^^^^^ Only ``unittest.TestCase`` subclasses are supported. When applied as a class decorator to such classes, time is mocked from the start of ``setUpClass()`` to the end of ``tearDownClass()``: .. code-block:: python import time import time_machine import unittest @time_machine.travel(0.0) class DeepPastTests(TestCase): def test_in_the_deep_past(self): assert 0.0 < time.time() < 1.0 Note this is different to ``unittest.mock.patch()``\'s behaviour, which is to mock only during the test methods. For pytest-style test classes, see the pattern `documented below <#pytest-plugin>`__. Timezone mocking ^^^^^^^^^^^^^^^^ If the ``destination`` passed to ``time_machine.travel()`` or ``Coordinates.move_to()`` has its ``tzinfo`` set to a |zoneinfo-instance2|_, the current timezone will be mocked. This will be done by calling |time-tzset|_, so it is only available on Unix. .. |zoneinfo-instance2| replace:: ``zoneinfo.ZoneInfo`` instance .. _zoneinfo-instance2: https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo .. |time-tzset| replace:: ``time.tzset()`` .. _time-tzset: https://docs.python.org/3/library/time.html#time.tzset ``time.tzset()`` changes the ``time`` module’s `timezone constants `__ and features that rely on those, such as ``time.localtime()``. It won’t affect other concepts of “the current timezone”, such as Django’s (which can be changed with its |timezone-override|_). .. |timezone-override| replace:: ``timezone.override()`` .. _timezone-override: https://docs.djangoproject.com/en/stable/ref/utils/#django.utils.timezone.override Here’s a worked example changing the current timezone: .. code-block:: python import datetime as dt import time from zoneinfo import ZoneInfo import time_machine hill_valley_tz = ZoneInfo("America/Los_Angeles") @time_machine.travel(dt.datetime(2015, 10, 21, 16, 29, tzinfo=hill_valley_tz)) def test_hoverboard_era(): assert time.tzname == ("PST", "PDT") now = dt.datetime.now() assert (now.hour, now.minute) == (16, 29) ``Coordinates`` --------------- The ``start()`` method and entry of the context manager both return a ``Coordinates`` object that corresponds to the given "trip" in time. This has a couple methods that can be used to travel to other times. ``move_to(destination, tick=None)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``move_to()`` moves the current time to a new destination. ``destination`` may be any of the types supported by ``travel``. ``tick`` may be set to a boolean, to change the ``tick`` flag of ``travel``. For example: .. code-block:: python import datetime as dt import time import time_machine with time_machine.travel(0, tick=False) as traveller: assert time.time() == 0 traveller.move_to(234) assert time.time() == 234 ``shift(delta)`` ^^^^^^^^^^^^^^^^ ``shift()`` takes one argument, ``delta``, which moves the current time by the given offset. ``delta`` may be a ``timedelta`` or a number of seconds, which will be added to destination. It may be negative, in which case time will move to an earlier point. For example: .. code-block:: python import datetime as dt import time import time_machine with time_machine.travel(0, tick=False) as traveller: assert time.time() == 0 traveller.shift(dt.timedelta(seconds=100)) assert time.time() == 100 traveller.shift(-dt.timedelta(seconds=10)) assert time.time() == 90 pytest plugin ------------- time-machine also works as a pytest plugin. It provides a function-scoped fixture called ``time_machine`` with methods ``move_to()`` and ``shift()``, which have the same signature as their equivalents in ``Coordinates``. 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. For example: .. code-block:: python import datetime as dt def test_delorean(time_machine): time_machine.move_to(dt.datetime(1985, 10, 26)) assert dt.date.today().isoformat() == "1985-10-26" time_machine.move_to(dt.datetime(2015, 10, 21)) assert dt.date.today().isoformat() == "2015-10-21" time_machine.shift(dt.timedelta(days=1)) assert dt.date.today().isoformat() == "2015-10-22" If you are using pytest test classes, you can apply the fixture to all test methods in a class by adding an autouse fixture: .. code-block:: python import time import pytest class TestSomething: @pytest.fixture(autouse=True) def set_time(self, time_machine): time_machine.move_to(1000.0) def test_one(self): assert int(time.time()) == 1000.0 def test_two(self, time_machine): assert int(time.time()) == 1000.0 time_machine.move_to(2000.0) assert int(time.time()) == 2000.0 ``escape_hatch`` ---------------- The ``escape_hatch`` object provides functions to bypass time-machine. These allow you to call the real datetime functions, without any mocking. It also provides a way to check if time-machine is currently time travelling. These capabilities are useful in rare circumstances. For example, if you need to authenticate with an external service during time travel, you may need the real value of ``datetime.now()``. The functions are: * ``escape_hatch.is_travelling() -> bool`` - returns ``True`` if ``time_machine.travel()`` is active, ``False`` otherwise. * ``escape_hatch.datetime.datetime.now()`` - wraps the real ``datetime.datetime.now()``. * ``escape_hatch.datetime.datetime.utcnow()`` - wraps the real ``datetime.datetime.utcnow()``. * ``escape_hatch.time.clock_gettime()`` - wraps the real ``time.clock_gettime()``. * ``escape_hatch.time.clock_gettime_ns()`` - wraps the real ``time.clock_gettime_ns()``. * ``escape_hatch.time.gmtime()`` - wraps the real ``time.gmtime()``. * ``escape_hatch.time.localtime()`` - wraps the real ``time.localtime()``. * ``escape_hatch.time.strftime()`` - wraps the real ``time.strftime()``. * ``escape_hatch.time.time()`` - wraps the real ``time.time()``. * ``escape_hatch.time.time_ns()`` - wraps the real ``time.time_ns()``. For example: .. code-block:: python import time_machine with time_machine.travel(...): if time_machine.escape_hatch.is_travelling(): print("We need to go back to the future!") real_now = time_machine.escape_hatch.datetime.datetime.now() external_authenticate(now=real_now) Caveats ======= Time is a global state. Any concurrent threads or asynchronous functions are also be affected. Some aren't ready for time to move so rapidly or backwards, and may crash or produce unexpected results. Also beware that other processes are not affected. For example, if you use SQL datetime functions on a database server, they will return the real time. Comparison ========== There are some prior libraries that try to achieve the same thing. They have their own strengths and weaknesses. Here's a quick comparison. unittest.mock ------------- The standard library's `unittest.mock `__ can be used to target imports of ``datetime`` and ``time`` to change the returned value for current time. Unfortunately, this is fragile as it only affects the import location the mock targets. Therefore, if you have several modules in a call tree requesting the date/time, you need several mocks. This is a general problem with unittest.mock - see `Why Your Mock Doesn't Work `__. It's also impossible to mock certain references, such as function default arguments: .. code-block:: python def update_books(_now=time.time): # set as default argument so faster lookup for book in books: ... Although such references are rare, they are occasionally used to optimize highly repeated loops. freezegun --------- Steve Pulec's `freezegun `__ library is a popular solution. It provides a clear API which was much of the inspiration for time-machine. The main drawback is its slow implementation. It essentially does a find-and-replace mock of all the places that the ``datetime`` and ``time`` modules have been imported. This gets around the problems with using unittest.mock, but it means the time it takes to do the mocking is proportional to the number of loaded modules. In large projects, this can take several seconds, an impractical overhead for an individual test. It's also not a perfect search, since it searches only module-level imports. Such imports are definitely the most common way projects use date and time functions, but they're not the only way. freezegun won’t find functions that have been “hidden” inside arbitrary objects, such as class-level attributes. It also can't affect C extensions that call the standard library functions, including (I believe) Cython-ized Python code. python-libfaketime ------------------ Simon Weber's `python-libfaketime `__ wraps the `libfaketime `__ library. libfaketime replaces all the C-level system calls for the current time with its own wrappers. It's therefore a "perfect" mock for the current process, affecting every single point the current time might be fetched, and performs much faster than freezegun. Unfortunately python-libfaketime comes with the limitations of ``LD_PRELOAD``. This is a mechanism to replace system libraries for a program as it loads (`explanation `__). This causes two issues in particular when you use python-libfaketime. First, ``LD_PRELOAD`` is only available on Unix platforms, which prevents you from using it on Windows. Second, you have to help manage ``LD_PRELOAD``. You either use python-libfaketime's ``reexec_if_needed()`` function, which restarts (*re-execs*) your test process while loading, or manually manage the ``LD_PRELOAD`` environment variable. Neither is ideal. Re-execing breaks anything that might wrap your test process, such as profilers, debuggers, and IDE test runners. Manually managing the environment variable is a bit of overhead, and must be done for each environment you run your tests in, including each developer's machine. time-machine ------------ time-machine is intended to combine the advantages of freezegun and libfaketime. It works without ``LD_PRELOAD`` but still mocks the standard library functions everywhere they may be referenced. Its weak point is that other libraries using date/time system calls won't be mocked. Thankfully this is rare. It's also possible such python libraries can be added to the set mocked by time-machine. One drawback is that it only works with CPython, so can't be used with other Python interpreters like PyPy. However it may possible to extend it to support other interpreters through different mocking mechanisms. Migrating from libfaketime or freezegun ======================================= freezegun has a useful API, and python-libfaketime copies some of it, with a different function name. time-machine also copies some of freezegun's API, in ``travel()``\'s ``destination``, and ``tick`` arguments, and the ``shift()`` method. There are a few differences: * time-machine's ``tick`` argument defaults to ``True``, because code tends to make the (reasonable) assumption that time progresses whilst running, and should normally be tested as such. Testing with time frozen can make it easy to write complete assertions, but it's quite artificial. Write assertions against time ranges, rather than against exact values. * freezegun interprets dates and naive datetimes in the local time zone (including those parsed from strings with ``dateutil``). This means tests can pass when run in one time zone and fail in another. time-machine instead interprets dates and naive datetimes in UTC so they are fixed points in time. Provide time zones where required. * freezegun's ``tick()`` method has been implemented as ``shift()``, to avoid confusion with the ``tick`` argument. It also requires an explicit delta rather than defaulting to 1 second. * freezegun's ``tz_offset`` argument is not supported, since it only partially mocks the current time zone. Time zones are more complicated than a single offset from UTC, and freezegun only uses the offset in ``time.localtime()``. Instead, time-machine will mock the current time zone if you give it a ``datetime`` with a ``ZoneInfo`` timezone. Some features aren't supported like the ``auto_tick_seconds`` argument. These may be added in a future release. If you are only fairly simple function calls, you should be able to migrate by replacing calls to ``freezegun.freeze_time()`` and ``libfaketime.fake_time()`` with ``time_machine.travel()``. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396560.0 time_machine-2.16.0/pyproject.toml0000644000175100001660000000373214701236420016545 0ustar00runnerdocker[build-system] build-backend = "setuptools.build_meta" requires = [ "setuptools", ] [project] name = "time-machine" version = "2.16.0" description = "Travel through time in your tests." readme = "README.rst" keywords = [ "date", "datetime", "mock", "test", "testing", "tests", "time", "warp", ] authors = [ { name = "Adam Johnson", email = "me@adamj.eu" }, ] requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Pytest", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Typing :: Typed", ] dependencies = [ "python-dateutil", ] urls.Changelog = "https://github.com/adamchainz/time-machine/blob/main/CHANGELOG.rst" urls.Funding = "https://adamj.eu/books/" urls.Repository = "https://github.com/adamchainz/time-machine" entry-points.pytest11.time_machine = "time_machine" [tool.isort] add_imports = [ "from __future__ import annotations", ] force_single_line = true profile = "black" [tool.pyproject-fmt] max_supported_python = "3.13" [tool.pytest.ini_options] addopts = """\ --strict-config --strict-markers """ xfail_strict = true [tool.coverage.run] branch = true parallel = true source = [ "src/_time_machine.c", "tests", ] [tool.coverage.paths] source = [ "src", ".tox/**/site-packages", ] [tool.coverage.report] show_missing = true [tool.mypy] enable_error_code = [ "ignore-without-code", "redundant-expr", "truthy-bool", ] mypy_path = "src/" namespace_packages = false strict = true warn_unreachable = true [[tool.mypy.overrides]] module = "tests.*" allow_untyped_defs = true [tool.rstcheck] report_level = "ERROR" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1728396564.596161 time_machine-2.16.0/setup.cfg0000644000175100001660000000004614701236425015452 0ustar00runnerdocker[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396560.0 time_machine-2.16.0/setup.py0000644000175100001660000000061314701236420015336 0ustar00runnerdockerfrom __future__ import annotations import sys from setuptools import Extension from setuptools import setup if hasattr(sys, "pypy_version_info"): raise RuntimeError( "PyPy is not currently supported by time-machine, see " "https://github.com/adamchainz/time-machine/issues/305" ) setup(ext_modules=[Extension(name="_time_machine", sources=["src/_time_machine.c"])]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728396564.5931609 time_machine-2.16.0/src/0000755000175100001660000000000014701236425014420 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396560.0 time_machine-2.16.0/src/_time_machine.c0000644000175100001660000004112114701236420017337 0ustar00runnerdocker#include "Python.h" #include #include // Module state typedef struct { #if PY_VERSION_HEX >= 0x030d00a4 PyCFunctionFastWithKeywords original_now; #else _PyCFunctionFastWithKeywords original_now; #endif PyCFunction original_utcnow; PyCFunction original_clock_gettime; PyCFunction original_clock_gettime_ns; PyCFunction original_gmtime; PyCFunction original_localtime; PyCFunction original_monotonic; PyCFunction original_monotonic_ns; PyCFunction original_strftime; PyCFunction original_time; PyCFunction original_time_ns; } _time_machine_state; static inline _time_machine_state* get_time_machine_state(PyObject *module) { void *state = PyModule_GetState(module); assert(state != NULL); return (_time_machine_state *)state; } /* datetime.datetime.now() */ static PyObject* _time_machine_now(PyTypeObject *type, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { PyObject *result = NULL; PyObject *time_machine_module = PyImport_ImportModule("time_machine"); PyObject *time_machine_now = PyObject_GetAttrString(time_machine_module, "now"); result = _PyObject_Vectorcall(time_machine_now, args, nargs, kwnames); Py_DECREF(time_machine_now); Py_DECREF(time_machine_module); return result; } static PyObject* _time_machine_original_now(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) { _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_now(datetime_class, args, nargs, kwnames); Py_DECREF(datetime_class); Py_DECREF(datetime_module); return result; } PyDoc_STRVAR(original_now_doc, "original_now() -> datetime\n\ \n\ Call datetime.datetime.now() after patching."); /* datetime.datetime.utcnow() */ static PyObject* _time_machine_utcnow(PyObject *cls, PyObject *args) { PyObject *time_machine_module = PyImport_ImportModule("time_machine"); PyObject *time_machine_utcnow = PyObject_GetAttrString(time_machine_module, "utcnow"); PyObject* result = PyObject_CallObject(time_machine_utcnow, args); Py_DECREF(time_machine_utcnow); Py_DECREF(time_machine_module); 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* _time_machine_clock_gettime(PyObject *self, PyObject *args) { PyObject *time_machine_module = PyImport_ImportModule("time_machine"); PyObject *time_machine_clock_gettime = PyObject_GetAttrString(time_machine_module, "clock_gettime"); #if PY_VERSION_HEX >= 0x030d00a2 PyObject* result = PyObject_CallOneArg(time_machine_clock_gettime, args); #else PyObject* result = PyObject_CallObject(time_machine_clock_gettime, args); #endif Py_DECREF(time_machine_clock_gettime); Py_DECREF(time_machine_module); return result; } static PyObject* _time_machine_original_clock_gettime(PyObject *module, PyObject *args) { _time_machine_state *state = get_time_machine_state(module); PyObject *time_module = PyImport_ImportModule("time"); PyObject* result = state->original_clock_gettime(time_module, args); Py_DECREF(time_module); return result; } PyDoc_STRVAR(original_clock_gettime_doc, "original_clock_gettime() -> floating point number\n\ \n\ Call time.clock_gettime() after patching."); /* time.clock_gettime_ns() */ static PyObject* _time_machine_clock_gettime_ns(PyObject *self, PyObject *args) { PyObject *time_machine_module = PyImport_ImportModule("time_machine"); PyObject *time_machine_clock_gettime_ns = PyObject_GetAttrString(time_machine_module, "clock_gettime_ns"); #if PY_VERSION_HEX >= 0x030d00a2 PyObject* result = PyObject_CallOneArg(time_machine_clock_gettime_ns, args); #else PyObject* result = PyObject_CallObject(time_machine_clock_gettime_ns, args); #endif Py_DECREF(time_machine_clock_gettime_ns); Py_DECREF(time_machine_module); return result; } static PyObject* _time_machine_original_clock_gettime_ns(PyObject *module, PyObject *args) { _time_machine_state *state = get_time_machine_state(module); PyObject *time_module = PyImport_ImportModule("time"); PyObject* result = state->original_clock_gettime_ns(time_module, args); Py_DECREF(time_module); return result; } PyDoc_STRVAR(original_clock_gettime_ns_doc, "original_clock_gettime_ns() -> int\n\ \n\ Call time.clock_gettime_ns() after patching."); /* time.gmtime() */ static PyObject* _time_machine_gmtime(PyObject *self, PyObject *args) { PyObject *time_machine_module = PyImport_ImportModule("time_machine"); PyObject *time_machine_gmtime = PyObject_GetAttrString(time_machine_module, "gmtime"); PyObject* result = PyObject_CallObject(time_machine_gmtime, args); Py_DECREF(time_machine_gmtime); Py_DECREF(time_machine_module); return result; } static PyObject* _time_machine_original_gmtime(PyObject *module, PyObject *args) { _time_machine_state *state = get_time_machine_state(module); PyObject *time_module = PyImport_ImportModule("time"); PyObject* result = state->original_gmtime(time_module, args); Py_DECREF(time_module); return result; } PyDoc_STRVAR(original_gmtime_doc, "original_gmtime() -> floating point number\n\ \n\ Call time.gmtime() after patching."); /* time.localtime() */ static PyObject* _time_machine_localtime(PyObject *self, PyObject *args) { PyObject *time_machine_module = PyImport_ImportModule("time_machine"); PyObject *time_machine_localtime = PyObject_GetAttrString(time_machine_module, "localtime"); PyObject* result = PyObject_CallObject(time_machine_localtime, args); Py_DECREF(time_machine_localtime); Py_DECREF(time_machine_module); return result; } static PyObject* _time_machine_original_localtime(PyObject *module, PyObject *args) { _time_machine_state *state = get_time_machine_state(module); PyObject *time_module = PyImport_ImportModule("time"); PyObject* result = state->original_localtime(time_module, args); Py_DECREF(time_module); return result; } PyDoc_STRVAR(original_localtime_doc, "original_localtime() -> floating point number\n\ \n\ Call time.localtime() after patching."); /* time.monotonic() */ static PyObject* _time_machine_original_monotonic(PyObject* module, PyObject* args) { _time_machine_state *state = get_time_machine_state(module); PyObject *time_module = PyImport_ImportModule("time"); PyObject* result = state->original_monotonic(time_module, args); Py_DECREF(time_module); return result; } PyDoc_STRVAR(original_monotonic_doc, "original_monotonic() -> floating point number\n\ \n\ Call time.monotonic() after patching."); /* time.monotonic_ns() */ static PyObject* _time_machine_original_monotonic_ns(PyObject* module, PyObject* args) { _time_machine_state *state = get_time_machine_state(module); PyObject *time_module = PyImport_ImportModule("time"); PyObject* result = state->original_monotonic_ns(time_module, args); Py_DECREF(time_module); return result; } PyDoc_STRVAR(original_monotonic_ns_doc, "original_monotonic_ns() -> int\n\ \n\ Call time.monotonic_ns() after patching."); /* time.strftime() */ static PyObject* _time_machine_strftime(PyObject *self, PyObject *args) { PyObject *time_machine_module = PyImport_ImportModule("time_machine"); PyObject *time_machine_strftime = PyObject_GetAttrString(time_machine_module, "strftime"); PyObject* result = PyObject_CallObject(time_machine_strftime, args); Py_DECREF(time_machine_strftime); Py_DECREF(time_machine_module); return result; } static PyObject* _time_machine_original_strftime(PyObject *module, PyObject *args) { _time_machine_state *state = get_time_machine_state(module); PyObject *time_module = PyImport_ImportModule("time"); PyObject* result = state->original_strftime(time_module, args); Py_DECREF(time_module); return result; } PyDoc_STRVAR(original_strftime_doc, "original_strftime() -> floating point number\n\ \n\ Call time.strftime() after patching."); /* time.time() */ static PyObject* _time_machine_time(PyObject *self, PyObject *args) { PyObject *time_machine_module = PyImport_ImportModule("time_machine"); PyObject *time_machine_time = PyObject_GetAttrString(time_machine_module, "time"); PyObject* result = PyObject_CallObject(time_machine_time, args); Py_DECREF(time_machine_time); Py_DECREF(time_machine_module); return result; } static PyObject* _time_machine_original_time(PyObject *module, PyObject *args) { _time_machine_state *state = get_time_machine_state(module); PyObject *time_module = PyImport_ImportModule("time"); PyObject* result = state->original_time(time_module, args); Py_DECREF(time_module); return result; } PyDoc_STRVAR(original_time_doc, "original_time() -> floating point number\n\ \n\ Call time.time() after patching."); /* time.time_ns() */ static PyObject* _time_machine_time_ns(PyObject *self, PyObject *args) { PyObject *time_machine_module = PyImport_ImportModule("time_machine"); PyObject *time_machine_time_ns = PyObject_GetAttrString(time_machine_module, "time_ns"); PyObject* result = PyObject_CallObject(time_machine_time_ns, args); Py_DECREF(time_machine_time_ns); Py_DECREF(time_machine_module); return result; } static PyObject* _time_machine_original_time_ns(PyObject *module, PyObject *args) { _time_machine_state *state = get_time_machine_state(module); PyObject *time_module = PyImport_ImportModule("time"); PyObject* result = state->original_time_ns(time_module, args); Py_DECREF(time_module); return result; } PyDoc_STRVAR(original_time_ns_doc, "original_time_ns() -> int\n\ \n\ Call time.time_ns() after patching."); static PyObject* _time_machine_patch_if_needed(PyObject *module, PyObject *unused) { _time_machine_state *state = PyModule_GetState(module); if (state == NULL) { return NULL; } if (state->original_time) Py_RETURN_NONE; PyObject *datetime_module = PyImport_ImportModule("datetime"); PyObject *datetime_class = PyObject_GetAttrString(datetime_module, "datetime"); PyCFunctionObject *datetime_datetime_now = (PyCFunctionObject *) PyObject_GetAttrString(datetime_class, "now"); #if PY_VERSION_HEX >= 0x030d00a4 state->original_now = (PyCFunctionFastWithKeywords) datetime_datetime_now->m_ml->ml_meth; #else state->original_now = (_PyCFunctionFastWithKeywords) datetime_datetime_now->m_ml->ml_meth; #endif datetime_datetime_now->m_ml->ml_meth = (PyCFunction) _time_machine_now; 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); Py_DECREF(datetime_class); Py_DECREF(datetime_module); PyObject *time_module = PyImport_ImportModule("time"); /* time.clock_gettime(), only available on Unix platforms. */ PyCFunctionObject *time_clock_gettime = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "clock_gettime"); if (time_clock_gettime == NULL) { PyErr_Clear(); } else { state->original_clock_gettime = time_clock_gettime->m_ml->ml_meth; time_clock_gettime->m_ml->ml_meth = _time_machine_clock_gettime; Py_DECREF(time_clock_gettime); } /* time.clock_gettime_ns(), only available on Unix platforms. */ PyCFunctionObject *time_clock_gettime_ns = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "clock_gettime_ns"); if (time_clock_gettime_ns == NULL) { PyErr_Clear(); } else { state->original_clock_gettime_ns = time_clock_gettime_ns->m_ml->ml_meth; time_clock_gettime_ns->m_ml->ml_meth = _time_machine_clock_gettime_ns; Py_DECREF(time_clock_gettime_ns); } PyCFunctionObject *time_gmtime = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "gmtime"); state->original_gmtime = time_gmtime->m_ml->ml_meth; time_gmtime->m_ml->ml_meth = _time_machine_gmtime; Py_DECREF(time_gmtime); PyCFunctionObject *time_localtime = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "localtime"); state->original_localtime = time_localtime->m_ml->ml_meth; time_localtime->m_ml->ml_meth = _time_machine_localtime; Py_DECREF(time_localtime); PyCFunctionObject *time_monotonic = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "monotonic"); state->original_monotonic = time_monotonic->m_ml->ml_meth; time_monotonic->m_ml->ml_meth = _time_machine_time; Py_DECREF(time_monotonic); PyCFunctionObject *time_monotonic_ns = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "monotonic_ns"); state->original_monotonic_ns = time_monotonic_ns->m_ml->ml_meth; time_monotonic_ns->m_ml->ml_meth = _time_machine_time_ns; Py_DECREF(time_monotonic_ns); PyCFunctionObject *time_strftime = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "strftime"); state->original_strftime = time_strftime->m_ml->ml_meth; time_strftime->m_ml->ml_meth = _time_machine_strftime; Py_DECREF(time_strftime); PyCFunctionObject *time_time = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "time"); state->original_time = time_time->m_ml->ml_meth; time_time->m_ml->ml_meth = _time_machine_time; Py_DECREF(time_time); PyCFunctionObject *time_time_ns = (PyCFunctionObject *) PyObject_GetAttrString(time_module, "time_ns"); state->original_time_ns = time_time_ns->m_ml->ml_meth; time_time_ns->m_ml->ml_meth = _time_machine_time_ns; Py_DECREF(time_time_ns); Py_DECREF(time_module); Py_RETURN_NONE; } PyDoc_STRVAR(patch_if_needed_doc, "patch_if_needed() -> None\n\ \n\ Swap in helpers."); 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}, #else {"original_clock_gettime", (PyCFunction)_time_machine_original_clock_gettime, METH_VARARGS, original_clock_gettime_doc}, {"original_clock_gettime_ns", (PyCFunction)_time_machine_original_clock_gettime_ns, METH_VARARGS, original_clock_gettime_ns_doc}, #endif {"original_gmtime", (PyCFunction)_time_machine_original_gmtime, METH_VARARGS, original_gmtime_doc}, {"original_localtime", (PyCFunction)_time_machine_original_localtime, METH_VARARGS, original_localtime_doc}, {"original_monotonic", (PyCFunction)_time_machine_original_monotonic, METH_NOARGS, original_monotonic_doc}, {"original_monotonic_ns", (PyCFunction)_time_machine_original_monotonic_ns, METH_NOARGS, original_monotonic_ns_doc}, {"original_strftime", (PyCFunction)_time_machine_original_strftime, METH_VARARGS, original_strftime_doc}, {"original_time", (PyCFunction)_time_machine_original_time, METH_NOARGS, original_time_doc}, {"original_time_ns", (PyCFunction)_time_machine_original_time_ns, METH_NOARGS, original_time_ns_doc}, {"patch_if_needed", (PyCFunction)_time_machine_patch_if_needed, METH_NOARGS, patch_if_needed_doc}, {NULL, NULL} /* sentinel */ }; static PyModuleDef_Slot _time_machine_slots[] = { {0, NULL} }; static struct PyModuleDef _time_machine_module = { PyModuleDef_HEAD_INIT, .m_name = "_time_machine", .m_doc = module_doc, .m_size = sizeof(_time_machine_state), .m_methods = module_functions, .m_slots = _time_machine_slots, .m_traverse = NULL, .m_clear = NULL, .m_free = NULL }; PyMODINIT_FUNC PyInit__time_machine(void) { return PyModuleDef_Init(&_time_machine_module); } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728396564.5941608 time_machine-2.16.0/src/time_machine/0000755000175100001660000000000014701236425017042 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396560.0 time_machine-2.16.0/src/time_machine/__init__.py0000644000175100001660000003604214701236420021153 0ustar00runnerdockerfrom __future__ import annotations import datetime as dt import functools import inspect import os import sys import time as time_module import uuid from collections.abc import Awaitable from collections.abc import Generator from collections.abc import Generator as TypingGenerator from time import gmtime as orig_gmtime from time import struct_time from types import TracebackType from typing import Any from typing import Callable from typing import TypeVar from typing import Union from typing import cast from typing import overload from unittest import TestCase from unittest import mock from zoneinfo import ZoneInfo import _time_machine from dateutil.parser import parse as parse_datetime # time.clock_gettime and time.CLOCK_REALTIME not always available # e.g. on builds against old macOS = official Python.org installer try: from time import CLOCK_REALTIME except ImportError: # Dummy value that won't compare equal to any value CLOCK_REALTIME = sys.maxsize try: from time import tzset HAVE_TZSET = True except ImportError: # pragma: no cover # Windows HAVE_TZSET = False try: import pytest except ImportError: # pragma: no cover HAVE_PYTEST = False else: HAVE_PYTEST = True NANOSECONDS_PER_SECOND = 1_000_000_000 # Windows' time epoch is not unix epoch but in 1601. This constant helps us # translate to it. _system_epoch = orig_gmtime(0) SYSTEM_EPOCH_TIMESTAMP_NS = int( dt.datetime( _system_epoch.tm_year, _system_epoch.tm_mon, _system_epoch.tm_mday, _system_epoch.tm_hour, _system_epoch.tm_min, _system_epoch.tm_sec, tzinfo=dt.timezone.utc, ).timestamp() * NANOSECONDS_PER_SECOND ) DestinationBaseType = Union[ int, float, dt.datetime, dt.timedelta, dt.date, str, ] DestinationType = Union[ DestinationBaseType, Callable[[], DestinationBaseType], TypingGenerator[DestinationBaseType, None, None], ] _F = TypeVar("_F", bound=Callable[..., Any]) _AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) TestCaseType = TypeVar("TestCaseType", bound=type[TestCase]) # copied from typeshed: _TimeTuple = tuple[int, int, int, int, int, int, int, int, int] def extract_timestamp_tzname( destination: DestinationType, ) -> tuple[float, str | None]: dest: DestinationBaseType if isinstance(destination, Generator): dest = next(destination) elif callable(destination): dest = destination() else: dest = destination timestamp: float tzname: str | None = None if isinstance(dest, int): timestamp = float(dest) elif isinstance(dest, float): timestamp = dest elif isinstance(dest, dt.datetime): if isinstance(dest.tzinfo, ZoneInfo): tzname = dest.tzinfo.key if dest.tzinfo is None: dest = dest.replace(tzinfo=dt.timezone.utc) timestamp = dest.timestamp() elif isinstance(dest, dt.timedelta): timestamp = time_module.time() + dest.total_seconds() elif isinstance(dest, dt.date): timestamp = dt.datetime.combine( dest, dt.time(0, 0), tzinfo=dt.timezone.utc ).timestamp() elif isinstance(dest, str): timestamp = parse_datetime(dest).timestamp() else: raise TypeError(f"Unsupported destination {dest!r}") return timestamp, tzname class Coordinates: def __init__( self, destination_timestamp: float, destination_tzname: str | None, tick: bool, ) -> None: self._destination_timestamp_ns = int( destination_timestamp * NANOSECONDS_PER_SECOND ) self._destination_tzname = destination_tzname self._tick = tick self._requested = False def time(self) -> float: return self.time_ns() / NANOSECONDS_PER_SECOND def time_ns(self) -> int: if not self._tick: return self._destination_timestamp_ns base = SYSTEM_EPOCH_TIMESTAMP_NS + self._destination_timestamp_ns now_ns: int = _time_machine.original_time_ns() if not self._requested: self._requested = True self._real_start_timestamp_ns = now_ns return base return base + (now_ns - self._real_start_timestamp_ns) def shift(self, delta: dt.timedelta | int | float) -> None: if isinstance(delta, dt.timedelta): total_seconds = delta.total_seconds() elif isinstance(delta, (int, float)): total_seconds = delta else: raise TypeError(f"Unsupported type for delta argument: {delta!r}") self._destination_timestamp_ns += int(total_seconds * NANOSECONDS_PER_SECOND) def move_to( self, destination: DestinationType, tick: bool | None = None, ) -> None: self._stop() timestamp, self._destination_tzname = extract_timestamp_tzname(destination) self._destination_timestamp_ns = int(timestamp * NANOSECONDS_PER_SECOND) self._requested = False self._start() if tick is not None: self._tick = tick def _start(self) -> None: if HAVE_TZSET and self._destination_tzname is not None: self._orig_tz = os.environ.get("TZ") os.environ["TZ"] = self._destination_tzname tzset() def _stop(self) -> None: if HAVE_TZSET and self._destination_tzname is not None: if self._orig_tz is None: del os.environ["TZ"] else: os.environ["TZ"] = self._orig_tz tzset() coordinates_stack: list[Coordinates] = [] # During time travel, patch the uuid module's time-based generation function to # None, which makes it use time.time(). Otherwise it makes a system call to # find the current datetime. The time it finds is stored in generated UUID1 # values. uuid_generate_time_attr = "_generate_time_safe" uuid_generate_time_patcher = mock.patch.object(uuid, uuid_generate_time_attr, new=None) uuid_uuid_create_patcher = mock.patch.object(uuid, "_UuidCreate", new=None) class travel: def __init__(self, destination: DestinationType, *, tick: bool = True) -> None: self.destination_timestamp, self.destination_tzname = extract_timestamp_tzname( destination ) self.tick = tick def start(self) -> Coordinates: global coordinates_stack _time_machine.patch_if_needed() if not coordinates_stack: uuid_generate_time_patcher.start() uuid_uuid_create_patcher.start() coordinates = Coordinates( destination_timestamp=self.destination_timestamp, destination_tzname=self.destination_tzname, tick=self.tick, ) coordinates_stack.append(coordinates) coordinates._start() return coordinates def stop(self) -> None: global coordinates_stack coordinates_stack.pop()._stop() if not coordinates_stack: uuid_generate_time_patcher.stop() uuid_uuid_create_patcher.stop() def __enter__(self) -> Coordinates: return self.start() def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.stop() @overload def __call__(self, wrapped: TestCaseType) -> TestCaseType: # pragma: no cover ... @overload def __call__(self, wrapped: _AF) -> _AF: # pragma: no cover ... @overload def __call__(self, wrapped: _F) -> _F: # pragma: no cover ... # 'Any' below is workaround for Mypy error: # Overloaded function implementation does not accept all possible arguments # of signature def __call__( self, wrapped: TestCaseType | _AF | _F | Any ) -> TestCaseType | _AF | _F | Any: if isinstance(wrapped, type): # Class decorator if not issubclass(wrapped, TestCase): raise TypeError("Can only decorate unittest.TestCase subclasses.") # Modify the setUpClass method orig_setUpClass = wrapped.setUpClass.__func__ # type: ignore[attr-defined] @functools.wraps(orig_setUpClass) def setUpClass(cls: type[TestCase]) -> None: self.__enter__() try: orig_setUpClass(cls) except Exception: self.__exit__(*sys.exc_info()) raise wrapped.setUpClass = classmethod(setUpClass) # type: ignore[assignment] orig_tearDownClass = ( wrapped.tearDownClass.__func__ # type: ignore[attr-defined] ) @functools.wraps(orig_tearDownClass) def tearDownClass(cls: type[TestCase]) -> None: orig_tearDownClass(cls) self.__exit__(None, None, None) wrapped.tearDownClass = classmethod( # type: ignore[assignment] tearDownClass ) return cast(TestCaseType, wrapped) elif inspect.iscoroutinefunction(wrapped): @functools.wraps(wrapped) async def wrapper(*args: Any, **kwargs: Any) -> Any: with self: return await wrapped(*args, **kwargs) return cast(_AF, wrapper) else: assert callable(wrapped) @functools.wraps(wrapped) def wrapper(*args: Any, **kwargs: Any) -> Any: with self: return wrapped(*args, **kwargs) return cast(_F, wrapper) # datetime module def now(tz: dt.tzinfo | None = None) -> dt.datetime: if not coordinates_stack: result: dt.datetime = _time_machine.original_now(tz) return result return dt.datetime.fromtimestamp(time(), tz) 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) # time module def clock_gettime(clk_id: int) -> float: if not coordinates_stack or clk_id != CLOCK_REALTIME: result: float = _time_machine.original_clock_gettime(clk_id) return result return time() def clock_gettime_ns(clk_id: int) -> int: if not coordinates_stack or clk_id != CLOCK_REALTIME: result: int = _time_machine.original_clock_gettime_ns(clk_id) return result return time_ns() def gmtime(secs: float | None = None) -> struct_time: result: struct_time if not coordinates_stack or secs is not None: result = _time_machine.original_gmtime(secs) else: result = _time_machine.original_gmtime(coordinates_stack[-1].time()) return result def localtime(secs: float | None = None) -> struct_time: result: struct_time if not coordinates_stack or secs is not None: result = _time_machine.original_localtime(secs) else: result = _time_machine.original_localtime(coordinates_stack[-1].time()) return result def strftime(format: str, t: _TimeTuple | struct_time | None = None) -> str: result: str if t is not None: result = _time_machine.original_strftime(format, t) elif not coordinates_stack: result = _time_machine.original_strftime(format) else: result = _time_machine.original_strftime(format, localtime()) return result def time() -> float: if not coordinates_stack: result: float = _time_machine.original_time() return result return coordinates_stack[-1].time() def time_ns() -> int: if not coordinates_stack: result: int = _time_machine.original_time_ns() return result return coordinates_stack[-1].time_ns() # pytest plugin if HAVE_PYTEST: # pragma: no branch class TimeMachineFixture: traveller: travel | None coordinates: Coordinates | None def __init__(self) -> None: self.traveller = None self.coordinates = None def move_to( self, destination: DestinationType, tick: bool | None = None, ) -> None: if self.traveller is None: if tick is None: tick = True self.traveller = travel(destination, tick=tick) self.coordinates = self.traveller.start() else: assert self.coordinates is not None self.coordinates.move_to(destination, tick=tick) def shift(self, delta: dt.timedelta | int | float) -> None: if self.traveller is None: raise RuntimeError( "Initialize time_machine with move_to() before using shift()." ) assert self.coordinates is not None self.coordinates.shift(delta=delta) def stop(self) -> None: if self.traveller is not None: self.traveller.stop() @pytest.fixture(name="time_machine") def time_machine_fixture() -> TypingGenerator[TimeMachineFixture, None, None]: fixture = TimeMachineFixture() yield fixture fixture.stop() # escape hatch class _EscapeHatchDatetimeDatetime: def now(self, tz: dt.tzinfo | None = None) -> dt.datetime: result: dt.datetime = _time_machine.original_now(tz) return result def utcnow(self) -> dt.datetime: result: dt.datetime = _time_machine.original_utcnow() return result class _EscapeHatchDatetime: def __init__(self) -> None: self.datetime = _EscapeHatchDatetimeDatetime() class _EscapeHatchTime: def clock_gettime(self, clk_id: int) -> float: result: float = _time_machine.original_clock_gettime(clk_id) return result def clock_gettime_ns(self, clk_id: int) -> int: result: int = _time_machine.original_clock_gettime_ns(clk_id) return result def gmtime(self, secs: float | None = None) -> struct_time: result: struct_time = _time_machine.original_gmtime(secs) return result def localtime(self, secs: float | None = None) -> struct_time: result: struct_time = _time_machine.original_localtime(secs) return result def monotonic(self) -> float: result: float = _time_machine.original_monotonic() return result def monotonic_ns(self) -> int: result: int = _time_machine.original_monotonic_ns() return result def strftime(self, format: str, t: _TimeTuple | struct_time | None = None) -> str: result: str if t is not None: result = _time_machine.original_strftime(format, t) else: result = _time_machine.original_strftime(format) return result def time(self) -> float: result: float = _time_machine.original_time() return result def time_ns(self) -> int: result: int = _time_machine.original_time_ns() return result class _EscapeHatch: def __init__(self) -> None: self.datetime = _EscapeHatchDatetime() self.time = _EscapeHatchTime() def is_travelling(self) -> bool: return bool(coordinates_stack) escape_hatch = _EscapeHatch() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396560.0 time_machine-2.16.0/src/time_machine/py.typed0000644000175100001660000000000014701236420020522 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1728396564.595161 time_machine-2.16.0/src/time_machine.egg-info/0000755000175100001660000000000014701236425020534 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396564.0 time_machine-2.16.0/src/time_machine.egg-info/PKG-INFO0000644000175100001660000005104614701236424021636 0ustar00runnerdockerMetadata-Version: 2.1 Name: time-machine Version: 2.16.0 Summary: Travel through time in your tests. Author-email: Adam Johnson Project-URL: Changelog, https://github.com/adamchainz/time-machine/blob/main/CHANGELOG.rst Project-URL: Funding, https://adamj.eu/books/ Project-URL: Repository, https://github.com/adamchainz/time-machine Keywords: date,datetime,mock,test,testing,tests,time,warp Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Typing :: Typed Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: python-dateutil ============ time-machine ============ .. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/time-machine/main.yml.svg?branch=main&style=for-the-badge :target: https://github.com/adamchainz/time-machine/actions?workflow=CI .. image:: https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge :target: https://github.com/adamchainz/time-machine/actions?workflow=CI .. image:: https://img.shields.io/pypi/v/time-machine.svg?style=for-the-badge :target: https://pypi.org/project/time-machine/ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge :target: https://github.com/psf/black .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge :target: https://github.com/pre-commit/pre-commit :alt: pre-commit Travel through time in your tests. A quick example: .. code-block:: python import datetime as dt from zoneinfo import ZoneInfo import time_machine hill_valley_tz = ZoneInfo("America/Los_Angeles") @time_machine.travel(dt.datetime(1985, 10, 26, 1, 24, tzinfo=hill_valley_tz)) def test_delorean(): assert dt.date.today().isoformat() == "1985-10-26" For a bit of background, see `the introductory blog post `__ and `the benchmark blog post `__. ---- **Testing a Django project?** Check out my book `Speed Up Your Django Tests `__ which covers loads of ways to write faster, more accurate tests. I created time-machine whilst writing the book. ---- Installation ============ Use **pip**: .. code-block:: sh python -m pip install time-machine Python 3.9 to 3.13 supported. Only CPython is supported at this time because time-machine directly hooks into the C-level API. Usage ===== If you’re coming from freezegun or libfaketime, see also the below section on migrating. ``travel(destination, *, tick=True)`` ------------------------------------- ``travel()`` is a class that allows time travel, to the datetime specified by ``destination``. It does so by mocking all functions from Python's standard library that return the current date or datetime. It can be used independently, as a function decorator, or as a context manager. ``destination`` specifies the datetime to move to. It may be: * A ``datetime.datetime``. If it is naive, it will be assumed to have the UTC timezone. If it has ``tzinfo`` set to a |zoneinfo-instance|_, the current timezone will also be mocked. * A ``datetime.date``. This will be converted to a UTC datetime with the time 00:00:00. * A ``datetime.timedelta``. This will be interpreted relative to the current time. If already within a ``travel()`` block, the ``shift()`` method is easier to use (documented below). * A ``float`` or ``int`` specifying a `Unix timestamp `__ * A string, which will be parsed with `dateutil.parse `__ and converted to a timestamp. If the result is naive, it will be assumed to be local time. .. |zoneinfo-instance| replace:: ``zoneinfo.ZoneInfo`` instance .. _zoneinfo-instance: https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo Additionally, you can provide some more complex types: * A generator, in which case ``next()`` will be called on it, with the result treated as above. * A callable, in which case it will be called with no parameters, with the result treated as above. ``tick`` defines whether time continues to "tick" after travelling, or is frozen. If ``True``, the default, successive calls to mocked functions return values increasing by the elapsed real time *since the first call.* So after starting travel to ``0.0`` (the UNIX epoch), the first call to any datetime function will return its representation of ``1970-01-01 00:00:00.000000`` exactly. The following calls "tick," so if a call was made exactly half a second later, it would return ``1970-01-01 00:00:00.500000``. Mocked Functions ^^^^^^^^^^^^^^^^ All datetime functions in the standard library are mocked to move to the destination current datetime: * ``datetime.datetime.now()`` * ``datetime.datetime.utcnow()`` * ``time.clock_gettime()`` (only for ``CLOCK_REALTIME``) * ``time.clock_gettime_ns()`` (only for ``CLOCK_REALTIME``) * ``time.gmtime()`` * ``time.localtime()`` * ``time.monotonic()`` (not a real monotonic clock, returns ``time.time()``) * ``time.monotonic_ns()`` (not a real monotonic clock, returns ``time.time_ns()``) * ``time.strftime()`` * ``time.time()`` * ``time.time_ns()`` The mocking is done at the C layer, replacing the function pointers for these built-ins. Therefore, it automatically affects everywhere those functions have been imported, unlike use of ``unittest.mock.patch()``. Usage with ``start()`` / ``stop()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To use independently, create an instance, use ``start()`` to move to the destination time, and ``stop()`` to move back. For example: .. code-block:: python import datetime as dt import time_machine traveller = time_machine.travel(dt.datetime(1985, 10, 26)) traveller.start() # It's the past! assert dt.date.today() == dt.date(1985, 10, 26) traveller.stop() # We've gone back to the future! assert dt.date.today() > dt.date(2020, 4, 29) ``travel()`` instances are nestable, but you'll need to be careful when manually managing to call their ``stop()`` methods in the correct order, even when exceptions occur. It's recommended to use the decorator or context manager forms instead, to take advantage of Python features to do this. Function Decorator ^^^^^^^^^^^^^^^^^^ When used as a function decorator, time is mocked during the wrapped function's duration: .. code-block:: python import time import time_machine @time_machine.travel("1970-01-01 00:00 +0000") def test_in_the_deep_past(): assert 0.0 < time.time() < 1.0 You can also decorate asynchronous functions (coroutines): .. code-block:: python import time import time_machine @time_machine.travel("1970-01-01 00:00 +0000") async def test_in_the_deep_past(): assert 0.0 < time.time() < 1.0 Beware: time is a *global* state - `see below <#caveats>`__. Context Manager ^^^^^^^^^^^^^^^ When used as a context manager, time is mocked during the ``with`` block: .. code-block:: python import time import time_machine def test_in_the_deep_past(): with time_machine.travel(0.0): assert 0.0 < time.time() < 1.0 Class Decorator ^^^^^^^^^^^^^^^ Only ``unittest.TestCase`` subclasses are supported. When applied as a class decorator to such classes, time is mocked from the start of ``setUpClass()`` to the end of ``tearDownClass()``: .. code-block:: python import time import time_machine import unittest @time_machine.travel(0.0) class DeepPastTests(TestCase): def test_in_the_deep_past(self): assert 0.0 < time.time() < 1.0 Note this is different to ``unittest.mock.patch()``\'s behaviour, which is to mock only during the test methods. For pytest-style test classes, see the pattern `documented below <#pytest-plugin>`__. Timezone mocking ^^^^^^^^^^^^^^^^ If the ``destination`` passed to ``time_machine.travel()`` or ``Coordinates.move_to()`` has its ``tzinfo`` set to a |zoneinfo-instance2|_, the current timezone will be mocked. This will be done by calling |time-tzset|_, so it is only available on Unix. .. |zoneinfo-instance2| replace:: ``zoneinfo.ZoneInfo`` instance .. _zoneinfo-instance2: https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo .. |time-tzset| replace:: ``time.tzset()`` .. _time-tzset: https://docs.python.org/3/library/time.html#time.tzset ``time.tzset()`` changes the ``time`` module’s `timezone constants `__ and features that rely on those, such as ``time.localtime()``. It won’t affect other concepts of “the current timezone”, such as Django’s (which can be changed with its |timezone-override|_). .. |timezone-override| replace:: ``timezone.override()`` .. _timezone-override: https://docs.djangoproject.com/en/stable/ref/utils/#django.utils.timezone.override Here’s a worked example changing the current timezone: .. code-block:: python import datetime as dt import time from zoneinfo import ZoneInfo import time_machine hill_valley_tz = ZoneInfo("America/Los_Angeles") @time_machine.travel(dt.datetime(2015, 10, 21, 16, 29, tzinfo=hill_valley_tz)) def test_hoverboard_era(): assert time.tzname == ("PST", "PDT") now = dt.datetime.now() assert (now.hour, now.minute) == (16, 29) ``Coordinates`` --------------- The ``start()`` method and entry of the context manager both return a ``Coordinates`` object that corresponds to the given "trip" in time. This has a couple methods that can be used to travel to other times. ``move_to(destination, tick=None)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``move_to()`` moves the current time to a new destination. ``destination`` may be any of the types supported by ``travel``. ``tick`` may be set to a boolean, to change the ``tick`` flag of ``travel``. For example: .. code-block:: python import datetime as dt import time import time_machine with time_machine.travel(0, tick=False) as traveller: assert time.time() == 0 traveller.move_to(234) assert time.time() == 234 ``shift(delta)`` ^^^^^^^^^^^^^^^^ ``shift()`` takes one argument, ``delta``, which moves the current time by the given offset. ``delta`` may be a ``timedelta`` or a number of seconds, which will be added to destination. It may be negative, in which case time will move to an earlier point. For example: .. code-block:: python import datetime as dt import time import time_machine with time_machine.travel(0, tick=False) as traveller: assert time.time() == 0 traveller.shift(dt.timedelta(seconds=100)) assert time.time() == 100 traveller.shift(-dt.timedelta(seconds=10)) assert time.time() == 90 pytest plugin ------------- time-machine also works as a pytest plugin. It provides a function-scoped fixture called ``time_machine`` with methods ``move_to()`` and ``shift()``, which have the same signature as their equivalents in ``Coordinates``. 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. For example: .. code-block:: python import datetime as dt def test_delorean(time_machine): time_machine.move_to(dt.datetime(1985, 10, 26)) assert dt.date.today().isoformat() == "1985-10-26" time_machine.move_to(dt.datetime(2015, 10, 21)) assert dt.date.today().isoformat() == "2015-10-21" time_machine.shift(dt.timedelta(days=1)) assert dt.date.today().isoformat() == "2015-10-22" If you are using pytest test classes, you can apply the fixture to all test methods in a class by adding an autouse fixture: .. code-block:: python import time import pytest class TestSomething: @pytest.fixture(autouse=True) def set_time(self, time_machine): time_machine.move_to(1000.0) def test_one(self): assert int(time.time()) == 1000.0 def test_two(self, time_machine): assert int(time.time()) == 1000.0 time_machine.move_to(2000.0) assert int(time.time()) == 2000.0 ``escape_hatch`` ---------------- The ``escape_hatch`` object provides functions to bypass time-machine. These allow you to call the real datetime functions, without any mocking. It also provides a way to check if time-machine is currently time travelling. These capabilities are useful in rare circumstances. For example, if you need to authenticate with an external service during time travel, you may need the real value of ``datetime.now()``. The functions are: * ``escape_hatch.is_travelling() -> bool`` - returns ``True`` if ``time_machine.travel()`` is active, ``False`` otherwise. * ``escape_hatch.datetime.datetime.now()`` - wraps the real ``datetime.datetime.now()``. * ``escape_hatch.datetime.datetime.utcnow()`` - wraps the real ``datetime.datetime.utcnow()``. * ``escape_hatch.time.clock_gettime()`` - wraps the real ``time.clock_gettime()``. * ``escape_hatch.time.clock_gettime_ns()`` - wraps the real ``time.clock_gettime_ns()``. * ``escape_hatch.time.gmtime()`` - wraps the real ``time.gmtime()``. * ``escape_hatch.time.localtime()`` - wraps the real ``time.localtime()``. * ``escape_hatch.time.strftime()`` - wraps the real ``time.strftime()``. * ``escape_hatch.time.time()`` - wraps the real ``time.time()``. * ``escape_hatch.time.time_ns()`` - wraps the real ``time.time_ns()``. For example: .. code-block:: python import time_machine with time_machine.travel(...): if time_machine.escape_hatch.is_travelling(): print("We need to go back to the future!") real_now = time_machine.escape_hatch.datetime.datetime.now() external_authenticate(now=real_now) Caveats ======= Time is a global state. Any concurrent threads or asynchronous functions are also be affected. Some aren't ready for time to move so rapidly or backwards, and may crash or produce unexpected results. Also beware that other processes are not affected. For example, if you use SQL datetime functions on a database server, they will return the real time. Comparison ========== There are some prior libraries that try to achieve the same thing. They have their own strengths and weaknesses. Here's a quick comparison. unittest.mock ------------- The standard library's `unittest.mock `__ can be used to target imports of ``datetime`` and ``time`` to change the returned value for current time. Unfortunately, this is fragile as it only affects the import location the mock targets. Therefore, if you have several modules in a call tree requesting the date/time, you need several mocks. This is a general problem with unittest.mock - see `Why Your Mock Doesn't Work `__. It's also impossible to mock certain references, such as function default arguments: .. code-block:: python def update_books(_now=time.time): # set as default argument so faster lookup for book in books: ... Although such references are rare, they are occasionally used to optimize highly repeated loops. freezegun --------- Steve Pulec's `freezegun `__ library is a popular solution. It provides a clear API which was much of the inspiration for time-machine. The main drawback is its slow implementation. It essentially does a find-and-replace mock of all the places that the ``datetime`` and ``time`` modules have been imported. This gets around the problems with using unittest.mock, but it means the time it takes to do the mocking is proportional to the number of loaded modules. In large projects, this can take several seconds, an impractical overhead for an individual test. It's also not a perfect search, since it searches only module-level imports. Such imports are definitely the most common way projects use date and time functions, but they're not the only way. freezegun won’t find functions that have been “hidden” inside arbitrary objects, such as class-level attributes. It also can't affect C extensions that call the standard library functions, including (I believe) Cython-ized Python code. python-libfaketime ------------------ Simon Weber's `python-libfaketime `__ wraps the `libfaketime `__ library. libfaketime replaces all the C-level system calls for the current time with its own wrappers. It's therefore a "perfect" mock for the current process, affecting every single point the current time might be fetched, and performs much faster than freezegun. Unfortunately python-libfaketime comes with the limitations of ``LD_PRELOAD``. This is a mechanism to replace system libraries for a program as it loads (`explanation `__). This causes two issues in particular when you use python-libfaketime. First, ``LD_PRELOAD`` is only available on Unix platforms, which prevents you from using it on Windows. Second, you have to help manage ``LD_PRELOAD``. You either use python-libfaketime's ``reexec_if_needed()`` function, which restarts (*re-execs*) your test process while loading, or manually manage the ``LD_PRELOAD`` environment variable. Neither is ideal. Re-execing breaks anything that might wrap your test process, such as profilers, debuggers, and IDE test runners. Manually managing the environment variable is a bit of overhead, and must be done for each environment you run your tests in, including each developer's machine. time-machine ------------ time-machine is intended to combine the advantages of freezegun and libfaketime. It works without ``LD_PRELOAD`` but still mocks the standard library functions everywhere they may be referenced. Its weak point is that other libraries using date/time system calls won't be mocked. Thankfully this is rare. It's also possible such python libraries can be added to the set mocked by time-machine. One drawback is that it only works with CPython, so can't be used with other Python interpreters like PyPy. However it may possible to extend it to support other interpreters through different mocking mechanisms. Migrating from libfaketime or freezegun ======================================= freezegun has a useful API, and python-libfaketime copies some of it, with a different function name. time-machine also copies some of freezegun's API, in ``travel()``\'s ``destination``, and ``tick`` arguments, and the ``shift()`` method. There are a few differences: * time-machine's ``tick`` argument defaults to ``True``, because code tends to make the (reasonable) assumption that time progresses whilst running, and should normally be tested as such. Testing with time frozen can make it easy to write complete assertions, but it's quite artificial. Write assertions against time ranges, rather than against exact values. * freezegun interprets dates and naive datetimes in the local time zone (including those parsed from strings with ``dateutil``). This means tests can pass when run in one time zone and fail in another. time-machine instead interprets dates and naive datetimes in UTC so they are fixed points in time. Provide time zones where required. * freezegun's ``tick()`` method has been implemented as ``shift()``, to avoid confusion with the ``tick`` argument. It also requires an explicit delta rather than defaulting to 1 second. * freezegun's ``tz_offset`` argument is not supported, since it only partially mocks the current time zone. Time zones are more complicated than a single offset from UTC, and freezegun only uses the offset in ``time.localtime()``. Instead, time-machine will mock the current time zone if you give it a ``datetime`` with a ``ZoneInfo`` timezone. Some features aren't supported like the ``auto_tick_seconds`` argument. These may be added in a future release. If you are only fairly simple function calls, you should be able to migrate by replacing calls to ``freezegun.freeze_time()`` and ``libfaketime.fake_time()`` with ``time_machine.travel()``. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396564.0 time_machine-2.16.0/src/time_machine.egg-info/SOURCES.txt0000644000175100001660000000060114701236424022414 0ustar00runnerdockerCHANGELOG.rst LICENSE MANIFEST.in README.rst pyproject.toml setup.py src/_time_machine.c src/time_machine/__init__.py src/time_machine/py.typed src/time_machine.egg-info/PKG-INFO src/time_machine.egg-info/SOURCES.txt src/time_machine.egg-info/dependency_links.txt src/time_machine.egg-info/entry_points.txt src/time_machine.egg-info/requires.txt src/time_machine.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396564.0 time_machine-2.16.0/src/time_machine.egg-info/dependency_links.txt0000644000175100001660000000000114701236424024601 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396564.0 time_machine-2.16.0/src/time_machine.egg-info/entry_points.txt0000644000175100001660000000004714701236424024032 0ustar00runnerdocker[pytest11] time_machine = time_machine ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396564.0 time_machine-2.16.0/src/time_machine.egg-info/requires.txt0000644000175100001660000000002014701236424023123 0ustar00runnerdockerpython-dateutil ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728396564.0 time_machine-2.16.0/src/time_machine.egg-info/top_level.txt0000644000175100001660000000003314701236424023261 0ustar00runnerdocker_time_machine time_machine