pax_global_header 0000666 0000000 0000000 00000000064 15051127025 0014510 g ustar 00root root 0000000 0000000 52 comment=c64e79f89bdb02ed01866eb9f74c86006606ecd6
adamchainz-time-machine-591fa85/ 0000775 0000000 0000000 00000000000 15051127025 0016511 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/.clang-format 0000664 0000000 0000000 00000001422 15051127025 0021063 0 ustar 00root root 0000000 0000000 # A clang-format style that approximates Python's PEP 7
# Adapted from: https://github.com/python/cpython/blob/34d7351ac770ac49875fc39396b2a97828ba05ad/Tools/peg_generator/.clang-format#L1
BasedOnStyle: Google
AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false
AlignAfterOpenBracket: DontAlign
BreakBeforeBraces: Stroustrup
ColumnLimit: 95
DerivePointerAlignment: false
IndentWidth: 4
Language: Cpp
PointerAlignment: Right
ReflowComments: true
SpaceBeforeParens: ControlStatements
SpacesInParentheses: false
TabWidth: 4
UseTab: Never
SortIncludes: false
BinPackParameters: false
BinPackArguments: false
AllowAllParametersOfDeclarationOnNextLine: true
AllowAllArgumentsOnNextLine: true
BreakConstructorInitializers: BeforeColon
PackConstructorInitializers: Never
adamchainz-time-machine-591fa85/.editorconfig 0000664 0000000 0000000 00000000345 15051127025 0021170 0 ustar 00root root 0000000 0000000 # http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
end_of_line = lf
[*.py]
indent_size = 4
[Makefile]
indent_style = tab
adamchainz-time-machine-591fa85/.github/ 0000775 0000000 0000000 00000000000 15051127025 0020051 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/.github/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000000131 15051127025 0022643 0 ustar 00root root 0000000 0000000 This project follows [Django's Code of Conduct](https://www.djangoproject.com/conduct/).
adamchainz-time-machine-591fa85/.github/FUNDING.yml 0000664 0000000 0000000 00000000123 15051127025 0021662 0 ustar 00root root 0000000 0000000 github: adamchainz
tidelift: pypi/time-machine
custom:
- "https://adamj.eu/books/"
adamchainz-time-machine-591fa85/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15051127025 0022234 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000034 15051127025 0024221 0 ustar 00root root 0000000 0000000 blank_issues_enabled: false
adamchainz-time-machine-591fa85/.github/ISSUE_TEMPLATE/feature-request.yml 0000664 0000000 0000000 00000000411 15051127025 0026074 0 ustar 00root root 0000000 0000000 name: Feature Request
description: Request an enhancement or new feature.
body:
- type: textarea
id: description
attributes:
label: Description
description: Please describe your feature request with appropriate detail.
validations:
required: true
adamchainz-time-machine-591fa85/.github/ISSUE_TEMPLATE/issue.yml 0000664 0000000 0000000 00000001537 15051127025 0024115 0 ustar 00root root 0000000 0000000 name: Issue
description: File an issue
body:
- type: input
id: python_version
attributes:
label: Python Version
description: Which version of Python were you using?
placeholder: 3.9.0
validations:
required: false
- type: input
id: pytest_version
attributes:
label: pytest Version
description: Which version of pytest were you using (if any)?
placeholder: 6.2.4
validations:
required: false
- type: input
id: package_version
attributes:
label: Package Version
description: Which version of this package were you using? If not the latest version, please check this issue has not since been resolved.
placeholder: 1.0.0
validations:
required: false
- type: textarea
id: description
attributes:
label: Description
description: Please describe your issue.
validations:
required: true
adamchainz-time-machine-591fa85/.github/SECURITY.md 0000664 0000000 0000000 00000000101 15051127025 0021632 0 ustar 00root root 0000000 0000000 Please report security issues directly over email to me@adamj.eu
adamchainz-time-machine-591fa85/.github/dependabot.yml 0000664 0000000 0000000 00000000247 15051127025 0022704 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
groups:
"GitHub Actions":
patterns:
- "*"
schedule:
interval: monthly
adamchainz-time-machine-591fa85/.github/workflows/ 0000775 0000000 0000000 00000000000 15051127025 0022106 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/.github/workflows/main.yml 0000664 0000000 0000000 00000007414 15051127025 0023563 0 ustar 00root root 0000000 0000000 name: CI
on:
push:
branches:
- main
tags:
- '**'
pull_request:
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
tests:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-24.04
strategy:
matrix:
python-version:
- '3.9'
- '3.10'
- '3.11'
- '3.12'
- '3.13'
- '3.13t'
- '3.14'
- '3.14t'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Run tox targets for ${{ matrix.python-version }}
run: uvx --with tox-uv tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
- name: Upload coverage data
uses: actions/upload-artifact@v4
with:
name: coverage-data-${{ matrix.python-version }}
path: '${{ github.workspace }}/.coverage.*'
include-hidden-files: true
if-no-files-found: error
coverage:
name: Coverage
runs-on: ubuntu-24.04
needs: tests
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install dependencies
run: uv pip install --system coverage[toml]
- name: Download data
uses: actions/download-artifact@v4
with:
path: ${{ github.workspace }}
pattern: coverage-data-*
merge-multiple: true
- name: Combine coverage and fail if it's <100%
run: |
python -m coverage combine
python -m coverage html --skip-covered --skip-empty
python -m coverage report --fail-under=100
echo "## Coverage summary" >> $GITHUB_STEP_SUMMARY
python -m coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
- name: Upload HTML report
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: html-report
path: htmlcov
build:
name: Build wheels on ${{ matrix.os }}
if:
startsWith(github.ref, 'refs/tags/')
|| github.ref == 'refs/heads/main'
|| contains(github.event.pull_request.labels.*.name, 'Build')
strategy:
matrix:
os:
- linux
- macos
- windows
runs-on:
${{
(matrix.os == 'linux' && 'ubuntu-24.04')
|| (matrix.os == 'macos' && 'macos-15')
|| (matrix.os == 'windows' && 'windows-2022')
|| 'unknown'
}}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
if: matrix.os == 'linux'
with:
platforms: all
- name: Build sdist
if: ${{ matrix.os == 'linux' }}
run: uv build --sdist
- name: Build wheels
run: uvx --from cibuildwheel==3.1.2 cibuildwheel --output-dir dist
- run: ${{ (matrix.os == 'windows' && 'dir') || 'ls -lh' }} dist/
- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.os }}
path: dist
release:
needs: [coverage, build]
if: success() && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-24.04
environment: release
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: get dist artifacts
uses: actions/download-artifact@v4
with:
merge-multiple: true
pattern: dist-*
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
adamchainz-time-machine-591fa85/.gitignore 0000664 0000000 0000000 00000000121 15051127025 0020473 0 ustar 00root root 0000000 0000000 *.egg-info/
*.pyc
*.so
/.coverage
/.coverage.*
/.tox
/build/
/dist/
/wheelhouse/
adamchainz-time-machine-591fa85/.pre-commit-config.yaml 0000664 0000000 0000000 00000003556 15051127025 0023003 0 ustar 00root root 0000000 0000000 ci:
autoupdate_schedule: monthly
skip:
- clang-format
default_language_version:
python: python3.13
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/crate-ci/typos
rev: 8d96c408b0dca5cc2e5b125482e8aabe5473e153 # frozen: v1
hooks:
- id: typos
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 8184a5b72f4a8fcd003b041ecb04c41a9f34fd2b # frozen: v2.6.0
hooks:
- id: pyproject-fmt
additional_dependencies: ["tox>=4.9"]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 4e2cb0e98735e1a57a027d9440b91663e31d10b0 # frozen: 1.6.0
hooks:
- id: tox-ini-fmt
- repo: https://github.com/rstcheck/rstcheck
rev: 27258fde1ee7d3b1e6a7bbc58f4c7b1dd0e719e5 # frozen: v6.2.5
hooks:
- id: rstcheck
additional_dependencies:
- sphinx==8.1.3
- tomli==2.0.1
- repo: https://github.com/adamchainz/blacken-docs
rev: 78a9dcbecf4f755f65d1f3dec556bc249d723600 # frozen: 1.19.1
hooks:
- id: blacken-docs
additional_dependencies:
- black==25.1.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 4cbc74d53fe5634e58e0e65db7d28939c9cec3f7 # frozen: v0.12.7
hooks:
- id: ruff-check
args: [ --fix ]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 412de98d50e846f31ea6f4b0ad036f2c24a7a024 # frozen: v1.17.1
hooks:
- id: mypy
additional_dependencies:
- pytest==7.1.2
- types-python-dateutil
- repo: local
hooks:
- id: clang-format
name: clang-format
description: ''
entry: clang-format -i
language: system
types_or:
- c
args: [ -style=file ]
adamchainz-time-machine-591fa85/.readthedocs.yaml 0000664 0000000 0000000 00000001107 15051127025 0021737 0 ustar 00root root 0000000 0000000 # .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-24.04
tools:
python: "3.13"
jobs:
pre_create_environment:
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
create_environment:
- uv venv "${READTHEDOCS_VIRTUALENV_PATH}"
install:
- UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --frozen --group docs
sphinx:
configuration: docs/conf.py
fail_on_warning: true
formats: all
adamchainz-time-machine-591fa85/.typos.toml 0000664 0000000 0000000 00000000413 15051127025 0020640 0 ustar 00root root 0000000 0000000 # Configuration file for 'typos' tool
# https://github.com/crate-ci/typos
[default]
extend-ignore-re = [
# Single line ignore comments
"(?Rm)^.*(#|//)\\s*typos: ignore$",
# Multi-line ignore comments
"(?s)(#|//)\\s*typos: off.*?\\n\\s*(#|//)\\s*typos: on"
]
adamchainz-time-machine-591fa85/CHANGELOG.rst 0000664 0000000 0000000 00000000101 15051127025 0020522 0 ustar 00root root 0000000 0000000 See https://time-machine.readthedocs.io/en/latest/changelog.html
adamchainz-time-machine-591fa85/HISTORY.rst 0000664 0000000 0000000 00000000107 15051127025 0020402 0 ustar 00root root 0000000 0000000 See https://github.com/adamchainz/time-machine/blob/main/CHANGELOG.rst
adamchainz-time-machine-591fa85/LICENSE 0000664 0000000 0000000 00000002055 15051127025 0017520 0 ustar 00root root 0000000 0000000 MIT 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.
adamchainz-time-machine-591fa85/MANIFEST.in 0000664 0000000 0000000 00000000163 15051127025 0020247 0 ustar 00root root 0000000 0000000 prune tests
include CHANGELOG.rst
include LICENSE
include pyproject.toml
include README.rst
include src/*/py.typed
adamchainz-time-machine-591fa85/README.rst 0000664 0000000 0000000 00000002322 15051127025 0020177 0 ustar 00root root 0000000 0000000 ============
time-machine
============
.. image:: https://img.shields.io/readthedocs/time-machine?style=for-the-badge
:target: https://time-machine.readthedocs.io/en/latest/
.. 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
----
.. figure:: https://raw.githubusercontent.com/adamchainz/time-machine/main/docs/_static/logo.svg
:alt: time-machine logo
:align: center
Documentation
=============
Please see https://time-machine.readthedocs.io/.
adamchainz-time-machine-591fa85/docs/ 0000775 0000000 0000000 00000000000 15051127025 0017441 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/docs/.gitignore 0000664 0000000 0000000 00000000011 15051127025 0021421 0 ustar 00root root 0000000 0000000 /_build/
adamchainz-time-machine-591fa85/docs/Makefile 0000664 0000000 0000000 00000001177 15051127025 0021107 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?= "-W"
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
adamchainz-time-machine-591fa85/docs/_static/ 0000775 0000000 0000000 00000000000 15051127025 0021067 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/docs/_static/logo.svg 0000664 0000000 0000000 00000053210 15051127025 0022551 0 ustar 00root root 0000000 0000000
adamchainz-time-machine-591fa85/docs/changelog.rst 0000664 0000000 0000000 00000023230 15051127025 0022122 0 ustar 00root root 0000000 0000000 =========
Changelog
=========
2.19.0 (2025-08-19)
-------------------
* Add marker support to :doc:`the pytest plugin `.
Decorate tests with ``@pytest.mark.time_machine()`` to set time during a test, affecting function-level fixtures as well.
Thanks to Javier Buzzi in `PR #499 `__.
* Add asynchronous context manager support to ``time_machine.travel()``.
You can now use ``async with time_machine.travel(...):`` in asynchronous code, per :ref:`the documentation `.
`PR #556 `__.
* Import date and time functions once in the C extension.
This should improve speed a little bit, and avoid segmentation faults when the functions have been swapped out, such as when freezegun is in effect.
(time-machine still won’t apply if freezegun is in effect.)
`PR #555 `__.
2.18.0 (2025-08-18)
-------------------
* Update the :ref:`migration CLI ` to detect unittest classes based on whether they use ``self.assert*`` methods like ``self.assertEqual()``.
* Fix free-threaded Python warning: ``RuntimeWarning: The global interpreter lock (GIL) has been enabled...`` as seen on Python 3.13+.
Thanks to Javier Buzzi in `PR #531 `__.
* Add support to ``travel()`` for ``datetime`` destinations with ``tzinfo`` set to ``datetime.UTC`` (``datetime.timezone.utc``).
Thanks to Lawrence Law in `PR #502 `__.
* Prevent segmentation faults in unlikely scenarios, such as if the ``time_machine`` module cannot be imported.
`PR #543 `__, `PR #545 `__.
* Make ``travel()`` fully unpatch date and time functions when travel ends. This may fix certain edge cases.
`Issue #532 `__.
2.17.0 (2025-08-05)
-------------------
* Include wheels for Python 3.14.
Thanks to Edgar Ramírez Mondragón in `PR #521 `__.
* Support free-threaded Python.
Thanks to Javier Buzzi in `PR #500 `__.
* Add a new CLI for migrating code from freezegun to time-machine.
Install with ``pip install time-machine[cli]`` and run with ``python -m time_machine migrate``.
See more :ref:`in the documentation `.
* Move the documentation to `Read the Docs `__, and add a retro-futuristic logo.
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.
adamchainz-time-machine-591fa85/docs/comparison.rst 0000664 0000000 0000000 00000010160 15051127025 0022343 0 ustar 00root root 0000000 0000000 ==========
Comparison
==========
There are some prior libraries that try to achieve the same thing, with 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.
adamchainz-time-machine-591fa85/docs/conf.py 0000664 0000000 0000000 00000004773 15051127025 0020753 0 ustar 00root root 0000000 0000000 # Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
from __future__ import annotations
import os
import sys
from pathlib import Path
import tomllib
# -- Path setup --------------------------------------------------------------
here = Path(__file__).parent.resolve()
sys.path.insert(0, str(here / ".." / "src"))
# -- Project information -----------------------------------------------------
with (here / ".." / "pyproject.toml").open("rb") as fp:
pyproject_toml_data = tomllib.load(fp)
project = pyproject_toml_data["project"]["name"]
copyright = "2020 Adam Johnson"
author = "Adam Johnson"
# The version info for the project you're documenting, acts as replacement
# for |version| and |release|, also used in various other places throughout
# the built documents.
version = pyproject_toml_data["project"]["version"]
release = version
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx_copybutton",
]
if os.environ.get("READTHEDOCS") == "True":
extensions.append("sphinx_build_compatibility.extension")
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = [
".venv",
"_build",
]
autodoc_typehints = "description"
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_logo = "_static/logo.svg"
html_theme = "furo"
html_theme_options = {
"dark_css_variables": {
"admonition-font-size": "100%",
"admonition-title-font-size": "100%",
},
"light_css_variables": {
"admonition-font-size": "100%",
"admonition-title-font-size": "100%",
},
}
# -- Options for LaTeX output ------------------------------------------
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
(
"index",
"time-machine.tex",
"time-machine Documentation",
"Adam Johnson",
"manual",
),
]
adamchainz-time-machine-591fa85/docs/index.rst 0000664 0000000 0000000 00000002373 15051127025 0021307 0 ustar 00root root 0000000 0000000 time-machine documentation
==========================
*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.
----
time-machine is a tool for mocking the time in tests.
To get started, see :doc:`installation`, :doc:`usage`, and :doc:`pytest_plugin`.
If you’re coming from freezegun or libfaketime, see :doc:`comparison` and :doc:`migration`.
.. toctree::
:maxdepth: 1
:caption: Contents:
installation
usage
pytest_plugin
changelog
comparison
migration
adamchainz-time-machine-591fa85/docs/installation.rst 0000664 0000000 0000000 00000000543 15051127025 0022676 0 ustar 00root root 0000000 0000000 ============
Installation
============
Requirements
------------
Python 3.9 to 3.14 supported, including free-threaded variants from Python 3.13 onwards.
Only CPython is supported at this time because time-machine directly hooks into the C-level API.
Installation
------------
Use **pip**:
.. code-block:: sh
python -m pip install time-machine
adamchainz-time-machine-591fa85/docs/migration.rst 0000664 0000000 0000000 00000010143 15051127025 0022163 0 ustar 00root root 0000000 0000000 =======================================
Migrating from freezegun or libfaketime
=======================================
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 exact 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()``.
.. _migration-cli:
Migration CLI
=============
time-machine comes with a command-line interface to help you migrate from freezegun.
It performs partial replacements on your code to update it to use time-machine's API.
It may leave your code in a broken state, for example where an import of ``freezegun`` has been replaced but calls using it remain—it’s recommended you have a good linting setup to find these, and then you can manually fix them up.
The tool edits files in place, reporting those that it changes.
It’s recommended you start from a clean, committed state in your version control system, so you can easily revert any broken changes.
Run with uv
-----------
If you have `uv `__ installed, you can use its ``uvx`` command to install and run the tool in one go:
.. code-block:: console
$ uvx --from 'time-machine[cli]' python -m time_machine migrate example/tests.py
Replace ``example/tests.py`` with one or more target files.
Run directly
------------
To install the tool before using it, first install time-machine with its ``cli`` extra.
For example, with Pip:
.. code-block:: console
$ python -m pip install time-machine[cli]
Then, run the ``migrate`` subcommand of the module on target files:
.. code-block:: console
$ python -m time_machine migrate example/tests.py
Rewriting example/tests.py
Replace ``example/tests.py`` with one or more target files.
Run against multiple files
--------------------------
To run the tool against all files from your Git repository, follow `this blog post `__.
Changes
-------
The changes the tool makes are:
* ``import freezegun`` -> ``import time_machine``
* ``from freezegun import freeze_time`` -> ``from time_machine import travel``
* In function decorators, class decorators, and context managers: ``freeze_time(...)`` -> ``travel(..., tick=False)``.
This change is only applied when ``freeze_time()`` is called with a single positional argument.
In context managers, it’s only applied when the result isn’t assigned to a variable with ``as``.
The tool is open to extension to cover other compatible changes—PRs welcome!
adamchainz-time-machine-591fa85/docs/pytest_plugin.rst 0000664 0000000 0000000 00000005716 15051127025 0023112 0 ustar 00root root 0000000 0000000 =============
pytest plugin
=============
time-machine works as a pytest plugin, which pytest will detect automatically.
The plugin supplies both a fixture and a marker to control the time during tests.
``time_machine`` marker
-----------------------
Use the ``time_machine`` `marker `__ with a valid destination for :class:`~.travel` to mock the time while a test function runs.
It applies for function-scoped fixtures too, meaning the time will be mocked for any setup or teardown code done in the test function.
For example:
.. code-block:: python
import datetime as dt
@pytest.mark.time_machine(dt.datetime(1985, 10, 26))
def test_delorean_marker():
assert dt.date.today().isoformat() == "1985-10-26"
Or for a class:
.. code-block:: python
import datetime as dt
import pytest
@pytest.mark.time_machine(dt.datetime(1985, 10, 26))
class TestSomething:
def test_one(self):
assert dt.date.today().isoformat() == "1985-10-26"
def test_two(self):
assert dt.date.today().isoformat() == "1985-10-26"
``time_machine`` fixture
------------------------
Use the function-scoped `fixture `__ ``time_machine`` to control time in your tests.
It provides an object with two methods, ``move_to()`` and ``shift()``, which work the same as their equivalents in the :class:`time_machine.Coordinates` class.
Until you call ``move_to()``, time is not mocked.
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
It’s possible to combine the marker and fixture in the same test:
.. code-block:: python
import datetime as dt
import pytest
@pytest.mark.time_machine(dt.datetime(1985, 10, 26))
def test_delorean_marker_and_fixture(time_machine):
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"
adamchainz-time-machine-591fa85/docs/usage.rst 0000664 0000000 0000000 00000026023 15051127025 0021302 0 ustar 00root root 0000000 0000000 =====
Usage
=====
.. currentmodule:: time_machine
This document covers time-machine’s API.
.. warning::
Time is a global state.
When mocking it, all concurrent threads or asynchronous functions are also 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 call datetime functions on a database server, they will return the real time.
Main API
========
.. autoclass:: travel
:param destination:
:param tick:
:return:
``travel`` instance
``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 (synchronous or asynchronous).
``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|_ or |datetime.UTC|_, 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
.. |datetime.UTC| replace:: ``datetime.UTC`` (``datetime.timezone.utc``)
.. _datetime.UTC: https://docs.python.org/3/library/datetime.html#datetime.UTC
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 ``travel()`` independently, use its ``start()`` method to move to the destination time, and ``stop()`` to move back.
.. automethod:: start
.. automethod:: stop
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
.. _travel-context-manager:
Context manager
^^^^^^^^^^^^^^^
When used as a context manager, time is mocked during the ``with`` block.
This works both synchronously:
.. 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
…and asynchronously:
.. code-block:: python
import time
import time_machine
async def test_in_the_deep_past():
async 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 autouse fixture pattern :doc:`in the pytest plugin documentation `.
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)
.. autoclass:: 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.
.. automethod:: move_to
``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
.. automethod:: shift
``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
Escape hatch API
================
.. autodata:: escape_hatch
The ``escape_hatch`` object provides functions to bypass time-machine, calling 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)
adamchainz-time-machine-591fa85/pyproject.toml 0000664 0000000 0000000 00000006670 15051127025 0021436 0 ustar 00root root 0000000 0000000 [build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=77",
]
[project]
name = "time-machine"
version = "2.19.0"
description = "Travel through time in your tests."
readme = "README.rst"
keywords = [
"date",
"datetime",
"mock",
"test",
"testing",
"tests",
"time",
"warp",
]
license = "MIT"
license-files = [ "LICENSE" ]
authors = [
{ name = "Adam Johnson", email = "me@adamj.eu" },
]
requires-python = ">=3.9"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: Pytest",
"Intended Audience :: Developers",
"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",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
"python-dateutil",
]
optional-dependencies.cli = [
"tokenize-rt",
]
urls.Changelog = "https://time-machine.readthedocs.io/en/latest/changelog.html"
urls.Documentation = "https://time-machine.readthedocs.io/"
urls.Funding = "https://adamj.eu/books/"
urls.Repository = "https://github.com/adamchainz/time-machine"
entry-points.pytest11.time_machine = "time_machine"
[dependency-groups]
test = [
"backports-zoneinfo; python_version<'3.9'",
"coverage[toml]",
"pytest",
"pytest-randomly",
"python-dateutil",
]
docs = [
"furo>=2024.8.6",
"sphinx>=7.4.7",
"sphinx-build-compatibility",
"sphinx-copybutton>=0.5.2",
]
[tool.cibuildwheel]
build = [
"cp314-*",
"cp314t-*",
"cp313-*",
"cp313t-*",
"cp312-*",
"cp311-*",
"cp310-*",
"cp39-*",
]
enable = [
# Enable free-threaded wheels on Python 3.13 (where it was experimental)
"cpython-freethreading",
]
linux.archs = [ "x86_64", "i686", "aarch64" ]
macos.archs = [ "x86_64", "universal2" ]
windows.archs = [ "AMD64", "x86", "ARM64" ]
[tool.ruff]
lint.select = [
# flake8-bugbear
"B",
# flake8-comprehensions
"C4",
# pycodestyle
"E",
# Pyflakes errors
"F",
# isort
"I",
# flake8-simplify
"SIM",
# flake8-tidy-imports
"TID",
# pyupgrade
"UP",
# Pyflakes warnings
"W",
]
lint.ignore = [
# flake8-bugbear opinionated rules
"B9",
# line-too-long
"E501",
# suppressible-exception
"SIM105",
# if-else-block-instead-of-if-exp
"SIM108",
]
lint.extend-safe-fixes = [
# non-pep585-annotation
"UP006",
]
lint.isort.required-imports = [ "from __future__ import annotations" ]
[tool.pyproject-fmt]
max_supported_python = "3.14"
[tool.pytest.ini_options]
addopts = """\
--strict-config
--strict-markers
-p pytester
"""
xfail_strict = true
[tool.coverage.run]
branch = true
parallel = true
source = [
"time_machine",
"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]
ignore_directives = [
"autoclass",
"autodata",
]
report_level = "ERROR"
[tool.uv.sources]
sphinx-build-compatibility = { git = "https://github.com/readthedocs/sphinx-build-compatibility", rev = "4f304bd4562cdc96316f4fec82b264ca379d23e0" }
adamchainz-time-machine-591fa85/setup.py 0000664 0000000 0000000 00000000565 15051127025 0020231 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import sys
from setuptools import Extension, 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"])])
adamchainz-time-machine-591fa85/src/ 0000775 0000000 0000000 00000000000 15051127025 0017300 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/src/_time_machine.c 0000664 0000000 0000000 00000057335 15051127025 0022242 0 ustar 00root root 0000000 0000000 #include "Python.h"
#include
#include
// Module state
typedef struct {
// Imported objects
PyObject *datetime_module;
PyObject *time_module;
PyObject *datetime_class;
PyCFunctionObject *datetime_datetime_now;
PyCFunctionObject *datetime_datetime_utcnow;
PyCFunctionObject *time_clock_gettime;
PyCFunctionObject *time_clock_gettime_ns;
PyCFunctionObject *time_gmtime;
PyCFunctionObject *time_localtime;
PyCFunctionObject *time_monotonic;
PyCFunctionObject *time_monotonic_ns;
PyCFunctionObject *time_strftime;
PyCFunctionObject *time_time;
PyCFunctionObject *time_time_ns;
// Original method pointers from date and time functions
#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");
if (time_machine_module == NULL) {
return NULL; // Propagate ImportError
}
PyObject *time_machine_now = PyObject_GetAttrString(time_machine_module, "now");
if (time_machine_now == NULL) {
Py_DECREF(time_machine_module);
return NULL; // Propagate AttributeError
}
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 *result = state->original_now(state->datetime_class, args, nargs, kwnames);
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");
if (time_machine_module == NULL) {
return NULL; // Propagate ImportError
}
PyObject *time_machine_utcnow = PyObject_GetAttrString(time_machine_module, "utcnow");
if (time_machine_utcnow == NULL) {
Py_DECREF(time_machine_module);
return NULL; // Propagate AttributeError
}
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 *result = state->original_utcnow(state->datetime_class, args);
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");
if (time_machine_module == NULL) {
return NULL; // Propagate ImportError
}
PyObject *time_machine_clock_gettime =
PyObject_GetAttrString(time_machine_module, "clock_gettime");
if (time_machine_clock_gettime == NULL) {
Py_DECREF(time_machine_module);
return NULL; // Propagate AttributeError
}
#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 *result = state->original_clock_gettime(state->time_module, args);
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");
if (time_machine_module == NULL) {
return NULL; // Propagate ImportError
}
PyObject *time_machine_clock_gettime_ns =
PyObject_GetAttrString(time_machine_module, "clock_gettime_ns");
if (time_machine_clock_gettime_ns == NULL) {
Py_DECREF(time_machine_module);
return NULL; // Propagate AttributeError
}
#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 *result = state->original_clock_gettime_ns(state->time_module, args);
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");
if (time_machine_module == NULL) {
return NULL; // Propagate ImportError
}
PyObject *time_machine_gmtime = PyObject_GetAttrString(time_machine_module, "gmtime");
if (time_machine_gmtime == NULL) {
Py_DECREF(time_machine_module);
return NULL; // Propagate AttributeError
}
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 *result = state->original_gmtime(state->time_module, args);
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");
if (time_machine_module == NULL) {
return NULL; // Propagate ImportError
}
PyObject *time_machine_localtime =
PyObject_GetAttrString(time_machine_module, "localtime");
if (time_machine_localtime == NULL) {
Py_DECREF(time_machine_module);
return NULL; // Propagate AttributeError
}
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 *result = state->original_localtime(state->time_module, args);
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 *result = state->original_monotonic(state->time_module, args);
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 *result = state->original_monotonic_ns(state->time_module, args);
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");
if (time_machine_module == NULL) {
return NULL; // Propagate ImportError
}
PyObject *time_machine_strftime = PyObject_GetAttrString(time_machine_module, "strftime");
if (time_machine_strftime == NULL) {
Py_DECREF(time_machine_module);
return NULL; // Propagate AttributeError
}
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 *result = state->original_strftime(state->time_module, args);
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");
if (time_machine_module == NULL) {
return NULL; // Propagate ImportError
}
PyObject *time_machine_time = PyObject_GetAttrString(time_machine_module, "time");
if (time_machine_time == NULL) {
Py_DECREF(time_machine_module);
return NULL; // Propagate AttributeError
}
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 *result = state->original_time(state->time_module, args);
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");
if (time_machine_module == NULL) {
return NULL; // Propagate ImportError
}
PyObject *time_machine_time_ns = PyObject_GetAttrString(time_machine_module, "time_ns");
if (time_machine_time_ns == NULL) {
Py_DECREF(time_machine_module);
return NULL; // Propagate AttributeError
}
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 *result = state->original_time_ns(state->time_module, args);
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(PyObject *module, PyObject *unused)
{
_time_machine_state *state = PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
if (state->original_time)
Py_RETURN_NONE;
#if PY_VERSION_HEX >= 0x030d00a4
state->original_now =
(PyCFunctionFastWithKeywords)state->datetime_datetime_now->m_ml->ml_meth;
#else
state->original_now =
(_PyCFunctionFastWithKeywords)state->datetime_datetime_now->m_ml->ml_meth;
#endif
state->datetime_datetime_now->m_ml->ml_meth = (PyCFunction)_time_machine_now;
state->original_utcnow = state->datetime_datetime_utcnow->m_ml->ml_meth;
state->datetime_datetime_utcnow->m_ml->ml_meth = _time_machine_utcnow;
/*
time.clock_gettime(), only available on Unix platforms.
*/
if (state->time_clock_gettime != NULL) {
state->original_clock_gettime = state->time_clock_gettime->m_ml->ml_meth;
state->time_clock_gettime->m_ml->ml_meth = _time_machine_clock_gettime;
}
/*
time.clock_gettime_ns(), only available on Unix platforms.
*/
if (state->time_clock_gettime_ns != NULL) {
state->original_clock_gettime_ns = state->time_clock_gettime_ns->m_ml->ml_meth;
state->time_clock_gettime_ns->m_ml->ml_meth = _time_machine_clock_gettime_ns;
}
state->original_gmtime = state->time_gmtime->m_ml->ml_meth;
state->time_gmtime->m_ml->ml_meth = _time_machine_gmtime;
state->original_localtime = state->time_localtime->m_ml->ml_meth;
state->time_localtime->m_ml->ml_meth = _time_machine_localtime;
state->original_monotonic = state->time_monotonic->m_ml->ml_meth;
state->time_monotonic->m_ml->ml_meth = _time_machine_time;
state->original_monotonic_ns = state->time_monotonic_ns->m_ml->ml_meth;
state->time_monotonic_ns->m_ml->ml_meth = _time_machine_time_ns;
state->original_strftime = state->time_strftime->m_ml->ml_meth;
state->time_strftime->m_ml->ml_meth = _time_machine_strftime;
state->original_time = state->time_time->m_ml->ml_meth;
state->time_time->m_ml->ml_meth = _time_machine_time;
state->original_time_ns = state->time_time_ns->m_ml->ml_meth;
state->time_time_ns->m_ml->ml_meth = _time_machine_time_ns;
Py_RETURN_NONE;
}
PyDoc_STRVAR(patch_doc,
"patch() -> None\n\
\n\
Swap in helpers.");
static PyObject *
_time_machine_unpatch(PyObject *module, PyObject *unused)
{
_time_machine_state *state = PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
if (!state->original_time)
Py_RETURN_NONE;
#if PY_VERSION_HEX >= 0x030d00a4
state->datetime_datetime_now->m_ml->ml_meth = (PyCFunction)state->original_now;
#else
state->datetime_datetime_now->m_ml->ml_meth = (PyCFunction)state->original_now;
#endif
state->original_now = NULL;
state->datetime_datetime_utcnow->m_ml->ml_meth = state->original_utcnow;
state->original_utcnow = NULL;
/*
time.clock_gettime(), only available on Unix platforms.
*/
if (state->time_clock_gettime != NULL) {
state->time_clock_gettime->m_ml->ml_meth = state->original_clock_gettime;
state->original_clock_gettime = NULL;
}
/*
time.clock_gettime_ns(), only available on Unix platforms.
*/
if (state->time_clock_gettime_ns != NULL) {
state->time_clock_gettime_ns->m_ml->ml_meth = state->original_clock_gettime_ns;
state->original_clock_gettime_ns = NULL;
}
state->time_gmtime->m_ml->ml_meth = state->original_gmtime;
state->original_gmtime = NULL;
state->time_localtime->m_ml->ml_meth = state->original_localtime;
state->original_localtime = NULL;
state->time_monotonic->m_ml->ml_meth = state->original_monotonic;
state->original_monotonic = NULL;
state->time_monotonic_ns->m_ml->ml_meth = state->original_monotonic_ns;
state->original_monotonic_ns = NULL;
state->time_strftime->m_ml->ml_meth = state->original_strftime;
state->original_strftime = NULL;
state->time_time->m_ml->ml_meth = state->original_time;
state->original_time = NULL;
state->time_time_ns->m_ml->ml_meth = state->original_time_ns;
state->original_time_ns = NULL;
Py_RETURN_NONE;
}
PyDoc_STRVAR(unpatch_doc,
"unpatch() -> None\n\
\n\
Swap out 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", (PyCFunction)_time_machine_patch, METH_NOARGS, patch_doc},
{"unpatch", (PyCFunction)_time_machine_unpatch, METH_NOARGS, unpatch_doc},
{NULL, NULL} /* sentinel */
};
static int
_time_machine_exec(PyObject *module)
{
_time_machine_state *state = get_time_machine_state(module);
state->datetime_module = PyImport_ImportModule("datetime");
if (state->datetime_module == NULL) {
goto error;
}
state->datetime_class = PyObject_GetAttrString(state->datetime_module, "datetime");
if (state->datetime_class == NULL) {
goto error;
}
state->datetime_datetime_now =
(PyCFunctionObject *)PyObject_GetAttrString(state->datetime_class, "now");
if (state->datetime_datetime_now == NULL) {
goto error;
}
state->datetime_datetime_utcnow =
(PyCFunctionObject *)PyObject_GetAttrString(state->datetime_class, "utcnow");
if (state->datetime_datetime_utcnow == NULL) {
goto error;
}
state->time_module = PyImport_ImportModule("time");
if (state->time_module == NULL) {
goto error;
}
state->time_clock_gettime =
(PyCFunctionObject *)PyObject_GetAttrString(state->time_module, "clock_gettime");
if (state->time_clock_gettime == NULL) {
// time.clock_gettime() is only available on Unix platforms.
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
}
else {
goto error;
}
}
state->time_clock_gettime_ns =
(PyCFunctionObject *)PyObject_GetAttrString(state->time_module, "clock_gettime_ns");
if (state->time_clock_gettime_ns == NULL) {
// time.clock_gettime_ns() is only available on Unix platforms.
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
}
else {
goto error;
}
}
state->time_gmtime =
(PyCFunctionObject *)PyObject_GetAttrString(state->time_module, "gmtime");
if (state->time_gmtime == NULL) {
goto error;
}
state->time_localtime =
(PyCFunctionObject *)PyObject_GetAttrString(state->time_module, "localtime");
if (state->time_localtime == NULL) {
goto error;
}
state->time_monotonic =
(PyCFunctionObject *)PyObject_GetAttrString(state->time_module, "monotonic");
if (state->time_monotonic == NULL) {
goto error;
}
state->time_monotonic_ns =
(PyCFunctionObject *)PyObject_GetAttrString(state->time_module, "monotonic_ns");
if (state->time_monotonic_ns == NULL) {
goto error;
}
state->time_strftime =
(PyCFunctionObject *)PyObject_GetAttrString(state->time_module, "strftime");
if (state->time_strftime == NULL) {
goto error;
}
state->time_time = (PyCFunctionObject *)PyObject_GetAttrString(state->time_module, "time");
if (state->time_time == NULL) {
goto error;
}
state->time_time_ns =
(PyCFunctionObject *)PyObject_GetAttrString(state->time_module, "time_ns");
if (state->time_time_ns == NULL) {
goto error;
}
return 0;
error:
Py_CLEAR(state->datetime_module);
Py_CLEAR(state->datetime_class);
Py_CLEAR(state->datetime_datetime_now);
Py_CLEAR(state->datetime_datetime_utcnow);
Py_CLEAR(state->time_module);
Py_CLEAR(state->time_clock_gettime);
Py_CLEAR(state->time_clock_gettime_ns);
Py_CLEAR(state->time_gmtime);
Py_CLEAR(state->time_localtime);
Py_CLEAR(state->time_monotonic);
Py_CLEAR(state->time_monotonic_ns);
Py_CLEAR(state->time_strftime);
Py_CLEAR(state->time_time);
Py_CLEAR(state->time_time_ns);
return -1;
}
static int
_time_machine_traverse(PyObject *module, visitproc visit, void *arg)
{
_time_machine_state *state = get_time_machine_state(module);
Py_VISIT(state->datetime_module);
Py_VISIT(state->datetime_class);
Py_VISIT(state->datetime_datetime_now);
Py_VISIT(state->datetime_datetime_utcnow);
Py_VISIT(state->time_module);
Py_VISIT(state->time_clock_gettime);
Py_VISIT(state->time_clock_gettime_ns);
Py_VISIT(state->time_gmtime);
Py_VISIT(state->time_localtime);
Py_VISIT(state->time_monotonic);
Py_VISIT(state->time_monotonic_ns);
Py_VISIT(state->time_strftime);
Py_VISIT(state->time_time);
Py_VISIT(state->time_time_ns);
return 0;
}
static int
_time_machine_clear(PyObject *module)
{
_time_machine_state *state = get_time_machine_state(module);
Py_CLEAR(state->datetime_module);
Py_CLEAR(state->datetime_class);
Py_CLEAR(state->datetime_datetime_now);
Py_CLEAR(state->datetime_datetime_utcnow);
Py_CLEAR(state->time_module);
Py_CLEAR(state->time_clock_gettime);
Py_CLEAR(state->time_clock_gettime_ns);
Py_CLEAR(state->time_gmtime);
Py_CLEAR(state->time_localtime);
Py_CLEAR(state->time_monotonic);
Py_CLEAR(state->time_monotonic_ns);
Py_CLEAR(state->time_strftime);
Py_CLEAR(state->time_time);
Py_CLEAR(state->time_time_ns);
return 0;
}
static PyModuleDef_Slot _time_machine_slots[] = {{Py_mod_exec, _time_machine_exec},
// On Python 3.13+, declare free-threaded support.
// https://py-free-threading.github.io/porting-extensions/#declaring-free-threaded-support
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{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 = _time_machine_traverse,
.m_clear = _time_machine_clear};
PyMODINIT_FUNC
PyInit__time_machine(void)
{
PyObject *result = PyModuleDef_Init(&_time_machine_module);
return result;
}
adamchainz-time-machine-591fa85/src/time_machine/ 0000775 0000000 0000000 00000000000 15051127025 0021722 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/src/time_machine/__init__.py 0000664 0000000 0000000 00000036501 15051127025 0024040 0 ustar 00root root 0000000 0000000 from __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, 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, Callable, TypeVar, Union, cast, overload
from unittest import TestCase, 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
elif dest.tzinfo == dt.timezone.utc:
tzname = "UTC"
elif 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:
if not coordinates_stack:
_time_machine.patch()
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:
coordinates_stack.pop()._stop()
if not coordinates_stack:
_time_machine.unpatch()
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()
async def __aenter__(self) -> Coordinates:
return self.start()
async def __aexit__(
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:
return dt.datetime.fromtimestamp(time(), tz)
def utcnow() -> dt.datetime:
return dt.datetime.fromtimestamp(time(), dt.timezone.utc).replace(tzinfo=None)
# time module
def clock_gettime(clk_id: int) -> float:
if 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 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 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 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)
else:
result = _time_machine.original_strftime(format, localtime())
return result
def time() -> float:
return coordinates_stack[-1].time()
def time_ns() -> int:
return coordinates_stack[-1].time_ns()
# pytest plugin
if HAVE_PYTEST: # pragma: no branch
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
"""
Add the fixture to any tests with the marker.
"""
for item in items:
if item.get_closest_marker("time_machine"):
item.fixturenames.insert(0, "time_machine") # type: ignore[attr-defined]
def pytest_configure(config: pytest.Config) -> None:
"""
Register the marker.
"""
config.addinivalue_line(
"markers", "time_machine(...): set the time with time-machine"
)
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(
request: pytest.FixtureRequest,
) -> TypingGenerator[TimeMachineFixture, None, None]:
fixture = TimeMachineFixture()
marker = request.node.get_closest_marker("time_machine")
if marker:
fixture.move_to(*marker.args, **marker.kwargs)
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()
adamchainz-time-machine-591fa85/src/time_machine/__main__.py 0000664 0000000 0000000 00000000223 15051127025 0024011 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from time_machine.cli import main
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())
adamchainz-time-machine-591fa85/src/time_machine/cli.py 0000664 0000000 0000000 00000030614 15051127025 0023047 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import argparse
import ast
import sys
import warnings
from collections import defaultdict
from collections.abc import Callable, Mapping, Sequence
from functools import partial
from tokenize_rt import (
UNIMPORTANT_WS,
Offset,
Token,
reversed_enumerate,
src_to_tokens,
tokens_to_src,
)
CODE = "CODE"
DEDENT = "DEDENT"
def main(argv: Sequence[str] | None = None) -> int:
"""Main entry point for the migration tool."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(
dest="command", help="Available commands", required=True
)
migrate_parser = subparsers.add_parser(
"migrate",
help="Migrate Python files from freezegun to time-machine",
)
migrate_parser.add_argument("file", nargs="+")
args = parser.parse_args(argv)
if args.command == "migrate":
return migrate_files(files=args.file)
else: # pragma: no cover
# Unreachable
raise NotImplementedError(f"Command {args.command} does not exist.")
def migrate_files(files: list[str]) -> int:
returncode = 0
for filename in files:
returncode |= migrate_file(filename)
return returncode
def migrate_file(filename: str) -> int:
if filename == "-":
contents_bytes = sys.stdin.buffer.read()
else:
with open(filename, "rb") as fb:
contents_bytes = fb.read()
try:
contents_text_orig = contents_text = contents_bytes.decode()
except UnicodeDecodeError:
print(f"{filename} is non-utf-8 (not supported)")
return 1
contents_text = migrate_contents(contents_text)
if filename == "-":
print(contents_text, end="")
elif contents_text != contents_text_orig:
print(f"Rewriting {filename}", file=sys.stderr)
with open(filename, "w", encoding="UTF-8", newline="") as f:
f.write(contents_text)
return contents_text != contents_text_orig
def migrate_contents(contents_text: str) -> str:
"""Migrate a single text from freezegun to time-machine."""
try:
ast_obj = ast_parse(contents_text)
except SyntaxError:
return contents_text
callbacks = visit(ast_obj)
if not callbacks:
return contents_text
tokens = src_to_tokens(contents_text)
fixup_dedent_tokens(tokens)
for i, token in reversed_enumerate(tokens):
if not token.src:
continue
# though this is a defaultdict, by using `.get()` this function's
# self time is almost 50% faster
for callback in callbacks.get(token.offset, ()):
callback(tokens, i)
# no types for tokenize-rt
return tokens_to_src(tokens) # type: ignore [no-any-return]
def ast_parse(contents_text: str) -> ast.Module:
# intentionally ignore warnings, we can't do anything about them
with warnings.catch_warnings():
warnings.simplefilter("ignore")
return ast.parse(contents_text.encode())
def fixup_dedent_tokens(tokens: list[Token]) -> None: # pragma: no cover
"""For whatever reason the DEDENT / UNIMPORTANT_WS tokens are misordered
| if True:
| if True:
| pass
| else:
|^ ^- DEDENT
|+----UNIMPORTANT_WS
"""
for i, token in enumerate(tokens):
if token.name == UNIMPORTANT_WS and tokens[i + 1].name == DEDENT:
tokens[i], tokens[i + 1] = tokens[i + 1], tokens[i]
TokenFunc = Callable[[list[Token], int], None]
def visit(tree: ast.Module) -> Mapping[Offset, list[TokenFunc]]:
"""
Visit the AST and return a list of callbacks to apply to the tokens.
This is a placeholder function; actual implementation would depend on
the specific migration logic.
"""
ret = defaultdict(list)
freezegun_import_seen = False
freeze_time_import_seen = False
for node in ast.walk(tree):
# On Python 3.10+, this would look a lot better with the match statement
if isinstance(node, ast.Import):
if (
len(node.names) == 1
and (alias := node.names[0]).name == "freezegun"
and alias.asname is None
):
freezegun_import_seen = True
ret[ast_start_offset(node)].append(replace_import)
elif isinstance(node, ast.ImportFrom):
if (
node.module == "freezegun"
and len(node.names) == 1
and (alias := node.names[0]).name == "freeze_time"
and alias.asname is None
):
freeze_time_import_seen = True
ret[ast_start_offset(node)].append(
partial(replace_import_from, node=node)
)
elif isinstance(node, ast.FunctionDef):
for decorator in node.decorator_list:
if (
isinstance(decorator, ast.Call)
and migratable_call(decorator)
and (
(
freezegun_import_seen
and isinstance(decorator.func, ast.Attribute)
and decorator.func.attr == "freeze_time"
and isinstance(decorator.func.value, ast.Name)
and decorator.func.value.id == "freezegun"
)
or (
freeze_time_import_seen
and isinstance(decorator.func, ast.Name)
and decorator.func.id == "freeze_time"
)
)
):
ret[ast_start_offset(decorator.func)].append(
partial(switch_to_travel, node=decorator.func)
)
ret[ast_start_offset(decorator)].append(
partial(add_tick_false, node=decorator)
)
elif isinstance(node, ast.ClassDef):
if node.decorator_list and looks_like_unittest_class(node):
for decorator in node.decorator_list:
if (
isinstance(decorator, ast.Call)
and migratable_call(decorator)
and (
(
freezegun_import_seen
and isinstance(decorator.func, ast.Attribute)
and decorator.func.attr == "freeze_time"
and isinstance(decorator.func.value, ast.Name)
and decorator.func.value.id == "freezegun"
)
or (
freeze_time_import_seen
and isinstance(decorator.func, ast.Name)
and decorator.func.id == "freeze_time"
)
)
):
ret[ast_start_offset(decorator.func)].append(
partial(switch_to_travel, node=decorator.func)
)
ret[ast_start_offset(decorator)].append(
partial(add_tick_false, node=decorator)
)
elif isinstance(node, ast.With):
for item in node.items:
context_expr = item.context_expr
if (
isinstance(context_expr, ast.Call)
and migratable_call(context_expr)
and item.optional_vars is None
and (
(
freezegun_import_seen
and isinstance(context_expr.func, ast.Attribute)
and context_expr.func.attr == "freeze_time"
and isinstance(context_expr.func.value, ast.Name)
and context_expr.func.value.id == "freezegun"
)
or (
freeze_time_import_seen
and isinstance(context_expr.func, ast.Name)
and context_expr.func.id == "freeze_time"
)
)
):
ret[ast_start_offset(context_expr.func)].append(
partial(switch_to_travel, node=context_expr.func)
)
ret[ast_start_offset(context_expr)].append(
partial(add_tick_false, node=context_expr)
)
return ret # type: ignore [return-value]
def migratable_call(node: ast.Call) -> bool:
return (
len(node.args) == 1
# We could allow tick being set, as long as we didn't then add it
and len(node.keywords) == 0
)
def looks_like_unittest_class(node: ast.ClassDef) -> bool:
"""
Heuristically determine if a class is a unittest.TestCase subclass.
"""
for base in node.bases:
if (
isinstance(base, ast.Name)
and base.id.endswith("TestCase")
or (
isinstance(base, ast.Attribute)
and isinstance(base.value, ast.Name)
and base.value.id == "unittest"
and base.attr.endswith("TestCase")
)
):
return True
subnode: ast.AST
for subnode in node.body:
if isinstance(subnode, ast.FunctionDef) and subnode.name in (
"setUp",
"setUpClass",
"tearDown",
"tearDownClass",
"setUpTestData",
):
return True
if isinstance(subnode, ast.AsyncFunctionDef) and subnode.name in (
"asyncSetUp",
"asyncTearDown",
):
return True
for subnode in ast.walk(node):
if (
isinstance(subnode, ast.Attribute)
and isinstance(subnode.value, ast.Name)
and subnode.value.id == "self"
and subnode.attr in UNITTEST_ASSERT_NAMES
):
return True
return False
UNITTEST_ASSERT_NAMES = frozenset(
[
"assertAlmostEqual",
"assertCountEqual",
"assertDictEqual",
"assertEqual",
"assertFalse",
"assertGreater",
"assertGreaterEqual",
"assertIn",
"assertIs",
"assertIsInstance",
"assertIsNone",
"assertIsNot",
"assertIsNotNone",
"assertLess",
"assertLessEqual",
"assertListEqual",
"assertLogs",
"assertMultiLineEqual",
"assertNoLogs",
"assertNotAlmostEqual",
"assertNotEqual",
"assertNotIn",
"assertNotIsInstance",
"assertNotRegex",
"assertRaises",
"assertRaisesRegex",
"assertRegex",
"assertSequenceEqual",
"assertSetEqual",
"assertTrue",
"assertTupleEqual",
"assertWarns",
"assertWarnsRegex",
]
)
def ast_start_offset(node: ast.alias | ast.expr | ast.keyword | ast.stmt) -> Offset:
return Offset(node.lineno, node.col_offset)
def replace_import(tokens: list[Token], i: int) -> None:
while True:
if tokens[i].name == "NAME" and tokens[i].src == "freezegun":
break
i += 1
tokens[i] = Token(name="NAME", src="time_machine")
def replace_import_from(tokens: list[Token], i: int, node: ast.ImportFrom) -> None:
j = find_last_token(tokens, i, node=node)
tokens[i : j + 1] = [Token(name=CODE, src="import time_machine")]
def switch_to_travel(
tokens: list[Token], i: int, node: ast.Attribute | ast.Name
) -> None:
j = find_last_token(tokens, i, node=node)
tokens[i : j + 1] = [Token(name=CODE, src="time_machine.travel")]
def add_tick_false(tokens: list[Token], i: int, node: ast.Call) -> None:
"""
Add `tick=False` to the function call.
"""
j = find_last_token(tokens, i, node=node)
tokens.insert(j, Token(name=CODE, src=", tick=False"))
# Token functions
def find_last_token(
tokens: list[Token], i: int, *, node: ast.expr | ast.keyword | ast.stmt
) -> int:
"""
Find the last token corresponding to the given ast node.
"""
while (
tokens[i].line is None or tokens[i].line < node.end_lineno
): # pragma: no cover
i += 1
while (
tokens[i].utf8_byte_offset is None
or tokens[i].utf8_byte_offset < node.end_col_offset
):
i += 1
return i - 1
adamchainz-time-machine-591fa85/src/time_machine/py.typed 0000664 0000000 0000000 00000000000 15051127025 0023407 0 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/tests/ 0000775 0000000 0000000 00000000000 15051127025 0017653 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/tests/__init__.py 0000664 0000000 0000000 00000000000 15051127025 0021752 0 ustar 00root root 0000000 0000000 adamchainz-time-machine-591fa85/tests/conftest.py 0000664 0000000 0000000 00000000224 15051127025 0022050 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import os
import time
# Isolate tests from the host machine’s timezone
os.environ["TZ"] = "UTC"
time.tzset()
adamchainz-time-machine-591fa85/tests/test_cli.py 0000664 0000000 0000000 00000043432 15051127025 0022041 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import io
import subprocess
import sys
from pathlib import Path
from textwrap import dedent
from unittest import mock
import pytest
# import __main__ for coverage
from time_machine import __main__ # noqa: F401
from time_machine.cli import main, migrate_contents
class TestMain:
def test_no_subcommand(self, capsys):
with pytest.raises(SystemExit) as excinfo:
main([])
assert excinfo.value.code == 2
out, err = capsys.readouterr()
prog_name = (
f"{Path(sys.executable).name} -m pytest"
if sys.version_info >= (3, 14) and sys.modules["__main__"].__spec__
else Path(sys.argv[0]).name
)
assert err == (
f"usage: {prog_name} [-h] {{migrate}} ...\n"
+ f"{prog_name}: error: the following arguments are required: command\n"
)
assert out == ""
def test_main_help(
self,
):
with pytest.raises(SystemExit) as excinfo:
main(["--help"])
assert excinfo.value.code == 0
def test_main_help_subprocess(
self,
):
proc = subprocess.run(
[sys.executable, "-m", "time_machine", "--help"],
check=True,
capture_output=True,
)
if sys.version_info >= (3, 14):
assert proc.stdout.startswith(
f"usage: {Path(sys.executable).name} -m time_machine ".encode()
)
else:
assert proc.stdout.startswith(b"usage: __main__.py ")
def test_migrate_help_command(self, capsys):
with pytest.raises(SystemExit) as excinfo:
main(["migrate", "--help"])
assert excinfo.value.code == 0
def test_migrate_no_files(self, capsys):
with pytest.raises(SystemExit) as excinfo:
main(["migrate"])
assert excinfo.value.code == 2
def test_migrate_empty(self, capsys, tmp_path):
path = tmp_path / "example.py"
path.write_text("\n")
result = main(["migrate", str(path)])
assert result == 0
out, err = capsys.readouterr()
assert out == ""
assert err == ""
assert path.read_text() == "\n"
def test_migrate_syntax_error(self, capsys, tmp_path):
path = tmp_path / "example.py"
path.write_text("def def def\n")
result = main(["migrate", str(path)])
assert result == 0
out, err = capsys.readouterr()
assert out == ""
assert err == ""
assert path.read_text() == "def def def\n"
def test_migrate_non_utf8(self, capsys, tmp_path):
path = tmp_path / "example.py"
path.write_bytes("# -*- coding: cp1252 -*-\nx = €\n".encode("cp1252"))
result = main(["migrate", str(path)])
assert result == 1
out, err = capsys.readouterr()
assert out == f"{path} is non-utf-8 (not supported)\n"
assert err == ""
def test_migrate_stdin_empty(self, capsys):
stdin = io.TextIOWrapper(io.BytesIO(b""), "UTF-8")
with mock.patch.object(sys, "stdin", stdin):
result = main(["migrate", "-"])
assert result == 0
out, err = capsys.readouterr()
assert out == ""
assert err == ""
def test_migrate_import(self, capsys, tmp_path):
path = tmp_path / "example.py"
path.write_text("import freezegun\n")
result = main(["migrate", str(path)])
assert result == 1
out, err = capsys.readouterr()
assert out == ""
assert err == f"Rewriting {path}\n"
assert path.read_text() == "import time_machine\n"
def test_migrate_stdin_import(self, capsys):
stdin = io.TextIOWrapper(io.BytesIO(b"import freezegun\n"), "UTF-8")
with mock.patch.object(sys, "stdin", stdin):
result = main(["migrate", "-"])
assert result == 1
out, err = capsys.readouterr()
assert out == "import time_machine\n"
assert err == ""
def check_noop(given: str) -> None:
given = dedent(given)
result = migrate_contents(given)
assert result == given
def check_transformed(given: str, expected: str) -> None:
given = dedent(given)
expected = dedent(expected)
result = migrate_contents(given)
assert result == expected
class TestMigrateContents:
def test_import_unrelated(self):
check_noop(
"import libfaketime",
)
def test_aliased(self):
check_noop(
"import freezegun as fg",
)
def test_import_freezegun(self):
check_transformed(
"import freezegun",
"import time_machine",
)
def test_import_from_unrelated(self):
check_noop(
"from libfaketime import freeze_time",
)
def test_import_from_freezegun_aliased(self):
check_noop(
"from freezegun import freeze_time as ft",
)
def test_import_from_freezegun_multiple(self):
check_noop(
"from freezegun import freeze_time, FakeDate",
)
def test_import_from_freezegun(self):
check_transformed(
"from freezegun import freeze_time",
"import time_machine",
)
def test_import_from_freezegun_more(self):
check_transformed(
"""
from freezegun import freeze_time
pass
""",
"""
import time_machine
pass
""",
)
def test_function_decorator_attr_unrelated(self):
check_noop(
"""
import libfaketime
@libfaketime.freeze_time("2023-01-01")
def test_function():
pass
""",
)
def test_function_decorator_attr_not_called(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time
def test_function():
pass
""",
"""
import time_machine
@freezegun.freeze_time
def test_function():
pass
""",
)
def test_function_decorator_attr(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time("2023-01-01")
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_function_decorator_name_unrelated(self):
check_noop(
"""
from libfaketime import freeze_time
@freeze_time("2023-01-01")
def test_function():
pass
""",
)
def test_function_decorator_name_not_called(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time
def test_function():
pass
""",
"""
import time_machine
@freeze_time
def test_function():
pass
""",
)
def test_function_decorator_name(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01")
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_class_decorator_attr_unrelated(self):
check_noop(
"""
import libfaketime
@libfaketime.freeze_time("2023-01-01")
class TestClass:
pass
""",
)
def test_class_decorator_attr_not_called(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time
class TestClass:
pass
""",
"""
import time_machine
@freezegun.freeze_time
class TestClass:
pass
""",
)
def test_class_decorator_attr_not_unittest_class(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time("2023-01-01")
class TestClass:
pass
""",
"""
import time_machine
@freezegun.freeze_time("2023-01-01")
class TestClass:
pass
""",
)
def test_class_decorator_attr_unittest_class_base_name(self):
check_transformed(
"""
import freezegun
from django.test import SimpleTestCase
@freezegun.freeze_time("2023-01-01")
class TestClass(SimpleTestCase):
pass
""",
"""
import time_machine
from django.test import SimpleTestCase
@time_machine.travel("2023-01-01", tick=False)
class TestClass(SimpleTestCase):
pass
""",
)
def test_class_decorator_attr_unittest_class_base_attr(self):
check_transformed(
"""
import freezegun
import unittest
@freezegun.freeze_time("2023-01-01")
class TestClass(unittest.TestCase):
pass
""",
"""
import time_machine
import unittest
@time_machine.travel("2023-01-01", tick=False)
class TestClass(unittest.TestCase):
pass
""",
)
def test_class_decorator_attr_unittest_class_method(self):
check_transformed(
"""
import freezegun
from testing import TestBase
@freezegun.freeze_time("2023-01-01")
class TestClass(TestBase):
def setUp(self):
print("I look like a unittest class!")
""",
"""
import time_machine
from testing import TestBase
@time_machine.travel("2023-01-01", tick=False)
class TestClass(TestBase):
def setUp(self):
print("I look like a unittest class!")
""",
)
def test_class_decorator_attr_unittest_class_async_method(self):
check_transformed(
"""
import freezegun
from testing import TestBase
@freezegun.freeze_time("2023-01-01")
class TestClass(TestBase):
async def asyncSetUp(self):
print("I look like a unittest class!")
""",
"""
import time_machine
from testing import TestBase
@time_machine.travel("2023-01-01", tick=False)
class TestClass(TestBase):
async def asyncSetUp(self):
print("I look like a unittest class!")
""",
)
def test_class_decorator_attr_multiple(self):
check_transformed(
"""
import freezegun
from testing import TestBase
from unittest import mock
@freezegun.freeze_time("2023-01-01")
@mock.patch("example.connect")
class TestClass(TestBase):
def setUp(self):
print("I look like a unittest class!")
""",
"""
import time_machine
from testing import TestBase
from unittest import mock
@time_machine.travel("2023-01-01", tick=False)
@mock.patch("example.connect")
class TestClass(TestBase):
def setUp(self):
print("I look like a unittest class!")
""",
)
def test_class_decorator_name_unrelated(self):
check_noop(
"""
from libfaketime import freeze_time
@freeze_time("2023-01-01")
class TestClass:
pass
""",
)
def test_class_decorator_name_not_called(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time
class TestClass:
pass
""",
"""
import time_machine
@freeze_time
class TestClass:
pass
""",
)
def test_class_decorator_name_not_unittest_class(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01")
class TestClass:
pass
""",
"""
import time_machine
@freeze_time("2023-01-01")
class TestClass:
pass
""",
)
def test_class_decorator_name_unittest_class_base_name(self):
check_transformed(
"""
from freezegun import freeze_time
from django.test import SimpleTestCase
@freeze_time("2023-01-01")
class TestClass(SimpleTestCase):
pass
""",
"""
import time_machine
from django.test import SimpleTestCase
@time_machine.travel("2023-01-01", tick=False)
class TestClass(SimpleTestCase):
pass
""",
)
def test_class_decorator_name_unittest_class_base_attr(self):
check_transformed(
"""
from freezegun import freeze_time
import unittest
@freeze_time("2023-01-01")
class TestClass(unittest.TestCase):
pass
""",
"""
import time_machine
import unittest
@time_machine.travel("2023-01-01", tick=False)
class TestClass(unittest.TestCase):
pass
""",
)
def test_class_decorator_name_unittest_class_method(self):
check_transformed(
"""
from freezegun import freeze_time
from testing import TestBase
@freeze_time("2023-01-01")
class TestClass(TestBase):
def setUp(self):
print("I look like a unittest class!")
""",
"""
import time_machine
from testing import TestBase
@time_machine.travel("2023-01-01", tick=False)
class TestClass(TestBase):
def setUp(self):
print("I look like a unittest class!")
""",
)
def test_class_decorator_name_unittest_class_uses_assert_method(self):
check_transformed(
"""
from freezegun import freeze_time
from testing import TestBase
@freeze_time("2023-01-01")
class TestClass(TestBase):
def test_something(self):
self.assertTrue(True)
""",
"""
import time_machine
from testing import TestBase
@time_machine.travel("2023-01-01", tick=False)
class TestClass(TestBase):
def test_something(self):
self.assertTrue(True)
""",
)
def test_with_attr_unrelated(self):
check_noop(
"""
import libfaketime
with libfaketime.freeze_time("2023-01-01"):
pass
""",
)
def test_with_attr_not_called(self):
check_transformed(
"""
import freezegun
with freezegun.freeze_time:
pass
""",
"""
import time_machine
with freezegun.freeze_time:
pass
""",
)
def test_with_attr_as(self):
check_transformed(
"""
import freezegun
with freezegun.freeze_time("2023-01-01") as ft:
pass
""",
"""
import time_machine
with freezegun.freeze_time("2023-01-01") as ft:
pass
""",
)
def test_with_attr(self):
check_transformed(
"""
import freezegun
with freezegun.freeze_time("2023-01-01"):
pass
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False):
pass
""",
)
def test_with_name_unrelated(self):
check_noop(
"""
from libfaketime import freeze_time
with freeze_time("2023-01-01"):
pass
""",
)
def test_with_name_not_called(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time:
pass
""",
"""
import time_machine
with freeze_time:
pass
""",
)
def test_with_name_as(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
pass
""",
"""
import time_machine
with freeze_time("2023-01-01") as ft:
pass
""",
)
def test_with_name(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01"):
pass
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False):
pass
""",
)
adamchainz-time-machine-591fa85/tests/test_time_machine.py 0000664 0000000 0000000 00000101046 15051127025 0023710 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import asyncio
import datetime as dt
import os
import subprocess
import sys
import time
import typing
import uuid
from contextlib import contextmanager
from importlib.util import module_from_spec, spec_from_file_location
from textwrap import dedent
from unittest import SkipTest, TestCase, mock
from zoneinfo import ZoneInfo
import pytest
from dateutil import tz
import time_machine
NANOSECONDS_PER_SECOND = time_machine.NANOSECONDS_PER_SECOND
EPOCH_DATETIME = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc)
EPOCH = EPOCH_DATETIME.timestamp()
EPOCH_PLUS_ONE_YEAR_DATETIME = dt.datetime(1971, 1, 1, tzinfo=dt.timezone.utc)
EPOCH_PLUS_ONE_YEAR = EPOCH_PLUS_ONE_YEAR_DATETIME.timestamp()
LIBRARY_EPOCH_DATETIME = dt.datetime(2020, 4, 29) # The day this library was made
LIBRARY_EPOCH = LIBRARY_EPOCH_DATETIME.timestamp()
py_have_clock_gettime = pytest.mark.skipif(
not hasattr(time, "clock_gettime"), reason="Doesn't have clock_gettime"
)
def sleep_one_cycle(clock: int) -> None:
time.sleep(time.clock_getres(clock))
@contextmanager
def change_local_timezone(local_tz: str | None) -> typing.Iterator[None]:
orig_tz = os.environ["TZ"]
if local_tz:
os.environ["TZ"] = local_tz
else:
del os.environ["TZ"]
time.tzset()
try:
yield
finally:
os.environ["TZ"] = orig_tz
time.tzset()
@pytest.mark.skipif(
not hasattr(time, "CLOCK_REALTIME"), reason="No time.CLOCK_REALTIME"
)
def test_import_without_clock_realtime():
orig = time.CLOCK_REALTIME
del time.CLOCK_REALTIME
try:
# Recipe for importing from path as documented in importlib
spec = spec_from_file_location(
f"{__name__}.time_machine_without_clock_realtime", time_machine.__file__
)
assert spec is not None
module = module_from_spec(spec)
# typeshed says exec_module does not always exist:
spec.loader.exec_module(module) # type: ignore[union-attr]
finally:
time.CLOCK_REALTIME = orig
# No assertions - testing for coverage only
# datetime module
def test_datetime_now_no_args():
with time_machine.travel(EPOCH):
now = dt.datetime.now()
assert now.year == 1970
assert now.month == 1
assert now.day == 1
# Not asserting on hour/minute because local timezone could shift it
assert now.second == 0
assert now.microsecond == 0
assert dt.datetime.now() >= LIBRARY_EPOCH_DATETIME
def test_datetime_now_no_args_no_tick():
with time_machine.travel(EPOCH, tick=False):
now = dt.datetime.now()
assert now.microsecond == 0
assert dt.datetime.now() >= LIBRARY_EPOCH_DATETIME
def test_datetime_now_arg():
with time_machine.travel(EPOCH):
now = dt.datetime.now(tz=dt.timezone.utc)
assert now.year == 1970
assert now.month == 1
assert now.day == 1
assert dt.datetime.now(dt.timezone.utc) >= LIBRARY_EPOCH_DATETIME.replace(
tzinfo=dt.timezone.utc
)
def test_datetime_utcnow():
with time_machine.travel(EPOCH):
now = dt.datetime.utcnow()
assert now.year == 1970
assert now.month == 1
assert now.day == 1
assert now.hour == 0
assert now.minute == 0
assert now.second == 0
assert now.microsecond == 0
assert now.tzinfo is None
assert dt.datetime.utcnow() >= LIBRARY_EPOCH_DATETIME
def test_datetime_utcnow_no_tick():
with time_machine.travel(EPOCH, tick=False):
now = dt.datetime.utcnow()
assert now.microsecond == 0
def test_date_today():
with time_machine.travel(EPOCH):
today = dt.date.today()
assert today.year == 1970
assert today.month == 1
assert today.day == 1
assert dt.datetime.today() >= LIBRARY_EPOCH_DATETIME
# time module
@py_have_clock_gettime
def test_time_clock_gettime_realtime():
with time_machine.travel(EPOCH + 180.0):
now = time.clock_gettime(time.CLOCK_REALTIME)
assert isinstance(now, float)
assert now == EPOCH + 180.0
now = time.clock_gettime(time.CLOCK_REALTIME)
assert isinstance(now, float)
assert now >= LIBRARY_EPOCH
@py_have_clock_gettime
def test_time_clock_gettime_monotonic_unaffected():
start = time.clock_gettime(time.CLOCK_MONOTONIC)
sleep_one_cycle(time.CLOCK_MONOTONIC)
with time_machine.travel(EPOCH + 180.0):
frozen = time.clock_gettime(time.CLOCK_MONOTONIC)
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert isinstance(frozen, float)
assert frozen > start
now = time.clock_gettime(time.CLOCK_MONOTONIC)
assert isinstance(now, float)
assert now > frozen
@py_have_clock_gettime
def test_time_clock_gettime_ns_realtime():
with time_machine.travel(EPOCH + 190.0):
first = time.clock_gettime_ns(time.CLOCK_REALTIME)
sleep_one_cycle(time.CLOCK_REALTIME)
assert isinstance(first, int)
assert first == int((EPOCH + 190.0) * NANOSECONDS_PER_SECOND)
second = time.clock_gettime_ns(time.CLOCK_REALTIME)
assert first < second < int((EPOCH + 191.0) * NANOSECONDS_PER_SECOND)
now = time.clock_gettime_ns(time.CLOCK_REALTIME)
assert isinstance(now, int)
assert now >= int(LIBRARY_EPOCH * NANOSECONDS_PER_SECOND)
@py_have_clock_gettime
def test_time_clock_gettime_ns_monotonic_unaffected():
start = time.clock_gettime_ns(time.CLOCK_MONOTONIC)
sleep_one_cycle(time.CLOCK_MONOTONIC)
with time_machine.travel(EPOCH + 190.0):
frozen = time.clock_gettime_ns(time.CLOCK_MONOTONIC)
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert isinstance(frozen, int)
assert frozen > start
now = time.clock_gettime_ns(time.CLOCK_MONOTONIC)
assert isinstance(now, int)
assert now > frozen
def test_time_gmtime_no_args():
with time_machine.travel(EPOCH):
local_time = time.gmtime()
assert local_time.tm_year == 1970
assert local_time.tm_mon == 1
assert local_time.tm_mday == 1
now_time = time.gmtime()
assert now_time.tm_year >= 2020
def test_time_gmtime_no_args_no_tick():
with time_machine.travel(EPOCH, tick=False):
local_time = time.gmtime()
assert local_time.tm_sec == 0
def test_time_gmtime_arg():
with time_machine.travel(EPOCH):
local_time = time.gmtime(EPOCH_PLUS_ONE_YEAR)
assert local_time.tm_year == 1971
assert local_time.tm_mon == 1
assert local_time.tm_mday == 1
def test_time_localtime():
with time_machine.travel(EPOCH):
local_time = time.localtime()
assert local_time.tm_year == 1970
assert local_time.tm_mon == 1
assert local_time.tm_mday == 1
now_time = time.localtime()
assert now_time.tm_year >= 2020
def test_time_localtime_no_tick():
with time_machine.travel(EPOCH, tick=False):
local_time = time.localtime()
assert local_time.tm_sec == 0
def test_time_localtime_arg():
with time_machine.travel(EPOCH):
local_time = time.localtime(EPOCH_PLUS_ONE_YEAR)
assert local_time.tm_year == 1971
assert local_time.tm_mon == 1
assert local_time.tm_mday == 1
def test_time_montonic():
with time_machine.travel(EPOCH, tick=False) as t:
assert time.monotonic() == EPOCH
t.shift(1)
assert time.monotonic() == EPOCH + 1
def test_time_monotonic_ns():
with time_machine.travel(EPOCH, tick=False) as t:
assert time.monotonic_ns() == int(EPOCH * NANOSECONDS_PER_SECOND)
t.shift(1)
assert (
time.monotonic_ns()
== int(EPOCH * NANOSECONDS_PER_SECOND) + NANOSECONDS_PER_SECOND
)
def test_time_strftime_format():
with time_machine.travel(EPOCH):
assert time.strftime("%Y-%m-%d") == "1970-01-01"
assert int(time.strftime("%Y")) >= 2020
def test_time_strftime_format_no_tick():
with time_machine.travel(EPOCH, tick=False):
assert time.strftime("%S") == "00"
def test_time_strftime_format_t():
with time_machine.travel(EPOCH):
assert (
time.strftime("%Y-%m-%d", time.localtime(EPOCH_PLUS_ONE_YEAR))
== "1971-01-01"
)
def test_time_time():
with time_machine.travel(EPOCH):
first = time.time()
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert isinstance(first, float)
assert first == EPOCH
second = time.time()
assert first < second < EPOCH + 1.0
now = time.time()
assert isinstance(now, float)
assert now >= LIBRARY_EPOCH
windows_epoch_in_posix = -11_644_445_222
@mock.patch.object(
time_machine,
"SYSTEM_EPOCH_TIMESTAMP_NS",
(windows_epoch_in_posix * NANOSECONDS_PER_SECOND),
)
def test_time_time_windows():
with time_machine.travel(EPOCH):
first = time.time()
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert isinstance(first, float)
assert first == windows_epoch_in_posix
second = time.time()
assert isinstance(second, float)
assert windows_epoch_in_posix < second < windows_epoch_in_posix + 1.0
def test_time_time_no_tick():
with time_machine.travel(EPOCH, tick=False):
assert time.time() == EPOCH
def test_time_time_ns():
with time_machine.travel(EPOCH + 150.0):
first = time.time_ns()
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert isinstance(first, int)
assert first == int((EPOCH + 150.0) * NANOSECONDS_PER_SECOND)
second = time.time_ns()
assert first < second < int((EPOCH + 151.0) * NANOSECONDS_PER_SECOND)
now = time.time_ns()
assert isinstance(now, int)
assert now >= int(LIBRARY_EPOCH * NANOSECONDS_PER_SECOND)
def test_time_time_ns_no_tick():
with time_machine.travel(EPOCH, tick=False):
assert time.time_ns() == int(EPOCH * NANOSECONDS_PER_SECOND)
# all supported forms
def test_nestable():
with time_machine.travel(EPOCH + 55.0):
assert time.time() == EPOCH + 55.0
with time_machine.travel(EPOCH + 50.0):
assert time.time() == EPOCH + 50.0
def test_unsupported_type():
with pytest.raises(TypeError) as excinfo, time_machine.travel([]): # type: ignore[arg-type]
pass # pragma: no cover
assert excinfo.value.args == ("Unsupported destination []",)
def test_exceptions_dont_break_it():
with pytest.raises(ValueError), time_machine.travel(0.0):
raise ValueError("Hi")
# Unreachable code analysis doesn’t work with raises being caught by
# context manager
with time_machine.travel(0.0):
pass
@time_machine.travel(EPOCH_DATETIME + dt.timedelta(seconds=70))
def test_destination_datetime():
assert time.time() == EPOCH + 70.0
@time_machine.travel(EPOCH_DATETIME.replace(tzinfo=tz.gettz("America/Chicago")))
def test_destination_datetime_tzinfo_non_zoneinfo():
assert time.time() == EPOCH + 21600.0
def test_destination_datetime_tzinfo_zoneinfo():
orig_timezone = time.timezone
orig_altzone = time.altzone
orig_tzname = time.tzname
orig_daylight = time.daylight
dest = LIBRARY_EPOCH_DATETIME.replace(tzinfo=ZoneInfo("Africa/Addis_Ababa"))
with time_machine.travel(dest):
assert time.timezone == -3 * 3600
assert time.altzone == -3 * 3600
assert time.tzname == ("EAT", "EAT")
assert time.daylight == 0
assert time.localtime() == time.struct_time(
(
2020,
4,
29,
0,
0,
0,
2,
120,
0,
)
)
assert time.timezone == orig_timezone
assert time.altzone == orig_altzone
assert time.tzname == orig_tzname
assert time.daylight == orig_daylight
def test_destination_datetime_tzinfo_zoneinfo_nested():
orig_tzname = time.tzname
dest = LIBRARY_EPOCH_DATETIME.replace(tzinfo=ZoneInfo("Africa/Addis_Ababa"))
with time_machine.travel(dest):
assert time.tzname == ("EAT", "EAT")
dest2 = LIBRARY_EPOCH_DATETIME.replace(tzinfo=ZoneInfo("Pacific/Auckland"))
with time_machine.travel(dest2):
assert time.tzname == ("NZST", "NZDT")
assert time.tzname == ("EAT", "EAT")
assert time.tzname == orig_tzname
def test_destination_datetime_tzinfo_zoneinfo_no_orig_tz():
with change_local_timezone(None):
orig_tzname = time.tzname
dest = LIBRARY_EPOCH_DATETIME.replace(tzinfo=ZoneInfo("Africa/Addis_Ababa"))
with time_machine.travel(dest):
assert time.tzname == ("EAT", "EAT")
assert time.tzname == orig_tzname
def test_destination_datetime_tzinfo_zoneinfo_utc_no_orig_tz():
with change_local_timezone(None):
orig_tzname = time.tzname
dest = LIBRARY_EPOCH_DATETIME.replace(tzinfo=ZoneInfo("UTC"))
with time_machine.travel(dest):
assert time.tzname == ("UTC", "UTC")
assert time.tzname == orig_tzname
def test_destination_datetime_tzinfo_datetime_timezone_utc_no_orig_tz():
with change_local_timezone(None):
orig_tzname = time.tzname
dest = LIBRARY_EPOCH_DATETIME.replace(tzinfo=dt.timezone.utc)
with time_machine.travel(dest):
assert time.tzname == ("UTC", "UTC")
assert time.tzname == orig_tzname
@pytest.mark.skipif(
sys.version_info < (3, 11), reason="datetime.UTC was introduced in Python 3.11"
)
def test_destination_datetime_tzinfo_datetime_utc_no_orig_tz():
with change_local_timezone(None):
orig_tzname = time.tzname
dest = LIBRARY_EPOCH_DATETIME.replace(tzinfo=dt.UTC)
with time_machine.travel(dest):
assert time.tzname == ("UTC", "UTC")
assert time.tzname == orig_tzname
def test_destination_datetime_tzinfo_zoneinfo_windows():
orig_timezone = time.timezone
pretend_windows_no_tzset = mock.patch.object(time_machine, "tzset", new=None)
mock_have_tzset_false = mock.patch.object(time_machine, "HAVE_TZSET", new=False)
dest = LIBRARY_EPOCH_DATETIME.replace(tzinfo=ZoneInfo("Africa/Addis_Ababa"))
with pretend_windows_no_tzset, mock_have_tzset_false, time_machine.travel(dest):
assert time.timezone == orig_timezone
@time_machine.travel(int(EPOCH + 77))
def test_destination_int():
assert time.time() == int(EPOCH + 77)
@time_machine.travel(EPOCH_DATETIME.replace(tzinfo=None) + dt.timedelta(seconds=120))
def test_destination_datetime_naive():
assert time.time() == EPOCH + 120.0
@time_machine.travel(EPOCH_DATETIME.date())
def test_destination_date():
assert time.time() == EPOCH
def test_destination_timedelta():
now = time.time()
with time_machine.travel(dt.timedelta(seconds=3600)):
assert now + 3600 <= time.time() <= now + 3601
def test_destination_timedelta_first_travel_in_process():
# Would previously segfault
subprocess.run(
[
sys.executable,
"-c",
dedent(
"""
from datetime import timedelta
import time_machine
with time_machine.travel(timedelta()):
pass
"""
),
],
check=True,
)
def test_destination_timedelta_negative():
now = time.time()
with time_machine.travel(dt.timedelta(seconds=-3600)):
assert now - 3600 <= time.time() <= now - 3599
def test_destination_timedelta_nested():
with time_machine.travel(EPOCH), time_machine.travel(dt.timedelta(seconds=10)):
assert time.time() == EPOCH + 10.0
@time_machine.travel("1970-01-01 00:01 +0000")
def test_destination_string():
assert time.time() == EPOCH + 60.0
@pytest.mark.parametrize(
["local_tz", "expected_offset"],
[
("UTC", 0),
("Europe/Amsterdam", -3600),
("US/Eastern", 5 * 3600),
],
)
@pytest.mark.parametrize("destination", ["1970-01-01 00:00", "1970-01-01"])
def test_destination_string_naive(local_tz, expected_offset, destination):
with change_local_timezone(local_tz), time_machine.travel(destination):
assert time.time() == EPOCH + expected_offset
@time_machine.travel(lambda: EPOCH + 140.0)
def test_destination_callable_lambda_float():
assert time.time() == EPOCH + 140.0
@time_machine.travel(lambda: "1970-01-01 00:02 +0000")
def test_destination_callable_lambda_string():
assert time.time() == EPOCH + 120.0
@time_machine.travel(EPOCH + 13.0 for _ in range(1)) # pragma: no branch
def test_destination_generator():
assert time.time() == EPOCH + 13.0
def test_traveller_object():
traveller = time_machine.travel(EPOCH + 10.0)
assert time.time() >= LIBRARY_EPOCH
try:
traveller.start()
assert time.time() == EPOCH + 10.0
finally:
traveller.stop()
assert time.time() >= LIBRARY_EPOCH
@time_machine.travel(EPOCH + 15.0)
def test_function_decorator():
assert time.time() == EPOCH + 15.0
def test_coroutine_decorator():
recorded_time = None
@time_machine.travel(EPOCH + 140.0)
async def record_time() -> None:
nonlocal recorded_time
recorded_time = time.time()
asyncio.run(record_time())
assert recorded_time == EPOCH + 140.0
def test_async_context_manager():
recorded_time = None
async def record_time() -> None:
nonlocal recorded_time
async with time_machine.travel(EPOCH + 150.0):
recorded_time = time.time()
asyncio.run(record_time())
assert recorded_time == EPOCH + 150.0
def test_async_context_manager_stops_properly():
recorded_times = []
async def record_times() -> None:
recorded_times.append(time.time())
async with time_machine.travel(EPOCH + 160.0):
recorded_times.append(time.time())
recorded_times.append(time.time())
asyncio.run(record_times())
assert recorded_times[0] >= LIBRARY_EPOCH
assert recorded_times[1] == EPOCH + 160.0
assert recorded_times[2] >= LIBRARY_EPOCH
def test_async_context_manager_traveller():
recorded_time = None
shifted_time = None
async def test_traveller() -> None:
nonlocal recorded_time, shifted_time
async with time_machine.travel(EPOCH + 170.0, tick=False) as traveller:
recorded_time = time.time()
traveller.shift(10.0)
shifted_time = time.time()
asyncio.run(test_traveller())
assert recorded_time == EPOCH + 170.0
assert shifted_time == EPOCH + 180.0
def test_class_decorator_fails_non_testcase():
with pytest.raises(TypeError) as excinfo:
@time_machine.travel(EPOCH)
class Something:
pass
assert excinfo.value.args == ("Can only decorate unittest.TestCase subclasses.",)
@time_machine.travel(EPOCH)
class ClassDecoratorInheritanceBase(TestCase):
prop: bool
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.setUpTestData()
@classmethod
def setUpTestData(cls) -> None:
cls.prop = True
class ClassDecoratorInheritanceTests(ClassDecoratorInheritanceBase):
@classmethod
def setUpTestData(cls) -> None:
super().setUpTestData()
cls.prop = False
def test_ineheritance_correctly_rebound(self):
assert self.prop is False
class TestMethodDecorator:
@time_machine.travel(EPOCH + 95.0)
def test_method_decorator(self):
assert time.time() == EPOCH + 95.0
class UnitTestMethodTests(TestCase):
@time_machine.travel(EPOCH + 25.0)
def test_method_decorator(self):
assert time.time() == EPOCH + 25.0
@time_machine.travel(EPOCH + 95.0)
class UnitTestClassTests(TestCase):
def test_class_decorator(self):
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert EPOCH + 95.0 < time.time() < EPOCH + 96.0
@time_machine.travel(EPOCH + 25.0)
def test_stacked_method_decorator(self):
assert time.time() == EPOCH + 25.0
@time_machine.travel(EPOCH + 95.0)
class UnitTestClassCustomSetUpClassTests(TestCase):
custom_setupclass_ran: bool
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.custom_setupclass_ran = True
def test_class_decorator(self):
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert EPOCH + 95.0 < time.time() < EPOCH + 96.0
assert self.custom_setupclass_ran
@time_machine.travel(EPOCH + 110.0)
class UnitTestClassSetUpClassSkipTests(TestCase):
@classmethod
def setUpClass(cls):
raise SkipTest("Not today")
# Other tests would fail if the travel() wasn't stopped
def test_thats_always_skipped(self): # pragma: no cover
pass
# shift() tests
def test_shift_with_timedelta():
with time_machine.travel(EPOCH, tick=False) as traveller:
traveller.shift(dt.timedelta(days=1))
assert time.time() == EPOCH + (3600.0 * 24)
def test_shift_integer_delta():
with time_machine.travel(EPOCH, tick=False) as traveller:
traveller.shift(10)
assert time.time() == EPOCH + 10
def test_shift_negative_delta():
with time_machine.travel(EPOCH, tick=False) as traveller:
traveller.shift(-10)
assert time.time() == EPOCH - 10
def test_shift_wrong_delta():
with (
time_machine.travel(EPOCH, tick=False) as traveller,
pytest.raises(TypeError) as excinfo,
):
traveller.shift(delta="1.1") # type: ignore[arg-type]
assert excinfo.value.args == ("Unsupported type for delta argument: '1.1'",)
def test_shift_when_tick():
with time_machine.travel(EPOCH, tick=True) as traveller:
traveller.shift(10.0)
assert EPOCH + 10.0 <= time.time() < EPOCH + 20.0
# move_to() tests
def test_move_to_datetime():
with time_machine.travel(EPOCH) as traveller:
assert time.time() == EPOCH
traveller.move_to(EPOCH_PLUS_ONE_YEAR_DATETIME)
first = time.time()
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert first == EPOCH_PLUS_ONE_YEAR
second = time.time()
assert first < second < first + 1.0
def test_move_to_datetime_no_tick():
with time_machine.travel(EPOCH, tick=False) as traveller:
traveller.move_to(EPOCH_PLUS_ONE_YEAR_DATETIME)
assert time.time() == EPOCH_PLUS_ONE_YEAR
assert time.time() == EPOCH_PLUS_ONE_YEAR
def test_move_to_past_datetime():
with time_machine.travel(EPOCH_PLUS_ONE_YEAR) as traveller:
assert time.time() == EPOCH_PLUS_ONE_YEAR
traveller.move_to(EPOCH_DATETIME)
assert time.time() == EPOCH
def test_move_to_datetime_with_tzinfo_zoneinfo():
orig_timezone = time.timezone
orig_altzone = time.altzone
orig_tzname = time.tzname
orig_daylight = time.daylight
with time_machine.travel(EPOCH) as traveller:
assert time.time() == EPOCH
assert time.timezone == orig_timezone
assert time.altzone == orig_altzone
assert time.tzname == orig_tzname
assert time.daylight == orig_daylight
dest = EPOCH_PLUS_ONE_YEAR_DATETIME.replace(
tzinfo=ZoneInfo("Africa/Addis_Ababa")
)
traveller.move_to(dest)
assert time.timezone == -3 * 3600
assert time.altzone == -3 * 3600
assert time.tzname == ("EAT", "EAT")
assert time.daylight == 0
assert time.localtime() == time.struct_time(
(
1971,
1,
1,
0,
0,
0,
4,
1,
0,
)
)
assert time.timezone == orig_timezone
assert time.altzone == orig_altzone
assert time.tzname == orig_tzname
assert time.daylight == orig_daylight
def test_move_to_datetime_change_tick_on():
with time_machine.travel(EPOCH, tick=False) as traveller:
traveller.move_to(EPOCH_PLUS_ONE_YEAR_DATETIME, tick=True)
assert time.time() == EPOCH_PLUS_ONE_YEAR
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert time.time() > EPOCH_PLUS_ONE_YEAR
def test_move_to_datetime_change_tick_off():
with time_machine.travel(EPOCH, tick=True) as traveller:
traveller.move_to(EPOCH_PLUS_ONE_YEAR_DATETIME, tick=False)
assert time.time() == EPOCH_PLUS_ONE_YEAR
assert time.time() == EPOCH_PLUS_ONE_YEAR
# uuid tests
def time_from_uuid1(value: uuid.UUID) -> dt.datetime:
return dt.datetime(1582, 10, 15) + dt.timedelta(microseconds=value.time // 10)
def test_uuid1():
"""
Test that the uuid.uuid1() methods generate values for the destination.
They are a known location in the stdlib that can make system calls to find
the current datetime.
"""
destination = dt.datetime(2017, 2, 6, 14, 8, 21)
with time_machine.travel(destination, tick=False):
assert time_from_uuid1(uuid.uuid1()) == destination
# error handling tests
def test_c_extension_init_import_error():
code = dedent(
"""\
import sys
sys.modules["datetime"] = None
try:
import _time_machine
except ImportError as exc:
print(exc.args[0])
"""
)
result = subprocess.run(
[sys.executable, "-c", code],
check=True,
stdout=subprocess.PIPE,
text=True,
)
assert result.stdout == "import of datetime halted; None in sys.modules\n"
@pytest.mark.parametrize(
"func, args",
[
(dt.datetime.now, ()),
(dt.datetime.utcnow, ()),
(time.gmtime, ()),
(time.clock_gettime, (time.CLOCK_REALTIME,)),
(time.clock_gettime_ns, (time.CLOCK_REALTIME,)),
(time.localtime, ()),
(time.strftime, ("%Y-%m-%d",)),
(time.time, ()),
(time.time_ns, ()),
],
)
def test_time_machine_import_error(func, args):
with (
time_machine.travel(EPOCH),
mock.patch.dict(sys.modules, {"time_machine": None}),
pytest.raises(ModuleNotFoundError) as excinfo,
):
func(*args)
assert excinfo.value.args == ("import of time_machine halted; None in sys.modules",)
@pytest.mark.parametrize(
"func, args",
[
(dt.datetime.now, ()),
(dt.datetime.utcnow, ()),
(time.gmtime, ()),
(time.clock_gettime, (time.CLOCK_REALTIME,)),
(time.clock_gettime_ns, (time.CLOCK_REALTIME,)),
(time.localtime, ()),
(time.strftime, ("%Y-%m-%d",)),
(time.time, ()),
(time.time_ns, ()),
],
)
def test_time_machine_attribute_error(func, args):
with (
time_machine.travel(EPOCH),
mock.patch.dict(sys.modules, {"time_machine": ()}),
pytest.raises(AttributeError) as excinfo,
):
func(*args)
assert excinfo.value.args == (f"'tuple' object has no attribute '{func.__name__}'",)
# pytest plugin tests
def test_fixture_unused(time_machine):
assert time.time() >= LIBRARY_EPOCH
def test_fixture_used(time_machine):
time_machine.move_to(EPOCH)
assert time.time() == EPOCH
def test_fixture_used_tick_false(time_machine):
time_machine.move_to(EPOCH, tick=False)
assert time.time() == EPOCH
assert time.time() == EPOCH
def test_fixture_used_tick_true(time_machine):
time_machine.move_to(EPOCH, tick=True)
original = time.time()
sleep_one_cycle(time.CLOCK_MONOTONIC)
assert original == EPOCH
assert original < time.time() < EPOCH + 10.0
def test_fixture_move_to_twice(time_machine):
time_machine.move_to(EPOCH)
assert time.time() == EPOCH
time_machine.move_to(EPOCH_PLUS_ONE_YEAR)
assert time.time() == EPOCH_PLUS_ONE_YEAR
def test_fixture_move_to_and_shift(time_machine):
time_machine.move_to(EPOCH, tick=False)
assert time.time() == EPOCH
time_machine.shift(100)
assert time.time() == EPOCH + 100
def test_fixture_shift_without_move_to(time_machine):
with pytest.raises(RuntimeError) as excinfo:
time_machine.shift(100)
assert excinfo.value.args == (
"Initialize time_machine with move_to() before using shift().",
)
def test_marker_function(testdir):
testdir.makepyfile(
"""
import pytest
import time
@pytest.fixture
def current_time():
return time.time()
@pytest.mark.time_machine(0)
def test(current_time):
assert current_time < 10.0
"""
)
result = testdir.runpytest("-v", "-s")
result.assert_outcomes(passed=1)
def test_marker_and_fixture(testdir):
testdir.makepyfile(
"""
import pytest
import time
@pytest.mark.time_machine(0)
def test(time_machine):
assert time.time() < 10.0
time_machine.shift(100)
assert 100.0 <= time.time() < 110.0
time_machine.move_to(0)
assert time.time() < 10.0
"""
)
result = testdir.runpytest("-v", "-s")
result.assert_outcomes(passed=1)
def test_marker_class(testdir):
testdir.makepyfile(
"""
import pytest
import time
@pytest.mark.time_machine(0)
class TestTimeMachine:
def test(self):
assert time.time() < 10.0
"""
)
result = testdir.runpytest("-v", "-s")
result.assert_outcomes(passed=1)
def test_marker_module(testdir):
testdir.makepyfile(
"""
import pytest
import time
pytestmark = pytest.mark.time_machine(0)
def test_module():
assert time.time() < 10.0
"""
)
result = testdir.runpytest("-v", "-s")
result.assert_outcomes(passed=1)
# escape hatch tests
class TestEscapeHatch:
def test_is_travelling_false(self):
assert time_machine.escape_hatch.is_travelling() is False
def test_is_travelling_true(self):
with time_machine.travel(EPOCH):
assert time_machine.escape_hatch.is_travelling() is True
def test_datetime_now(self):
real_now = dt.datetime.now()
with time_machine.travel(EPOCH):
eh_now = time_machine.escape_hatch.datetime.datetime.now()
assert eh_now >= real_now
def test_datetime_now_tz(self):
real_now = dt.datetime.now(tz=dt.timezone.utc)
with time_machine.travel(EPOCH):
eh_now = time_machine.escape_hatch.datetime.datetime.now(tz=dt.timezone.utc)
assert eh_now >= real_now
def test_datetime_utcnow(self):
real_now = dt.datetime.utcnow()
with time_machine.travel(EPOCH):
eh_now = time_machine.escape_hatch.datetime.datetime.utcnow()
assert eh_now >= real_now
@py_have_clock_gettime
def test_time_clock_gettime(self):
now = time.clock_gettime(time.CLOCK_REALTIME)
with time_machine.travel(EPOCH + 180.0):
eh_now = time_machine.escape_hatch.time.clock_gettime(time.CLOCK_REALTIME)
assert eh_now >= now
@py_have_clock_gettime
def test_time_clock_gettime_ns(self):
now = time.clock_gettime_ns(time.CLOCK_REALTIME)
with time_machine.travel(EPOCH + 190.0):
eh_now = time_machine.escape_hatch.time.clock_gettime_ns(
time.CLOCK_REALTIME
)
assert eh_now >= now
def test_time_gmtime(self):
now = time.gmtime()
with time_machine.travel(EPOCH):
eh_now = time_machine.escape_hatch.time.gmtime()
assert eh_now >= now
def test_time_localtime(self):
now = time.localtime()
with time_machine.travel(EPOCH):
eh_now = time_machine.escape_hatch.time.localtime()
assert eh_now >= now
def test_time_monotonic(self):
with time_machine.travel(LIBRARY_EPOCH):
# real monotonic time counts from a small number
assert time_machine.escape_hatch.time.monotonic() < LIBRARY_EPOCH
def test_time_monotonic_ns(self):
with time_machine.travel(LIBRARY_EPOCH):
# real monotonic time counts from a small number
assert (
time_machine.escape_hatch.time.monotonic_ns()
< LIBRARY_EPOCH * NANOSECONDS_PER_SECOND
)
def test_time_strftime_no_arg(self):
today = dt.date.today()
with time_machine.travel(EPOCH):
eh_formatted = time_machine.escape_hatch.time.strftime("%Y-%m-%d")
eh_today = dt.datetime.strptime(eh_formatted, "%Y-%m-%d").date()
assert eh_today >= today
def test_time_strftime_arg(self):
with time_machine.travel(EPOCH):
formatted = time_machine.escape_hatch.time.strftime(
"%Y-%m-%d",
time.localtime(EPOCH_PLUS_ONE_YEAR),
)
assert formatted == "1971-01-01"
def test_time_time(self):
now = time.time()
with time_machine.travel(EPOCH):
eh_now = time_machine.escape_hatch.time.time()
assert eh_now >= now
def test_time_time_ns(self):
now = time.time_ns()
with time_machine.travel(EPOCH):
eh_now = time_machine.escape_hatch.time.time_ns()
assert eh_now >= now
adamchainz-time-machine-591fa85/tox.ini 0000664 0000000 0000000 00000000706 15051127025 0020027 0 ustar 00root root 0000000 0000000 [tox]
requires =
tox>=4.2
env_list =
py{314, 313, 312, 311, 310, 39, 314t, 313t}
[testenv]
runner = uv-venv-lock-runner
package = wheel
extras =
cli
set_env =
PYTHONDEVMODE = 1
commands =
python \
-W error \
-W ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning \
-W ignore:datetime.datetime.utcnow:DeprecationWarning \
-m coverage run \
-m pytest {posargs:tests}
dependency_groups =
test
adamchainz-time-machine-591fa85/uv.lock 0000664 0000000 0000000 00000352655 15051127025 0020035 0 ustar 00root root 0000000 0000000 version = 1
revision = 3
requires-python = ">=3.9"
resolution-markers = [
"python_full_version >= '3.11'",
"python_full_version == '3.10.*'",
"python_full_version < '3.10'",
]
[[package]]
name = "accessible-pygments"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" },
]
[[package]]
name = "alabaster"
version = "0.7.16"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" },
]
[[package]]
name = "alabaster"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.11'",
"python_full_version == '3.10.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
]
[[package]]
name = "babel"
version = "2.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" },
]
[[package]]
name = "certifi"
version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" },
{ url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" },
{ url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" },
{ url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" },
{ url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" },
{ url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" },
{ url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" },
{ url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" },
{ url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" },
{ url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" },
{ url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" },
{ url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" },
{ url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" },
{ url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" },
{ url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" },
{ url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" },
{ url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" },
{ url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" },
{ url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
{ url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
{ url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
{ url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
{ url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
{ url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
{ url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
{ url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
{ url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
{ url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" },
{ url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" },
{ url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" },
{ url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" },
{ url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" },
{ url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" },
{ url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" },
{ url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" },
{ url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" },
{ url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" },
{ url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" },
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
version = "7.10.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936, upload-time = "2025-08-10T21:27:39.968Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/44/e14576c34b37764c821866909788ff7463228907ab82bae188dab2b421f1/coverage-7.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53808194afdf948c462215e9403cca27a81cf150d2f9b386aee4dab614ae2ffe", size = 215964, upload-time = "2025-08-10T21:25:22.828Z" },
{ url = "https://files.pythonhosted.org/packages/e6/15/f4f92d9b83100903efe06c9396ee8d8bdba133399d37c186fc5b16d03a87/coverage-7.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4d1b837d1abf72187a61645dbf799e0d7705aa9232924946e1f57eb09a3bf00", size = 216361, upload-time = "2025-08-10T21:25:25.603Z" },
{ url = "https://files.pythonhosted.org/packages/e9/3a/c92e8cd5e89acc41cfc026dfb7acedf89661ce2ea1ee0ee13aacb6b2c20c/coverage-7.10.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2a90dd4505d3cc68b847ab10c5ee81822a968b5191664e8a0801778fa60459fa", size = 243115, upload-time = "2025-08-10T21:25:27.09Z" },
{ url = "https://files.pythonhosted.org/packages/23/53/c1d8c2778823b1d95ca81701bb8f42c87dc341a2f170acdf716567523490/coverage-7.10.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d52989685ff5bf909c430e6d7f6550937bc6d6f3e6ecb303c97a86100efd4596", size = 244927, upload-time = "2025-08-10T21:25:28.77Z" },
{ url = "https://files.pythonhosted.org/packages/79/41/1e115fd809031f432b4ff8e2ca19999fb6196ab95c35ae7ad5e07c001130/coverage-7.10.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdb558a1d97345bde3a9f4d3e8d11c9e5611f748646e9bb61d7d612a796671b5", size = 246784, upload-time = "2025-08-10T21:25:30.195Z" },
{ url = "https://files.pythonhosted.org/packages/c7/b2/0eba9bdf8f1b327ae2713c74d4b7aa85451bb70622ab4e7b8c000936677c/coverage-7.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c9e6331a8f09cb1fc8bda032752af03c366870b48cce908875ba2620d20d0ad4", size = 244828, upload-time = "2025-08-10T21:25:31.785Z" },
{ url = "https://files.pythonhosted.org/packages/1f/cc/74c56b6bf71f2a53b9aa3df8bc27163994e0861c065b4fe3a8ac290bed35/coverage-7.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:992f48bf35b720e174e7fae916d943599f1a66501a2710d06c5f8104e0756ee1", size = 242844, upload-time = "2025-08-10T21:25:33.37Z" },
{ url = "https://files.pythonhosted.org/packages/b6/7b/ac183fbe19ac5596c223cb47af5737f4437e7566100b7e46cc29b66695a5/coverage-7.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5595fc4ad6a39312c786ec3326d7322d0cf10e3ac6a6df70809910026d67cfb", size = 243721, upload-time = "2025-08-10T21:25:34.939Z" },
{ url = "https://files.pythonhosted.org/packages/57/96/cb90da3b5a885af48f531905234a1e7376acfc1334242183d23154a1c285/coverage-7.10.3-cp310-cp310-win32.whl", hash = "sha256:9e92fa1f2bd5a57df9d00cf9ce1eb4ef6fccca4ceabec1c984837de55329db34", size = 218481, upload-time = "2025-08-10T21:25:36.935Z" },
{ url = "https://files.pythonhosted.org/packages/15/67/1ba4c7d75745c4819c54a85766e0a88cc2bff79e1760c8a2debc34106dc2/coverage-7.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b96524d6e4a3ce6a75c56bb15dbd08023b0ae2289c254e15b9fbdddf0c577416", size = 219382, upload-time = "2025-08-10T21:25:38.267Z" },
{ url = "https://files.pythonhosted.org/packages/87/04/810e506d7a19889c244d35199cbf3239a2f952b55580aa42ca4287409424/coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397", size = 216075, upload-time = "2025-08-10T21:25:39.891Z" },
{ url = "https://files.pythonhosted.org/packages/2e/50/6b3fbab034717b4af3060bdaea6b13dfdc6b1fad44b5082e2a95cd378a9a/coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85", size = 216476, upload-time = "2025-08-10T21:25:41.137Z" },
{ url = "https://files.pythonhosted.org/packages/c7/96/4368c624c1ed92659812b63afc76c492be7867ac8e64b7190b88bb26d43c/coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157", size = 246865, upload-time = "2025-08-10T21:25:42.408Z" },
{ url = "https://files.pythonhosted.org/packages/34/12/5608f76070939395c17053bf16e81fd6c06cf362a537ea9d07e281013a27/coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54", size = 248800, upload-time = "2025-08-10T21:25:44.098Z" },
{ url = "https://files.pythonhosted.org/packages/ce/52/7cc90c448a0ad724283cbcdfd66b8d23a598861a6a22ac2b7b8696491798/coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a", size = 250904, upload-time = "2025-08-10T21:25:45.384Z" },
{ url = "https://files.pythonhosted.org/packages/e6/70/9967b847063c1c393b4f4d6daab1131558ebb6b51f01e7df7150aa99f11d/coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84", size = 248597, upload-time = "2025-08-10T21:25:47.059Z" },
{ url = "https://files.pythonhosted.org/packages/2d/fe/263307ce6878b9ed4865af42e784b42bb82d066bcf10f68defa42931c2c7/coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160", size = 246647, upload-time = "2025-08-10T21:25:48.334Z" },
{ url = "https://files.pythonhosted.org/packages/8e/27/d27af83ad162eba62c4eb7844a1de6cf7d9f6b185df50b0a3514a6f80ddd/coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124", size = 247290, upload-time = "2025-08-10T21:25:49.945Z" },
{ url = "https://files.pythonhosted.org/packages/28/83/904ff27e15467a5622dbe9ad2ed5831b4a616a62570ec5924d06477dff5a/coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8", size = 218521, upload-time = "2025-08-10T21:25:51.208Z" },
{ url = "https://files.pythonhosted.org/packages/b8/29/bc717b8902faaccf0ca486185f0dcab4778561a529dde51cb157acaafa16/coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117", size = 219412, upload-time = "2025-08-10T21:25:52.494Z" },
{ url = "https://files.pythonhosted.org/packages/7b/7a/5a1a7028c11bb589268c656c6b3f2bbf06e0aced31bbdf7a4e94e8442cc0/coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770", size = 218091, upload-time = "2025-08-10T21:25:54.102Z" },
{ url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262, upload-time = "2025-08-10T21:25:55.367Z" },
{ url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496, upload-time = "2025-08-10T21:25:56.759Z" },
{ url = "https://files.pythonhosted.org/packages/34/b1/bc83788ba31bde6a0c02eb96bbc14b2d1eb083ee073beda18753fa2c4c66/coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7", size = 247989, upload-time = "2025-08-10T21:25:58.067Z" },
{ url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738, upload-time = "2025-08-10T21:25:59.406Z" },
{ url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868, upload-time = "2025-08-10T21:26:00.65Z" },
{ url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790, upload-time = "2025-08-10T21:26:02.009Z" },
{ url = "https://files.pythonhosted.org/packages/69/91/5dcaa134568202397fa4023d7066d4318dc852b53b428052cd914faa05e1/coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613", size = 247907, upload-time = "2025-08-10T21:26:03.757Z" },
{ url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344, upload-time = "2025-08-10T21:26:05.11Z" },
{ url = "https://files.pythonhosted.org/packages/5f/55/c8a273ed503cedc07f8a00dcd843daf28e849f0972e4c6be4c027f418ad6/coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a", size = 218693, upload-time = "2025-08-10T21:26:06.534Z" },
{ url = "https://files.pythonhosted.org/packages/94/58/dd3cfb2473b85be0b6eb8c5b6d80b6fc3f8f23611e69ef745cef8cf8bad5/coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5", size = 219501, upload-time = "2025-08-10T21:26:08.195Z" },
{ url = "https://files.pythonhosted.org/packages/56/af/7cbcbf23d46de6f24246e3f76b30df099d05636b30c53c158a196f7da3ad/coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571", size = 218135, upload-time = "2025-08-10T21:26:09.584Z" },
{ url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298, upload-time = "2025-08-10T21:26:10.973Z" },
{ url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546, upload-time = "2025-08-10T21:26:12.616Z" },
{ url = "https://files.pythonhosted.org/packages/de/bb/e1ade16b9e3f2d6c323faeb6bee8e6c23f3a72760a5d9af102ef56a656cb/coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46", size = 247538, upload-time = "2025-08-10T21:26:14.455Z" },
{ url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141, upload-time = "2025-08-10T21:26:15.787Z" },
{ url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415, upload-time = "2025-08-10T21:26:17.535Z" },
{ url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575, upload-time = "2025-08-10T21:26:18.921Z" },
{ url = "https://files.pythonhosted.org/packages/2d/fd/ae963c7a8e9581c20fa4355ab8940ca272554d8102e872dbb932a644e410/coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c", size = 247466, upload-time = "2025-08-10T21:26:20.263Z" },
{ url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084, upload-time = "2025-08-10T21:26:21.638Z" },
{ url = "https://files.pythonhosted.org/packages/66/4d/a0bcb561645c2c1e21758d8200443669d6560d2a2fb03955291110212ec4/coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0", size = 218735, upload-time = "2025-08-10T21:26:23.009Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c3/78b4adddbc0feb3b223f62761e5f9b4c5a758037aaf76e0a5845e9e35e48/coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c", size = 219531, upload-time = "2025-08-10T21:26:24.474Z" },
{ url = "https://files.pythonhosted.org/packages/70/1b/1229c0b2a527fa5390db58d164aa896d513a1fbb85a1b6b6676846f00552/coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87", size = 218162, upload-time = "2025-08-10T21:26:25.847Z" },
{ url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003, upload-time = "2025-08-10T21:26:27.231Z" },
{ url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238, upload-time = "2025-08-10T21:26:28.83Z" },
{ url = "https://files.pythonhosted.org/packages/62/23/8dfc52e95da20957293fb94d97397a100e63095ec1e0ef5c09dd8c6f591a/coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f", size = 258561, upload-time = "2025-08-10T21:26:30.475Z" },
{ url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735, upload-time = "2025-08-10T21:26:32.333Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960, upload-time = "2025-08-10T21:26:33.701Z" },
{ url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515, upload-time = "2025-08-10T21:26:35.16Z" },
{ url = "https://files.pythonhosted.org/packages/86/11/bb59f7f33b2cac0c5b17db0d9d0abba9c90d9eda51a6e727b43bd5fce4ae/coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256", size = 258278, upload-time = "2025-08-10T21:26:36.539Z" },
{ url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408, upload-time = "2025-08-10T21:26:37.954Z" },
{ url = "https://files.pythonhosted.org/packages/d2/5c/6375e9d905da22ddea41cd85c30994b8b6f6c02e44e4c5744b76d16b026f/coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e", size = 219396, upload-time = "2025-08-10T21:26:39.426Z" },
{ url = "https://files.pythonhosted.org/packages/33/3b/7da37fd14412b8c8b6e73c3e7458fef6b1b05a37f990a9776f88e7740c89/coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c", size = 220458, upload-time = "2025-08-10T21:26:40.905Z" },
{ url = "https://files.pythonhosted.org/packages/28/cc/59a9a70f17edab513c844ee7a5c63cf1057041a84cc725b46a51c6f8301b/coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098", size = 218722, upload-time = "2025-08-10T21:26:42.362Z" },
{ url = "https://files.pythonhosted.org/packages/2d/84/bb773b51a06edbf1231b47dc810a23851f2796e913b335a0fa364773b842/coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de", size = 216280, upload-time = "2025-08-10T21:26:44.132Z" },
{ url = "https://files.pythonhosted.org/packages/92/a8/4d8ca9c111d09865f18d56facff64d5fa076a5593c290bd1cfc5dceb8dba/coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8", size = 216557, upload-time = "2025-08-10T21:26:45.598Z" },
{ url = "https://files.pythonhosted.org/packages/fe/b2/eb668bfc5060194bc5e1ccd6f664e8e045881cfee66c42a2aa6e6c5b26e8/coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667", size = 247598, upload-time = "2025-08-10T21:26:47.081Z" },
{ url = "https://files.pythonhosted.org/packages/fd/b0/9faa4ac62c8822219dd83e5d0e73876398af17d7305968aed8d1606d1830/coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4", size = 250131, upload-time = "2025-08-10T21:26:48.65Z" },
{ url = "https://files.pythonhosted.org/packages/4e/90/203537e310844d4bf1bdcfab89c1e05c25025c06d8489b9e6f937ad1a9e2/coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26", size = 251485, upload-time = "2025-08-10T21:26:50.368Z" },
{ url = "https://files.pythonhosted.org/packages/b9/b2/9d894b26bc53c70a1fe503d62240ce6564256d6d35600bdb86b80e516e7d/coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a", size = 249488, upload-time = "2025-08-10T21:26:52.045Z" },
{ url = "https://files.pythonhosted.org/packages/b4/28/af167dbac5281ba6c55c933a0ca6675d68347d5aee39cacc14d44150b922/coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd", size = 247419, upload-time = "2025-08-10T21:26:53.533Z" },
{ url = "https://files.pythonhosted.org/packages/f4/1c/9a4ddc9f0dcb150d4cd619e1c4bb39bcf694c6129220bdd1e5895d694dda/coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec", size = 248917, upload-time = "2025-08-10T21:26:55.11Z" },
{ url = "https://files.pythonhosted.org/packages/92/27/c6a60c7cbe10dbcdcd7fc9ee89d531dc04ea4c073800279bb269954c5a9f/coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5", size = 218999, upload-time = "2025-08-10T21:26:56.637Z" },
{ url = "https://files.pythonhosted.org/packages/36/09/a94c1369964ab31273576615d55e7d14619a1c47a662ed3e2a2fe4dee7d4/coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833", size = 219801, upload-time = "2025-08-10T21:26:58.207Z" },
{ url = "https://files.pythonhosted.org/packages/23/59/f5cd2a80f401c01cf0f3add64a7b791b7d53fd6090a4e3e9ea52691cf3c4/coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4", size = 218381, upload-time = "2025-08-10T21:26:59.707Z" },
{ url = "https://files.pythonhosted.org/packages/73/3d/89d65baf1ea39e148ee989de6da601469ba93c1d905b17dfb0b83bd39c96/coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6", size = 217019, upload-time = "2025-08-10T21:27:01.242Z" },
{ url = "https://files.pythonhosted.org/packages/7d/7d/d9850230cd9c999ce3a1e600f85c2fff61a81c301334d7a1faa1a5ba19c8/coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241", size = 217237, upload-time = "2025-08-10T21:27:03.442Z" },
{ url = "https://files.pythonhosted.org/packages/36/51/b87002d417202ab27f4a1cd6bd34ee3b78f51b3ddbef51639099661da991/coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e", size = 258735, upload-time = "2025-08-10T21:27:05.124Z" },
{ url = "https://files.pythonhosted.org/packages/1c/02/1f8612bfcb46fc7ca64a353fff1cd4ed932bb6e0b4e0bb88b699c16794b8/coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5", size = 260901, upload-time = "2025-08-10T21:27:06.68Z" },
{ url = "https://files.pythonhosted.org/packages/aa/3a/fe39e624ddcb2373908bd922756384bb70ac1c5009b0d1674eb326a3e428/coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b", size = 263157, upload-time = "2025-08-10T21:27:08.398Z" },
{ url = "https://files.pythonhosted.org/packages/5e/89/496b6d5a10fa0d0691a633bb2b2bcf4f38f0bdfcbde21ad9e32d1af328ed/coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0", size = 260597, upload-time = "2025-08-10T21:27:10.237Z" },
{ url = "https://files.pythonhosted.org/packages/b6/a6/8b5bf6a9e8c6aaeb47d5fe9687014148efc05c3588110246d5fdeef9b492/coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1", size = 258353, upload-time = "2025-08-10T21:27:11.773Z" },
{ url = "https://files.pythonhosted.org/packages/c3/6d/ad131be74f8afd28150a07565dfbdc86592fd61d97e2dc83383d9af219f0/coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c", size = 259504, upload-time = "2025-08-10T21:27:13.254Z" },
{ url = "https://files.pythonhosted.org/packages/ec/30/fc9b5097092758cba3375a8cc4ff61774f8cd733bcfb6c9d21a60077a8d8/coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869", size = 219782, upload-time = "2025-08-10T21:27:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/72/9b/27fbf79451b1fac15c4bda6ec6e9deae27cf7c0648c1305aa21a3454f5c4/coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64", size = 220898, upload-time = "2025-08-10T21:27:16.297Z" },
{ url = "https://files.pythonhosted.org/packages/d1/cf/a32bbf92869cbf0b7c8b84325327bfc718ad4b6d2c63374fef3d58e39306/coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35", size = 218922, upload-time = "2025-08-10T21:27:18.22Z" },
{ url = "https://files.pythonhosted.org/packages/f1/66/c06f4a93c65b6fc6578ef4f1fe51f83d61fc6f2a74ec0ce434ed288d834a/coverage-7.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da749daa7e141985487e1ff90a68315b0845930ed53dc397f4ae8f8bab25b551", size = 215951, upload-time = "2025-08-10T21:27:19.815Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ea/cc18c70a6f72f8e4def212eaebd8388c64f29608da10b3c38c8ec76f5e49/coverage-7.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3126fb6a47d287f461d9b1aa5d1a8c97034d1dffb4f452f2cf211289dae74ef", size = 216335, upload-time = "2025-08-10T21:27:21.737Z" },
{ url = "https://files.pythonhosted.org/packages/f2/fb/9c6d1d67c6d54b149f06b9f374bc9ca03e4d7d7784c8cfd12ceda20e3787/coverage-7.10.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3da794db13cc27ca40e1ec8127945b97fab78ba548040047d54e7bfa6d442dca", size = 242772, upload-time = "2025-08-10T21:27:23.884Z" },
{ url = "https://files.pythonhosted.org/packages/5a/e5/4223bdb28b992a19a13ab1410c761e2bfe92ca1e7bba8e85ee2024eeda85/coverage-7.10.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4e27bebbd184ef8d1c1e092b74a2b7109dcbe2618dce6e96b1776d53b14b3fe8", size = 244596, upload-time = "2025-08-10T21:27:25.842Z" },
{ url = "https://files.pythonhosted.org/packages/d2/13/d646ba28613669d487c654a760571c10128247d12d9f50e93f69542679a2/coverage-7.10.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fd4ee2580b9fefbd301b4f8f85b62ac90d1e848bea54f89a5748cf132782118", size = 246370, upload-time = "2025-08-10T21:27:27.503Z" },
{ url = "https://files.pythonhosted.org/packages/02/7c/aff99c67d8c383142b0877ee435caf493765356336211c4899257325d6c7/coverage-7.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6999920bdd73259ce11cabfc1307484f071ecc6abdb2ca58d98facbcefc70f16", size = 244254, upload-time = "2025-08-10T21:27:29.357Z" },
{ url = "https://files.pythonhosted.org/packages/b0/13/a51ea145ed51ddfa8717bb29926d9111aca343fab38f04692a843d50be6b/coverage-7.10.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c3623f929db885fab100cb88220a5b193321ed37e03af719efdbaf5d10b6e227", size = 242325, upload-time = "2025-08-10T21:27:30.931Z" },
{ url = "https://files.pythonhosted.org/packages/d8/4b/6119be0089c89ad49d2e5a508d55a1485c878642b706a7f95b26e299137d/coverage-7.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:25b902c5e15dea056485d782e420bb84621cc08ee75d5131ecb3dbef8bd1365f", size = 243281, upload-time = "2025-08-10T21:27:32.815Z" },
{ url = "https://files.pythonhosted.org/packages/34/c8/1b2e7e53eee4bc1304e56e10361b08197a77a26ceb07201dcc9e759ef132/coverage-7.10.3-cp39-cp39-win32.whl", hash = "sha256:f930a4d92b004b643183451fe9c8fe398ccf866ed37d172ebaccfd443a097f61", size = 218489, upload-time = "2025-08-10T21:27:34.905Z" },
{ url = "https://files.pythonhosted.org/packages/dd/1e/9c0c230a199809c39e2dff0f1f889dfb04dcd07d83c1c26a8ef671660e08/coverage-7.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:08e638a93c8acba13c7842953f92a33d52d73e410329acd472280d2a21a6c0e1", size = 219396, upload-time = "2025-08-10T21:27:36.61Z" },
{ url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload-time = "2025-08-10T21:27:38.254Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "docutils"
version = "0.21.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
]
[[package]]
name = "furo"
version = "2025.7.19"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "accessible-pygments" },
{ name = "beautifulsoup4" },
{ name = "pygments" },
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-basic-ng" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/69/312cd100fa45ddaea5a588334d2defa331ff427bcb61f5fe2ae61bdc3762/furo-2025.7.19.tar.gz", hash = "sha256:4164b2cafcf4023a59bb3c594e935e2516f6b9d35e9a5ea83d8f6b43808fe91f", size = 1662054, upload-time = "2025-07-19T10:52:09.754Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl", hash = "sha256:bdea869822dfd2b494ea84c0973937e35d1575af088b6721a29c7f7878adc9e3", size = 342175, upload-time = "2025-07-19T10:52:02.399Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "imagesize"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
]
[[package]]
name = "importlib-metadata"
version = "8.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" },
{ url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" },
{ url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" },
{ url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" },
{ url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" },
{ url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" },
{ url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" },
{ url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" },
{ url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" },
{ url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" },
{ url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
{ url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
{ url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
{ url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
{ url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
{ url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
{ url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
{ url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
{ url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
{ url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" },
{ url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" },
{ url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" },
{ url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" },
{ url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" },
{ url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" },
{ url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" },
{ url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" },
{ url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" },
{ url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
]
[[package]]
name = "pytest-randomly"
version = "3.16.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367, upload-time = "2024-10-25T15:45:34.274Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396, upload-time = "2024-10-25T15:45:32.78Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "roman-numerals-py"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "snowballstemmer"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
]
[[package]]
name = "soupsieve"
version = "2.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
]
[[package]]
name = "sphinx"
version = "7.4.7"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
dependencies = [
{ name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "babel", marker = "python_full_version < '3.10'" },
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
{ name = "docutils", marker = "python_full_version < '3.10'" },
{ name = "imagesize", marker = "python_full_version < '3.10'" },
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
{ name = "jinja2", marker = "python_full_version < '3.10'" },
{ name = "packaging", marker = "python_full_version < '3.10'" },
{ name = "pygments", marker = "python_full_version < '3.10'" },
{ name = "requests", marker = "python_full_version < '3.10'" },
{ name = "snowballstemmer", marker = "python_full_version < '3.10'" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" },
{ name = "tomli", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" },
]
[[package]]
name = "sphinx"
version = "8.1.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.10.*'",
]
dependencies = [
{ name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "babel", marker = "python_full_version == '3.10.*'" },
{ name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" },
{ name = "docutils", marker = "python_full_version == '3.10.*'" },
{ name = "imagesize", marker = "python_full_version == '3.10.*'" },
{ name = "jinja2", marker = "python_full_version == '3.10.*'" },
{ name = "packaging", marker = "python_full_version == '3.10.*'" },
{ name = "pygments", marker = "python_full_version == '3.10.*'" },
{ name = "requests", marker = "python_full_version == '3.10.*'" },
{ name = "snowballstemmer", marker = "python_full_version == '3.10.*'" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" },
{ name = "tomli", marker = "python_full_version == '3.10.*'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" },
]
[[package]]
name = "sphinx"
version = "8.2.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.11'",
]
dependencies = [
{ name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "babel", marker = "python_full_version >= '3.11'" },
{ name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
{ name = "docutils", marker = "python_full_version >= '3.11'" },
{ name = "imagesize", marker = "python_full_version >= '3.11'" },
{ name = "jinja2", marker = "python_full_version >= '3.11'" },
{ name = "packaging", marker = "python_full_version >= '3.11'" },
{ name = "pygments", marker = "python_full_version >= '3.11'" },
{ name = "requests", marker = "python_full_version >= '3.11'" },
{ name = "roman-numerals-py", marker = "python_full_version >= '3.11'" },
{ name = "snowballstemmer", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" },
]
[[package]]
name = "sphinx-basic-ng"
version = "1.0.0b2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" },
]
[[package]]
name = "sphinx-build-compatibility"
version = "0.0.1"
source = { git = "https://github.com/readthedocs/sphinx-build-compatibility?rev=4f304bd4562cdc96316f4fec82b264ca379d23e0#4f304bd4562cdc96316f4fec82b264ca379d23e0" }
dependencies = [
{ name = "requests" },
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
[[package]]
name = "sphinx-copybutton"
version = "0.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" },
]
[[package]]
name = "sphinxcontrib-applehelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" },
]
[[package]]
name = "sphinxcontrib-devhelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" },
]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
]
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
]
[[package]]
name = "sphinxcontrib-qthelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" },
]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
]
[[package]]
name = "time-machine"
version = "2.19.0"
source = { editable = "." }
dependencies = [
{ name = "python-dateutil" },
]
[package.optional-dependencies]
cli = [
{ name = "tokenize-rt" },
]
[package.dev-dependencies]
docs = [
{ name = "furo" },
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-build-compatibility" },
{ name = "sphinx-copybutton" },
]
test = [
{ name = "coverage", extra = ["toml"] },
{ name = "pytest" },
{ name = "pytest-randomly" },
{ name = "python-dateutil" },
]
[package.metadata]
requires-dist = [
{ name = "python-dateutil" },
{ name = "tokenize-rt", marker = "extra == 'cli'" },
]
provides-extras = ["cli"]
[package.metadata.requires-dev]
docs = [
{ name = "furo", specifier = ">=2024.8.6" },
{ name = "sphinx", specifier = ">=7.4.7" },
{ name = "sphinx-build-compatibility", git = "https://github.com/readthedocs/sphinx-build-compatibility?rev=4f304bd4562cdc96316f4fec82b264ca379d23e0" },
{ name = "sphinx-copybutton", specifier = ">=0.5.2" },
]
test = [
{ name = "backports-zoneinfo", marker = "python_full_version < '3.9'" },
{ name = "coverage", extras = ["toml"] },
{ name = "pytest" },
{ name = "pytest-randomly" },
{ name = "python-dateutil" },
]
[[package]]
name = "tokenize-rt"
version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/69/ed/8f07e893132d5051d86a553e749d5c89b2a4776eb3a579b72ed61f8559ca/tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", size = 5476, upload-time = "2025-05-23T23:48:00.035Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44", size = 6004, upload-time = "2025-05-23T23:47:58.812Z" },
]
[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
]
[[package]]
name = "typing-extensions"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]