123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- Metadata-Version: 2.1
- Name: time-machine
- Version: 2.16.0
- Summary: Travel through time in your tests.
- Author-email: Adam Johnson <me@adamj.eu>
- 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 <https://adamj.eu/tech/2020/06/03/introducing-time-machine/>`__ and `the benchmark blog post <https://adamj.eu/tech/2021/02/19/freezegun-versus-time-machine/>`__.
- ----
- **Testing a Django project?**
- Check out my book `Speed Up Your Django Tests <https://adamchainz.gumroad.com/l/suydt>`__ 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 <https://en.m.wikipedia.org/wiki/Unix_time>`__
- * A string, which will be parsed with `dateutil.parse <https://dateutil.readthedocs.io/en/stable/parser.html>`__ 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 <https://docs.python.org/3/library/time.html#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 <https://docs.python.org/3/library/unittest.mock.html>`__ 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 <https://nedbatchelder.com//blog/201908/why_your_mock_doesnt_work.html>`__.
- 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 <https://github.com/spulec/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 <https://github.com/simon-weber/python-libfaketime/>`__ wraps the `libfaketime <https://github.com/wolfcw/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 <http://www.goldsborough.me/c/low-level/kernel/2016/08/29/16-48-53-the_-ld_preload-_trick/>`__).
- 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()``.
|