pax_global_header 0000666 0000000 0000000 00000000064 15250103717 0014513 g ustar 00root root 0000000 0000000 52 comment=27b1d58f8f6f8af6982db66a1a6a9abc8189d3ec
adamchainz-time-machine-18d4852/ 0000775 0000000 0000000 00000000000 15250103717 0016431 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/.clang-format 0000664 0000000 0000000 00000001422 15250103717 0021003 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-18d4852/.editorconfig 0000664 0000000 0000000 00000000345 15250103717 0021110 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-18d4852/.github/ 0000775 0000000 0000000 00000000000 15250103717 0017771 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/.github/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000000131 15250103717 0022563 0 ustar 00root root 0000000 0000000 This project follows [Django's Code of Conduct](https://www.djangoproject.com/conduct/).
adamchainz-time-machine-18d4852/.github/FUNDING.yml 0000664 0000000 0000000 00000000123 15250103717 0021602 0 ustar 00root root 0000000 0000000 github: adamchainz
tidelift: pypi/time-machine
custom:
- "https://adamj.eu/books/"
adamchainz-time-machine-18d4852/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15250103717 0022154 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000034 15250103717 0024141 0 ustar 00root root 0000000 0000000 blank_issues_enabled: false
adamchainz-time-machine-18d4852/.github/ISSUE_TEMPLATE/feature-request.yml 0000664 0000000 0000000 00000000411 15250103717 0026014 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-18d4852/.github/ISSUE_TEMPLATE/issue.yml 0000664 0000000 0000000 00000001540 15250103717 0024027 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.14.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-18d4852/.github/SECURITY.md 0000664 0000000 0000000 00000000101 15250103717 0021552 0 ustar 00root root 0000000 0000000 Please report security issues directly over email to me@adamj.eu
adamchainz-time-machine-18d4852/.github/workflows/ 0000775 0000000 0000000 00000000000 15250103717 0022026 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/.github/workflows/main.yml 0000664 0000000 0000000 00000011761 15250103717 0023503 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.10'
- '3.11'
- '3.12'
- '3.13'
- '3.13t'
- '3.14'
- '3.14t'
- '3.15'
- '3.15t'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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
if: always()
steps:
- name: Check all test jobs passed
if: needs.tests.result != 'success'
run: exit 1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Install dependencies
run: uv pip install --system coverage[toml]
- name: Download data
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: .coverage
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: html-report
path: htmlcov
build:
name: Build wheels for ${{ matrix.os }}-${{ matrix.arch }}
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Build')
strategy:
fail-fast: false
matrix:
include:
- os: linux
arch: aarch64
- os: linux
arch: x86_64
- os: macos
arch: arm64
- os: macos
arch: x86_64
- os: windows
arch: AMD64
- os: windows
arch: ARM64
runs-on: ${{ (matrix.os == 'linux' && matrix.arch == 'aarch64' && 'ubuntu-24.04-arm') || (matrix.os == 'linux' && 'ubuntu-24.04') || (matrix.os == 'macos' && matrix.arch == 'arm64' && 'macos-15') || (matrix.os == 'macos' && matrix.arch == 'x86_64' && 'macos-15-intel') || (matrix.os == 'windows' && matrix.arch == 'ARM64' && 'windows-11-arm') || (matrix.os == 'windows' && 'windows-2022') || 'unknown' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: false
- name: Build sdist
if: matrix.os == 'linux' && matrix.arch == 'x86_64'
run: uv build --sdist
- name: Build wheels
run: uv run --only-group build cibuildwheel --output-dir dist
- run: ${{ (matrix.os == 'windows' && 'dir') || 'ls -lh' }} dist/
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist-${{ matrix.os }}-${{ matrix.arch }}
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: get dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
merge-multiple: true
pattern: dist-*
path: dist
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
adamchainz-time-machine-18d4852/.gitignore 0000664 0000000 0000000 00000000121 15250103717 0020413 0 ustar 00root root 0000000 0000000 *.egg-info/
*.pyc
*.so
/.coverage
/.coverage.*
/.tox
/build/
/dist/
/wheelhouse/
adamchainz-time-machine-18d4852/.pre-commit-config.yaml 0000664 0000000 0000000 00000003610 15250103717 0022712 0 ustar 00root root 0000000 0000000 ci:
autoupdate_schedule: monthly
skip:
- clang-format
default_language_version:
python: python3.14
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.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: c132f8b9e3ac7f8f0625490b1cb5b906d1d6a63a # frozen: v1
hooks:
- id: typos
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 3b42b9776870badc9eaffdef9a4a0834b764d87c # frozen: v2.29.3
hooks:
- id: pyproject-fmt
additional_dependencies: ["tox>=4.9"]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 241ced4140307006cfd8b2b9ccfd43b1139cfe4a # frozen: 1.9.0
hooks:
- id: tox-ini-fmt
- repo: https://github.com/rstcheck/rstcheck
rev: 27a8027d6c6df787891a01b76e8298bac493fb58 # frozen: v6.3.0
hooks:
- id: rstcheck
additional_dependencies:
- sphinx==8.1.3
- tomli==2.0.1
- repo: https://github.com/adamchainz/blacken-docs
rev: dda8db18cfc68df532abf33b185ecd12d5b7b326 # frozen: 1.20.0
hooks:
- id: blacken-docs
additional_dependencies:
- black==25.1.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 321478e58f4938179c6b86e4ddfa923d1547a49b # frozen: v0.16.6
hooks:
- id: ruff-check
args: [ --fix ]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 7ff8d35ae36a7d2b968f2f90b4c723e292e594ee # frozen: v2.3.1
hooks:
- id: mypy
additional_dependencies:
- hypothesis==6.165.2
- 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-18d4852/.readthedocs.yaml 0000664 0000000 0000000 00000000550 15250103717 0021660 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.14"
python:
install:
- method: uv
command: sync
groups:
- docs
sphinx:
configuration: docs/conf.py
fail_on_warning: true
formats: all
adamchainz-time-machine-18d4852/.typos.toml 0000664 0000000 0000000 00000000413 15250103717 0020560 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-18d4852/CHANGELOG.rst 0000664 0000000 0000000 00000000101 15250103717 0020442 0 ustar 00root root 0000000 0000000 See https://time-machine.readthedocs.io/en/latest/changelog.html
adamchainz-time-machine-18d4852/HISTORY.rst 0000664 0000000 0000000 00000000107 15250103717 0020322 0 ustar 00root root 0000000 0000000 See https://github.com/adamchainz/time-machine/blob/main/CHANGELOG.rst
adamchainz-time-machine-18d4852/LICENSE 0000664 0000000 0000000 00000002055 15250103717 0017440 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-18d4852/MANIFEST.in 0000664 0000000 0000000 00000000163 15250103717 0020167 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-18d4852/README.rst 0000664 0000000 0000000 00000003125 15250103717 0020121 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
*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"
Documentation
=============
Please see https://time-machine.readthedocs.io/.
adamchainz-time-machine-18d4852/docs/ 0000775 0000000 0000000 00000000000 15250103717 0017361 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/docs/.gitignore 0000664 0000000 0000000 00000000011 15250103717 0021341 0 ustar 00root root 0000000 0000000 /_build/
adamchainz-time-machine-18d4852/docs/Makefile 0000664 0000000 0000000 00000001177 15250103717 0021027 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-18d4852/docs/_static/ 0000775 0000000 0000000 00000000000 15250103717 0021007 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/docs/_static/logo.svg 0000664 0000000 0000000 00000053210 15250103717 0022471 0 ustar 00root root 0000000 0000000
adamchainz-time-machine-18d4852/docs/changelog.rst 0000664 0000000 0000000 00000073125 15250103717 0022052 0 ustar 00root root 0000000 0000000 =========
Changelog
=========
3.5.1 (2026-09-08)
------------------
* Only pass the compiler option ``-mno-omit-leaf-frame-pointer`` when the compiler supports it, checked with a tiny probe program.
The option is unsupported outside of x86 and ARM64, so this fixes building from source on other architectures, like PowerPC, which failed since the option was added in version 3.3.0.
`PR #692 `__.
Thanks to Colin Watson for pointing this out and linking to the workaround in `Debian Bug #1146407 `__.
* Fix the mocked ``datetime.date.today()`` and ``datetime.datetime.today()`` to be exact for all supported dates, like ``datetime.datetime.now()``.
Previously, they went through a floating-point timestamp, which could round the microseconds, or even the whole day, for dates far in the future.
`PR #690 `__.
* Fix the class decorator to stop time travelling when ``tearDownClass()`` raises an exception, or when ``setUpClass()`` raises an exception not deriving from ``Exception``, such as the skip outcome from ``pytest.skip()``.
Previously, time remained mocked for the rest of the process in these cases.
`PR #678 `__.
* Fix ``Traveller.move_to()`` to keep the current timezone mocked when the given destination is unsupported.
Previously, the timezone was restored before the destination was checked, leaving it unmocked whilst still time travelling.
`PR #679 `__.
* Fix the ``time_machine`` pytest fixture to not try to stop a traveller that failed to start.
`PR #680 `__.
* Fix the mocked ``time.strftime()`` to raise ``TypeError`` when passed ``None`` as its second argument, like the unmocked function, rather than treating it as the current time.
`PR #681 `__.
* Fix the :ref:`Migration CLI ` to keep the rewritten imports on the same line when a freezegun import shares its line with other code, like ``if TYPE_CHECKING: from freezegun import freeze_time, FakeDate``.
Previously, the remaining ``from freezegun import FakeDate`` was moved to a new line, outside the block.
`PR #682 `__.
* Extend the :ref:`Migration CLI ` to migrate uses of ``FrozenDateTimeFactory`` in string annotations, like ``freezer: "FrozenDateTimeFactory"``.
Previously, the import was removed whilst such annotations were left referring to it.
`PR #683 `__.
* Fix the :ref:`Migration CLI ` to not rename a ``freezer`` argument when the function already has a ``time_machine`` argument, which produced invalid syntax.
`PR #684 `__.
* Fix the :ref:`Migration CLI ` to report positions in the rewritten file, rather than the original.
Previously, positions could be off when rewrites earlier in the file changed the number of lines, or the length of the same line.
`PR #686 `__.
* Fix the :ref:`Migration CLI ` to not report a ``freezegun`` usage for migrated calls with a parenthesized module name, like ``(freezegun).freeze_time(...)``.
`PR #687 `__.
* Fix the :ref:`Migration CLI ` to leave a ``pass`` statement when removing every unused ``FrozenDateTimeFactory`` import in a block, rather than producing invalid syntax.
`PR #688 `__.
* Make ``TimeMachineFixture`` importable even when pytest isn’t installed.
This avoids triggering ``possibly-missing-attribute`` or ``possibly-missing-import`` violations in ty for test files with annotations.
Thanks to Nick Pope in `PR #675 `__.
3.5.0 (2026-08-25)
------------------
* Support ``None`` as a destination for ``travel()`` and ``Traveller.move_to()``, meaning the current time.
Use this with ``tick=False`` to freeze time at the present moment, matching freezegun’s behaviour when ``freeze_time()`` is called with no arguments.
`PR #666 `__.
* Extend the :ref:`Migration CLI ` to migrate aliased imports: ``import freezegun as fg`` and ``from freezegun import freeze_time as ft``, plus calls using such aliases.
`PR #662 `__.
* Extend the :ref:`Migration CLI ` to migrate the ``pytest.mark.freeze_time`` marker in module-level and class-level ``pytestmark`` assignments.
`PR #663 `__.
* Extend the :ref:`Migration CLI ` to migrate ``freeze_time()`` calls and markers that pass ``tz_offset`` with a literal zero value.
The argument is dropped, since a zero offset has no effect.
`PR #664 `__.
* Extend the :ref:`Migration CLI ` to migrate ``freeze_time()`` calls and markers that pass ``real_asyncio``.
The argument is dropped, whatever its value, since time-machine does not mock ``time.monotonic()``, so asyncio event loops always see real time.
`PR #665 `__.
* Extend the :ref:`Migration CLI ` to migrate ``freeze_time()`` calls and markers with no destination argument, which freeze at the current time.
``None`` is added as the destination, using the new support for ``None`` destinations (above).
`PR #667 `__.
* Extend the :ref:`Migration CLI ` to migrate ``freeze_time()`` calls and markers that pass ``ignore``.
The argument is dropped, since it works around problems with freezegun’s module patching, which time-machine’s C-level mocking doesn’t have.
`PR #671 `__.
* Make the :ref:`Migration CLI ` report freezegun-related usages that it recognizes but cannot migrate, with their positions, like:
.. code-block:: console
example/tests.py:9:2: freeze_time usage not migrated
This makes it easier to find and fix the remaining usages manually.
`PR #668 `__ and `PR #669 `__.
* Make the :ref:`Migration CLI ` report files that cannot be parsed, rather than skipping them silently.
This helps notice files using syntax from a newer Python version than the tool is run with.
`PR #674 `__.
* Extend the :ref:`Migration CLI ` to migrate “raw use” assignments of ``freeze_time()`` to variables or ``self.`` attributes used with ``start()`` and ``stop()``, covering unittest ``setUp()`` / ``tearDown()`` patterns like ``self.freezer = freeze_time(...)`` with ``self.addCleanup(self.freezer.stop)``.
`PR #670 `__ and `PR #672 `__.
* Extend the :ref:`Migration CLI ` to migrate imports and uses of freezegun’s ``FrozenDateTimeFactory`` class, often used to annotate the ``freezer`` fixture argument, to time-machine’s newly-documented equivalent, ``time_machine.TimeMachineFixture``.
`PR #673 `__.
3.4.0 (2026-08-10)
------------------
* Publish wheels for Python 3.15.
Thanks to Edgar Ramírez Mondragón in `PR #655 `__.
* Fix a reference leak in the patched ``datetime.datetime.utcnow()`` that occurred when its Python 3.12+ ``DeprecationWarning`` was raised as an error, such as under ``-W error``.
Each such call leaked a reference to the active traveller object.
`PR #656 `__.
* Extend the :ref:`Migration CLI ` to migrate patterns from freezegun’s pytest plugins, pytest-freezegun and pytest-freezer: the ``freezer`` fixture and the ``pytest.mark.freeze_time`` marker.
Also make it migrate ``freeze_time()`` context managers that bind the result with ``as``, including converting ``tick()`` method calls to ``shift()``.
`PR #658 `__.
Thanks to Javier Buzzi for the initial work in `PR #562 `__.
* Fix the :ref:`Migration CLI ` to handle trailing commas in ``freeze_time()`` calls when adding ``tick=False``.
Previously, it produced invalid syntax, like ``travel("2023-01-01",, tick=False)``.
`PR #658 `__.
* Fix the :ref:`Migration CLI ` to not indent imports kept from a rewritten from-import when the import doesn’t start its line, such as after ``if True:`` on one line.
Previously, the kept import gained leading whitespace, producing invalid syntax.
`PR #658 `__.
3.3.1 (2026-08-04)
------------------
* Fix small rounding errors in destination calculations.
Previously, time-machine computed destinations in floating-point seconds, which could lead to small rounding errors: up to a few hundred nanoseconds for present-day destinations, growing to several microseconds by the year 2500.
This change fixes the pipeline to use integer nanoseconds throughout, so time-travel is precise for any supported date.
`PR #651 `__.
3.3.0 (2026-07-31)
------------------
* Support Python 3.15.
`PR #631 `__.
* Mock ``datetime.date.today()`` directly, for Python 3.15 support.
Previously time-machine was mocked only indirectly, since CPython implemented it by calling ``cls.fromtimestamp(time.time())``.
Python 3.15 added a fast path that reads the system clock directly (`CPython Issue #130980 `__), so time travel no longer affected it, which the new mock fixes.
Consequently, there are new :ref:`escape hatch ` functions: ``escape_hatch.datetime.date.today()`` and ``escape_hatch.datetime.datetime.today()``.
Thanks to Miro Hrončok and Karolina Surma for the report in `Issue #610 `__, Lumír 'Frenzy' Balhar for the fix in `PR #618 `__, and Maurycy Pawłowski-Wieroński for review
* Support isolated subinterpreters, as created by |concurrent.interpreters|__ on Python 3.14+.
Previously, importing time-machine in an isolated subinterpreter failed with an ``ImportError``.
.. |concurrent.interpreters| replace:: ``concurrent.interpreters``
__ https://docs.python.org/3.14/library/concurrent.interpreters.html
`PR #644 `__.
* Move the intermediary functions that patched functions call from Python to C.
This change reduces the overhead of calling patched functions like ``time.time()`` while time travelling, making them around 10-20% faster.
`PR #643 `__.
* Fix returning ``datetime`` subclasses from their ``now()`` and ``utcnow()`` methods while time travelling.
Previously, time-machine would always return a plain ``datetime.datetime`` instance, rather than the subclass.
`PR #643 `__.
* Fix hiding of the ``DeprecationWarning`` from |datetime.utcnow()|__ on Python 3.12+ while time travelling.
Previously, the mocked version of the function did not raise the warning at all, so deprecated calls could pass unnoticed in tests.
The warning is attributed to the calling code, like the real function does.
.. |datetime.utcnow()| replace:: ``datetime.utcnow()``
__ https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow
The :ref:`escape hatch ` function ``escape_hatch.datetime.datetime.utcnow()`` also now attributes its warning to the calling code, rather than to a line within time-machine.
`PR #649 `__.
Thanks to Tamir Duberstein for the report in `Issue #445 `__ and Anders Kaseorg for the initial implementation in `PR #486 `__.
* Fix the standard |uuid|__ library functions ``uuid1()``, ``uuid6()``, and ``uuid7()`` to correctly generate values for the destination when time travels backwards.
These functions cache the timestamp of the value they generated most recently, and never generate a value stamped before it.
Previously, after time-travelling backwards, these functions would keep generating values stamped for the cached timestamp, rather than the new destination.
The caches are now reset whenever time travel starts, stops, or moves backwards.
.. |uuid| replace:: ``uuid``
__ https://docs.python.org/3/library/uuid.html
`PR #648 `__.
Thanks to Diego Carrasco for the report in `Issue #601 `__ and initial work in `PR #597 `__.
* Build with frame pointers enabled, preparation for `PEP 831 `__.
`PR #627 `__.
* Stop shipping wheels for 32-bit Linux and Windows.
`PR #605 `__.
* Stop shipping wheels for free-threaded Python 3.13 since `cibuildwheel 4.0.0 dropped support for building them `__.
* Extend the :ref:`Migration CLI ` to update ``freeze_time()`` calls that pass ``tick``.
Thanks to George-Cristian Birzan for the report in `Issue #609 `__ and tanren for the fix in `PR #636 `__.
* Extend the :ref:`Migration CLI ` to rewrite ``freeze_time()`` decorators on async functions.
Thanks to George-Cristian Birzan for the report in `Issue #608 `__ and Sanjay Santhanam for the fix in `PR #640 `__.
* Fix the :ref:`Migration CLI ` to leave relative imports called ``freezegun`` alone.
Previously it rewrote ``from .freezegun import freeze_time``, which imports from a local module that happens to share freezegun’s name.
`PR #645 `__.
* Extend the :ref:`Migration CLI ` to rewrite from-imports that import ``freeze_time`` alongside other names.
For example, ``from freezegun import freeze_time, FakeDate`` now becomes ``import time_machine`` plus ``from freezegun import FakeDate``.
`PR #647 `__.
3.2.0 (2025-12-17)
------------------
* Add :attr:`time_machine.naive_mode` to control how time-machine interprets naive datetimes.
The default mode is ``MIXED``, which preserves existing behaviour: naive ``datetime`` objects and ``date`` objects are interpreted as UTC, while naive datetime strings are interpreted as local time.
Three alternative modes are available:
* ``UTC``: naive datetimes are always interpreted as UTC.
* ``LOCAL``: naive datetimes are interpreted as local time, matching Python's default semantics, and freezegun.
* ``ERROR``: naive datetimes raise a ``RuntimeError``, ensuring your tests are isolated from the current timezone.
.. note::
It’s recommended you use ``LOCAL`` or ``ERROR`` to avoid confusion around naive datetimes.
`PR #591 `__. Thanks to Paolo Melchiorre for review.
Thanks to PhML, Stefaan Lippens, Matthieu Rigal, Nikita Demir, Steve Mavens, Andy Freeland, and Paul Ganssle for their input on `Issue #257 `__.
* Raise ``RuntimeError`` when attempting to start time travelling if `freezegun `__ is active.
This change should help avoid surprises when migrating complex test suites from freezegun to time-machine.
`PR #590 `__.
3.1.0 (2025-11-21)
------------------
* Optimize patching of ``uuid`` module.
By avoiding using ``unittest.mock``, this small overhead from starting ``time_machine.travel()`` has been reduced about 20x, from ~600ns to ~30ns by one benchmark.
`PR #585 `__.
3.0.0 (2025-11-18)
------------------
* Remove mocking of ``time.monotonic()`` and ``time.monotonic_ns()``.
This mocking caused too many issues, such as causing freezes in asyncio event loops (`Issue #387 `__), preventing pytest-durations from timing tests correctly (`Issue #505 `__), and triggering timeouts in psycopg (`Issue #509 `__).
The root cause here is that mocking the monotonic clock breaks its contract, allowing it to move backwards when it’s meant to only move forwards.
As an alternative, use |unittest.mock|__ to mock the monotonic function for the specific tested modules that need it.
That means that your code should import ``monotonic()`` or ``monotonic_ns()`` directly, so that your tests can mock it in those places only.
For example, if your system under test looks like:
.. |unittest.mock| replace:: ``unittest.mock``
__ https://docs.python.org/3/library/unittest.mock.html
.. code-block:: python
# example.py
from time import monotonic
def measurement():
start = monotonic()
...
end = monotonic()
return end - start
…then your tests can mock ``monotonic()`` like this:
.. code-block:: python
from unittest import TestCase, mock
import example
class MeasurementTests(TestCase):
def test_success(self):
with mock.patch.object(example, "monotonic", side_effect=[0.0, 1.23]):
result = example.measurement()
assert result == 1.23
`PR #560 `__.
* Parse ``str`` destinations with |datetime.fromisoformat()|__ first, before falling back to dateutil if installed.
``datetime.fromisoformat()`` can parse most valid ISO 8601 formats, with better performance and no extra dependencies.
.. |datetime.fromisoformat()| replace:: ``datetime.fromisoformat()``
__ https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat
`PR #578 `__.
* Make the dependency on `dateutil `__ optional.
To include dateutil support, install with the ``dateutil`` extra:
.. code-block:: sh
python -m pip install time-machine[dateutil]
Beware that some of the formats that dateutil parses are ambiguous and may lead to unexpected results.
`PR #576 `__.
* Rename the ``Coordinates`` class to ``Traveller``, to match the recommended context manager variable name.
Thanks to Matt Wang in `PR #535 `__.
* Drop Python 3.9 support.
* Make the ``escape_hatch`` functions raise ``ValueError`` when called outside of time-travelling, rather than triggering segmentation faults.
`PR #580 `__.
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-18d4852/docs/comparison.rst 0000664 0000000 0000000 00000010160 15250103717 0022263 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-18d4852/docs/conf.py 0000664 0000000 0000000 00000005015 15250103717 0020661 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 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",
]
# 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%",
},
"source_repository": "https://github.com/adamchainz/time-machine/",
"source_branch": "main",
"source_directory": "docs/",
}
# -- 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-18d4852/docs/index.rst 0000664 0000000 0000000 00000002373 15250103717 0021227 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-18d4852/docs/installation.rst 0000664 0000000 0000000 00000002516 15250103717 0022620 0 ustar 00root root 0000000 0000000 ============
Installation
============
Requirements
------------
Python 3.10 to 3.15 supported, including free-threaded variants from Python 3.14 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
For support parsing strings as datetimes, include the ``dateutil`` extra:
.. code-block:: sh
python -m pip install time-machine[dateutil]
For support running the :ref:`migration CLI `, include the ``cli`` extra:
.. code-block:: sh
python -m pip install time-machine[cli]
Consider setting naive mode
~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, time-machine has mixed behaviour for naive datetimes: it interprets naive ``datetime`` and ``date`` objects as UTC, but naive datetime strings as local time.
This mixed behaviour is probably surprising, is definitely regrettable, but is left, for now, for backwards compatibility.
When adding time-machine to a new project, consider changing :attr:`time_machine.naive_mode` to one of these clearer modes:
* ``NaiveMode.LOCAL`` - for consistency with Python’s default semantics, especially when migrating from freezegun.
* ``NaiveMode.ERROR`` - for maximum test reproducibility.
See :attr:`time_machine.naive_mode` for details.
adamchainz-time-machine-18d4852/docs/migration.rst 0000664 0000000 0000000 00000031443 15250103717 0022111 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.
To help with this, the tool reports freezegun-related usages that it recognizes but cannot migrate, with their positions:
.. code-block:: console
$ python -m time_machine migrate example/tests.py
Rewriting example/tests.py
example/tests.py:9:2: freeze_time usage not migrated
These reports are heuristic and may occasionally flag unrelated code that reuses a freezegun-related name, such as a function parameter that shadows an imported ``freeze_time``.
Files that cannot be parsed are also reported, and otherwise skipped.
Since the tool parses files with the Python version it runs on, run it with a version at least as new as the target project uses.
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 tool makes the below changes, grouped here by the kind of code they apply to.
Import updates
~~~~~~~~~~~~~~
* ``import freezegun`` -> ``import time_machine``:
.. code-block:: diff
-import freezegun
+import time_machine
* ``from freezegun import freeze_time`` -> ``import time_machine``:
.. code-block:: diff
-from freezegun import freeze_time
+import time_machine
* Aliased imports like ``import freezegun as fg`` or ``from freezegun import freeze_time as ft`` -> ``import time_machine``.
The alias is dropped, since calls using it are migrated to use the ``time_machine`` module, per the below:
.. code-block:: diff
-import freezegun as fg
+import time_machine
-@fg.freeze_time("2023-01-01")
+@time_machine.travel("2023-01-01", tick=False)
def test_function():
...
* ``from freezegun import freeze_time, FakeDate`` -> ``import time_machine`` plus ``from freezegun import FakeDate``, keeping the other imported names:
.. code-block:: diff
-from freezegun import freeze_time, FakeDate
+import time_machine
+from freezegun import FakeDate
``freeze_time()`` calls
~~~~~~~~~~~~~~~~~~~~~~~
* In function decorators, class decorators, and context managers: ``freeze_time(...)`` -> ``time_machine.travel(...)``.
This change is applied only when ``freeze_time()`` is called with a single positional argument and only supported keyword arguments: ``tick``, ``tz_offset`` with a literal zero value, ``real_asyncio``, and ``ignore``.
If ``tick`` is passed, it is kept as-is, otherwise it is replaced with ``tick=False`` (matching freezegun’s default behaviour):
.. code-block:: diff
-@freeze_time("2023-01-01")
+@time_machine.travel("2023-01-01", tick=False)
def test_function():
...
-@freeze_time("2023-01-01", tick=True)
+@time_machine.travel("2023-01-01", tick=True)
def test_function2():
...
If no positional argument is passed, ``None`` is added as the destination, meaning the current time, matching freezegun’s behaviour of freezing at the current time:
.. code-block:: diff
-@freeze_time()
+@time_machine.travel(None, tick=False)
def test_function():
...
``tz_offset=0`` is dropped, since a zero offset has no effect.
``real_asyncio`` is dropped, whatever its value, since time-machine does not mock ``time.monotonic()``, so asyncio event loops always see real time.
``ignore`` is dropped, since it works around problems with freezegun’s module patching, which time-machine’s C-level mocking doesn’t have.
.. code-block:: diff
-@freeze_time("2023-01-01", tz_offset=0, real_asyncio=True, ignore=["threading"])
+@time_machine.travel("2023-01-01", tick=False)
def test_function():
...
* “Raw use” assignments that bind ``freeze_time()`` to a variable for later ``start()`` and ``stop()`` calls: the assigned call is migrated as above, since ``travel()`` instances have the same ``start()`` / ``stop()`` interface:
.. code-block:: diff
def test_function():
- freezer = freeze_time("2023-01-01")
+ freezer = time_machine.travel("2023-01-01", tick=False)
freezer.start()
...
freezer.stop()
This applies to plain variables, like ``freezer = freeze_time(...)``, checked within the enclosing function or module, and to ``self.`` attributes, checked across the enclosing class, so unittest ``setUp()`` / ``tearDown()`` patterns are covered, including cleanup registrations like ``self.addCleanup(self.freezer.stop)``:
.. code-block:: diff
class TestSomething(TestCase):
def setUp(self):
- self.freezer = freeze_time("2023-01-01")
+ self.freezer = time_machine.travel("2023-01-01", tick=False)
self.freezer.start()
self.addCleanup(self.freezer.stop)
The migration only applies when the variable is used solely for ``start()`` and ``stop()``: as calls with no arguments in statements, or as bare references passed as call arguments.
Other uses, such as ``move_to()`` or ``tick()`` calls, prevent migration, since freezegun’s object has other methods with no equivalent on ``travel()``.
* In context managers that bind the result with ``as``, additionally: calls of the bound variable’s ``tick()`` method -> ``shift()``, with freezegun’s default delta of one second made explicit.
Calls of the ``move_to()`` method are left unchanged, since it behaves the same in both libraries:
.. code-block:: diff
-with freeze_time("2023-01-01") as ft:
+with time_machine.travel("2023-01-01", tick=False) as ft:
ft.move_to("2023-06-01")
- ft.tick()
+ ft.shift(1)
These changes are only applied when the bound variable is used solely for ``move_to()`` calls and ``tick()`` calls as statements, since ``tick()`` returns the new time whilst ``shift()`` returns ``None``, and other freezegun attributes have no equivalent on the object that ``travel()`` yields.
``pytest.mark.freeze_time`` marker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``pytest.mark.freeze_time`` marker from `pytest-freezegun `__ or `pytest-freezer `__ is migrated: ``@pytest.mark.freeze_time(...)`` -> ``@pytest.mark.time_machine(...)``, the marker from time-machine’s :doc:`pytest plugin `.
This migration uses the same argument handling as for ``freeze_time()`` calls:
.. code-block:: diff
-@pytest.mark.freeze_time("2023-01-01")
+@pytest.mark.time_machine("2023-01-01", tick=False)
def test_function():
...
As well as in decorators, the marker is migrated in module-level and class-level ``pytestmark`` assignments, whether assigned alone or within a list or tuple of markers:
.. code-block:: diff
pytestmark = [
- pytest.mark.freeze_time("2023-01-01"),
+ pytest.mark.time_machine("2023-01-01", tick=False),
pytest.mark.django_db,
]
``freezer`` fixture
~~~~~~~~~~~~~~~~~~~
The ``freezer`` fixture from pytest-freezegun or pytest-freezer is migrated to the ``time_machine`` fixture from time-machine’s pytest plugin.
In functions with an argument named ``freezer``, the argument is renamed to ``time_machine`` and calls of the fixture’s methods are migrated:
* ``freezer.move_to(...)`` -> ``time_machine.move_to(..., tick=False)``, again matching freezegun’s default behaviour:
.. code-block:: diff
-def test_function(freezer):
- freezer.move_to("2023-01-01")
+def test_function(time_machine):
+ time_machine.move_to("2023-01-01", tick=False)
``tick=False`` isn’t added in functions using a migrated ``pytest.mark.freeze_time`` marker, since there the fixture inherits the ``tick`` behaviour from the marker:
.. code-block:: diff
-@pytest.mark.freeze_time("2023-01-01")
-def test_function(freezer):
- freezer.move_to("2023-06-01")
+@pytest.mark.time_machine("2023-01-01", tick=False)
+def test_function(time_machine):
+ time_machine.move_to("2023-06-01")
* ``freezer.tick()`` -> ``time_machine.shift(1)``, as for context manager variables, again only for calls as statements:
.. code-block:: diff
-def test_function(freezer):
- freezer.tick()
+def test_function(time_machine):
+ time_machine.shift(1)
Other uses of ``freezer`` are left unchanged, for your linter to flag.
``freeze_time()`` calls within such functions are also left unchanged, because the renamed argument shadows the ``time_machine`` module.
Imports and uses of ``FrozenDateTimeFactory``, freezegun’s class for the ``freezer`` fixture, often used to annotate the fixture argument, are also migrated: uses, including in string annotations, are rewritten to time-machine’s equivalent, ``TimeMachineFixture``, and ``from time_machine import TimeMachineFixture`` replaces the freezegun import:
.. code-block:: diff
-from freezegun.api import FrozenDateTimeFactory
+from time_machine import TimeMachineFixture
-def test_function(freezer: FrozenDateTimeFactory):
- freezer.move_to("2023-01-01")
+def test_function(time_machine: TimeMachineFixture):
+ time_machine.move_to("2023-01-01", tick=False)
Note that the ``time_machine`` fixture doesn’t mock the time until its ``move_to()`` method is called, unlike ``freezer``, which mocks from the start of the test.
Migrated tests that relied on that, for example by calling ``freezer.tick()`` before any ``move_to()``, will need manual adjustment.
The tool is open to extension to cover other compatible changes—PRs welcome!
adamchainz-time-machine-18d4852/docs/pytest_plugin.rst 0000664 0000000 0000000 00000007004 15250103717 0023022 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"
The marker takes the same arguments as :class:`~.travel`, including the ``tick`` argument:
.. code-block:: python
import time
import pytest
@pytest.mark.time_machine(0, tick=False)
def test_delorean_marker_tick():
for _ in range(10):
assert time.time() == 0.0
``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.Traveller` class.
Until you call ``move_to()``, time is not mocked.
The fixture object is an instance of ``time_machine.TimeMachineFixture``, which you can import to annotate the fixture argument:
.. code-block:: python
from time_machine import TimeMachineFixture
def test_delorean(time_machine: TimeMachineFixture): ...
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-18d4852/docs/usage.rst 0000664 0000000 0000000 00000043377 15250103717 0021235 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.
Similarly, each interpreter’s time travelling only affects itself, when using |concurrent.interpreters|__.
Patched functions called in interpreters that are not travelling fall back to the original functions and return the real time.
.. |concurrent.interpreters| replace:: ``concurrent.interpreters``
__ https://docs.python.org/3.14/library/concurrent.interpreters.html
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 have a timezone added per the current :attr:`naive_mode` value (which defaults to ``MIXED``, which treats naive datetimes as UTC).
If it has ``tzinfo`` set to a |zoneinfo-instance|__ or |datetime.UTC|__, the current timezone will also be mocked.
.. |datetime.datetime| replace:: ``datetime.datetime``
__ https://docs.python.org/3/library/datetime.html#datetime.datetime
.. |zoneinfo-instance| replace:: ``zoneinfo.ZoneInfo`` instance
__ https://docs.python.org/3/library/zoneinfo.html#zoneinfo.ZoneInfo
.. |datetime.UTC| replace:: ``datetime.UTC`` (``datetime.timezone.utc``)
__ https://docs.python.org/3/library/datetime.html#datetime.UTC
* A |datetime.date|__.
This will be converted to a datetime with the time 00:00:00, and the timezone set per the current :attr:`naive_mode` value (defaults to MIXED, which treats dates as UTC).
.. |datetime.date| replace:: ``datetime.date``
__ https://docs.python.org/3/library/datetime.html#datetime.date
* A |datetime.timedelta|__.
This will be interpreted relative to the current time.
If already within a ``travel()`` block, the :meth:`Traveller.shift` method is easier to use.
.. |datetime.timedelta| replace:: ``datetime.timedelta``
__ https://docs.python.org/3/library/datetime.html#datetime.timedelta
* A ``float`` or ``int`` specifying a `Unix timestamp `__.
* A ``str``.
This will be parsed with |datetime.fromisoformat()|__ first, which supports ISO 8601 formats like ``YYYY-MM-DD`` and ``YYYY-MM-DDTHH:MM:SS``.
If that parsing fails and `dateutil `__ is installed, the value will be parsed with |dateutil.parse()|__, which supports a wider variety of formats, although beware that some are ambiguous.
In either case, if the result is naive, it will be assumed to be in local time.
.. |datetime.fromisoformat()| replace:: ``datetime.fromisoformat()``
__ https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat
.. |dateutil.parse()| replace:: ``dateutil.parse()``
__ https://dateutil.readthedocs.io/en/stable/parser.html
* ``None``, meaning the current time.
Use this with ``tick=False`` to freeze time at the present moment.
If already time travelling, the current time is the mocked one, as with a ``datetime.timedelta`` destination.
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.date.today()``
* ``datetime.datetime.now()``
* ``datetime.datetime.today()``
* ``datetime.datetime.utcnow()``
* ``time.clock_gettime()`` (only for ``CLOCK_REALTIME``)
* ``time.clock_gettime_ns()`` (only for ``CLOCK_REALTIME``)
* ``time.gmtime()``
* ``time.localtime()``
* ``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()``.
Clock reads from C-level code outside of the standard library are unaffected—see :ref:`Unmocked time sources `.
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 ``Traveller.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:: Traveller
The ``start()`` method and entry of the context manager both return a ``Traveller`` 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
.. attribute:: naive_mode
The ``naive_mode`` attribute controls how naive datetimes are interpreted.
It takes a value from the ``NaiveMode`` enum:
.. class:: NaiveMode
.. attribute:: MIXED
Naive ``datetime`` objects and ``date`` objects are interpreted as UTC.
Naive datetime strings are interpreted as local time.
This mode is the default for backward compatibility with existing behaviour.
.. attribute:: UTC
Naive datetimes are interpreted as UTC, regardless of input type.
This mode provides consistency across all input types, but is inconsistent with Python's default behaviour for naive datetimes.
.. attribute:: LOCAL
All naive datetimes are interpreted as local time.
This mode represents Python’s default behaviour for naive datetimes, as well as how freezegun interprets them.
It’s especially useful when migrating tests from freezegun to time-machine.
.. attribute:: ERROR
Using a naive datetime raises a ``RuntimeError``.
This includes ``dt.date`` objects, as they do not have timezone information.
This mode prevents your test suite from changing meaning depending on the timezone of the machine it is run on.
Use it for maximum reproducibility.
To use a different mode, set the ``time_machine.naive_mode`` attribute to your chosen value, like:
.. code-block:: python
import time_machine
time_machine.naive_mode = time_machine.NaiveMode.LOCAL
This is probably best done at the start of your test suite, such as within module-level code in a |pytest conftest.py file|__.
.. |pytest conftest.py file| replace:: pytest ``conftest.py`` file
__ https://docs.pytest.org/en/stable/reference/fixtures.html#conftest-py-sharing-fixtures-across-multiple-files
To temporarily set a different mode, use |mock.patch.object()|__:
.. |mock.patch.object()| replace:: ``unittest.mock.patch.object()``
__ https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch.object
.. code-block:: python
from unittest import mock
import time_machine
with mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.ERROR):
# Code within this block will use the ERROR mode
with time_machine.travel(...):
...
.. _unmocked-time-sources:
.. note:: **Unmocked time sources**
Because time-machine’s mocking works by replacing the standard library functions, any code that reads the current time another way is unaffected.
This mainly applies to compiled extensions that call the operating system clock directly, bypassing Python’s standard library.
Known examples:
* NumPy’s |np.datetime64|__ with the special values ``"now"`` and ``"today"``, which read the system clock in NumPy’s C code:
.. |np.datetime64| replace:: ``np.datetime64``
__ https://numpy.org/doc/stable/reference/arrays.datetime.html
.. code-block:: python
import numpy as np
import time_machine
with time_machine.travel(0.0):
np.datetime64("now") # returns the real current time
To get a mocked value, construct it from a standard library function instead:
.. code-block:: python
import datetime as dt
import numpy as np
import time_machine
with time_machine.travel(0.0):
np.datetime64(dt.datetime.now())
# numpy.datetime64('1970-01-01T00:00:00.000000')
* SQLite’s `date and time functions `__, such as ``datetime('now')`` and ``CURRENT_TIMESTAMP``.
Even when using the standard library ``sqlite3`` module, where SQLite runs within the current process, these values come from SQLite’s C code:
.. code-block:: python
import sqlite3
import time_machine
connection = sqlite3.connect(":memory:")
with time_machine.travel(0.0):
connection.execute("SELECT datetime('now')").fetchone()
# returns the real current time
To get a mocked value, pass the current time as a parameter, from a standard library function like ``datetime.now()``.
* DuckDB’s SQL functions for the current time, such as ``now()`` and ``current_date``, which similarly come from DuckDB’s compiled code.
Most third-party libraries do use the standard library functions, so time travel affects them as expected.
For example, the current-time functions of pandas (``Timestamp.now()``), arrow, pendulum, and whenever are all mocked correctly.
If you find another notable case of a popular library that reads the time in an unmocked way, please report it in `the issue tracker `__.
.. _escape-hatch:
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()``.
When not time-travelling, these functions (except ``is_travelling()``) raise ``ValueError`` to avoid accidental use.
The functions are:
* ``escape_hatch.is_travelling() -> bool``
Returns ``True`` if ``time_machine.travel()`` is active, ``False`` otherwise.
* ``escape_hatch.datetime.date.today()``
Wraps the real ``datetime.date.today()``.
* ``escape_hatch.datetime.datetime.now()``
Wraps the real ``datetime.datetime.now()``.
* ``escape_hatch.datetime.datetime.today()``
Wraps the real ``datetime.datetime.today()``.
* ``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-18d4852/pyproject.toml 0000664 0000000 0000000 00000006255 15250103717 0021355 0 ustar 00root root 0000000 0000000 [build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=77",
]
[project]
name = "time-machine"
version = "3.5.1"
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.10"
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.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Typing :: Typed",
]
optional-dependencies.cli = [
"tokenize-rt",
]
optional-dependencies.dateutil = [
"python-dateutil>=2.8.2",
]
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 = [
"hypothesis",
{ include-group = "test-base" },
]
docs = [
"furo>=2024.8.6",
"sphinx>=7.4.7",
"sphinx-copybutton>=0.5.2",
]
build = [
"cibuildwheel>=3.3.1",
]
test-base = [
"coverage[toml]",
"freezegun",
"pytest>=9",
"pytest-randomly",
"python-dateutil",
]
[tool.uv]
dependency-groups.build = { requires-python = ">=3.14" }
[tool.cibuildwheel]
build = [
"cp315-*",
"cp315t-*",
"cp314-*",
"cp314t-*",
"cp313-*",
"cp312-*",
"cp311-*",
"cp310-*",
]
archs = [ "auto64" ]
[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.15"
[tool.mypy]
mypy_path = "src/"
namespace_packages = false
warn_unreachable = true
enable_error_code = [
"ignore-without-code",
"redundant-expr",
"truthy-bool",
]
strict = true
overrides = [ { module = "tests.*", allow_untyped_defs = true } ]
[tool.pytest]
addopts = [ "-p", "pytester" ]
strict = true
[tool.coverage]
run.source = [
"tests",
"time_machine",
]
run.branch = true
run.data_file = ".coverage/cov"
run.parallel = true
paths.source = [
"src",
".tox/**/site-packages",
]
report.skip_covered = true
report.skip_empty = true
report.show_missing = true
[tool.rstcheck]
ignore_directives = [
"autoclass",
"autodata",
]
report_level = "ERROR"
adamchainz-time-machine-18d4852/setup.py 0000664 0000000 0000000 00000003260 15250103717 0020144 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import os
import sys
import tempfile
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
from setuptools.errors import CompileError
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"
)
class BuildExt(build_ext): # type: ignore[misc]
def build_extensions(self) -> None:
if self.compiler.compiler_type != "msvc":
flags = ["-fno-omit-frame-pointer"]
# Unsupported on some architectures, like PowerPC:
if self._has_flag("-mno-omit-leaf-frame-pointer"):
flags.append("-mno-omit-leaf-frame-pointer")
for extension in self.extensions:
extension.extra_compile_args.extend(flags)
super().build_extensions()
def _has_flag(self, flag: str) -> bool:
with tempfile.TemporaryDirectory() as tmpdir:
source = os.path.join(tmpdir, "probe.c")
with open(source, "w") as fp:
fp.write("int main(void) { return 0; }\n")
try:
self.compiler.compile(
[source],
output_dir=tmpdir,
# -Werror since Clang only warns on unknown -m… flags:
extra_postargs=[flag, "-Werror"],
)
except CompileError:
return False
return True
setup(
cmdclass={"build_ext": BuildExt},
ext_modules=[
Extension(
name="_time_machine",
sources=["src/_time_machine.c"],
)
],
)
adamchainz-time-machine-18d4852/src/ 0000775 0000000 0000000 00000000000 15250103717 0017220 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/src/_time_machine.c 0000664 0000000 0000000 00000120440 15250103717 0022146 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;
PyObject *timezone_utc;
PyObject *str_traveller_stack;
PyObject *str_time_ns;
PyObject *str_replace;
PyObject *str_fromtimestamp;
PyObject *tzinfo_kwnames;
PyObject *microsecond_kwnames;
PyObject *nanoseconds_per_second;
PyObject *nanoseconds_per_microsecond;
PyObject *microseconds_per_second;
PyCFunctionObject *datetime_datetime_now;
PyCFunctionObject *datetime_datetime_utcnow;
PyCFunctionObject *date_today;
PyCFunctionObject *time_clock_gettime;
PyCFunctionObject *time_clock_gettime_ns;
PyCFunctionObject *time_gmtime;
PyCFunctionObject *time_localtime;
PyCFunctionObject *time_strftime;
PyCFunctionObject *time_time;
PyCFunctionObject *time_time_ns;
// Whether this interpreter has patched the date and time functions
int patched;
} _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;
}
/*
Original method pointers from the patched date and time functions. These
point to static C functions shared by every interpreter's copy of the
datetime and time modules, so, unlike Python objects, they are safe to
store in process-wide statics. They are captured at the first patch() and
never reset, since unpatched method defs always hold these same pointers.
*/
#if PY_VERSION_HEX >= 0x030d00a4
static PyCFunctionFastWithKeywords original_now = NULL;
#else
static _PyCFunctionFastWithKeywords original_now = NULL;
#endif
static PyCFunction original_utcnow = NULL;
static PyCFunction original_date_today = NULL;
static PyCFunction original_clock_gettime = NULL;
static PyCFunction original_clock_gettime_ns = NULL;
static PyCFunction original_gmtime = NULL;
static PyCFunction original_localtime = NULL;
static PyCFunction original_strftime = NULL;
static PyCFunction original_time = NULL;
static PyCFunction original_time_ns = NULL;
// time.CLOCK_REALTIME, the same for every interpreter. Not always available,
// e.g. on builds against old macOS = official Python.org installer.
static int have_clock_realtime = 0;
static long clock_realtime = 0;
/*
Mutex serializing patch() and unpatch() across interpreters, which may
run concurrently under per-interpreter GILs (Python 3.12+) or
free-threading. Use natively static-initializable locks since PyMutex is
only available on Python 3.13+. The critical sections only read and write
C pointers, making no blocking calls, so holding a (shared) GIL whilst
waiting for this mutex cannot deadlock.
*/
#ifdef MS_WINDOWS
#include
static SRWLOCK patch_mutex = SRWLOCK_INIT;
static inline void
patch_mutex_lock(void)
{
AcquireSRWLockExclusive(&patch_mutex);
}
static inline void
patch_mutex_unlock(void)
{
ReleaseSRWLockExclusive(&patch_mutex);
}
#else
#include
static pthread_mutex_t patch_mutex = PTHREAD_MUTEX_INITIALIZER;
static inline void
patch_mutex_lock(void)
{
pthread_mutex_lock(&patch_mutex);
}
static inline void
patch_mutex_unlock(void)
{
pthread_mutex_unlock(&patch_mutex);
}
#endif
/*
Helpers for the patched functions. These functions are swapped into other
modules' functions, so they don't receive this module as 'self'. Instead
they look for the time_machine module in sys.modules and this module's
state through its `_time_machine` attribute. This finds the state of the
current interpreter's copy of the module, so all cached objects are only
used within the interpreter that created them.
*/
/*
Return the current interpreter's active traveller,
time_machine.traveller_stack[-1], as a new reference, and set *state to
the current interpreter's module state.
Return NULL, with no exception set, if the current interpreter is not
time travelling: because time_machine is not imported in it, or no travel
is in progress. Patching applies process-wide, so this happens when
another interpreter is travelling and this one is not. Callers should
then fall back to the original functions.
*/
static PyObject *
_time_machine_current_traveller(_time_machine_state **state)
{
PyObject *name = PyUnicode_FromString("time_machine");
if (name == NULL) {
PyErr_Clear();
return NULL;
}
// Only look in sys.modules, to avoid triggering an import of
// time_machine in interpreters that don't use it.
PyObject *time_machine_module = PyImport_GetModule(name);
Py_DECREF(name);
if (time_machine_module == NULL) {
PyErr_Clear();
return NULL;
}
PyObject *c_module = PyObject_GetAttrString(time_machine_module, "_time_machine");
if (c_module == NULL) {
PyErr_Clear();
Py_DECREF(time_machine_module);
return NULL;
}
*state = (_time_machine_state *)PyModule_GetState(c_module);
// The references in sys.modules and the time_machine module keep this
// module, and thus its state, alive.
Py_DECREF(c_module);
if (*state == NULL || (*state)->str_traveller_stack == NULL) {
PyErr_Clear();
Py_DECREF(time_machine_module);
return NULL;
}
PyObject *traveller_stack =
PyObject_GetAttr(time_machine_module, (*state)->str_traveller_stack);
Py_DECREF(time_machine_module);
if (traveller_stack == NULL) {
PyErr_Clear();
return NULL;
}
PyObject *traveller = PySequence_GetItem(traveller_stack, -1);
Py_DECREF(traveller_stack);
if (traveller == NULL) {
PyErr_Clear();
return NULL;
}
return traveller;
}
/* Call traveller.time_ns() */
static PyObject *
_time_machine_traveller_time_ns(PyObject *traveller, _time_machine_state *state)
{
return PyObject_VectorcallMethod(
state->str_time_ns, &traveller, 1 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL);
}
/* Compute traveller.time_ns() / NANOSECONDS_PER_SECOND */
static PyObject *
_time_machine_traveller_time(PyObject *traveller, _time_machine_state *state)
{
PyObject *time_ns = _time_machine_traveller_time_ns(traveller, state);
if (time_ns == NULL) {
return NULL;
}
PyObject *result = PyNumber_TrueDivide(time_ns, state->nanoseconds_per_second);
Py_DECREF(time_ns);
return result;
}
/* Compute traveller.time_ns() // NANOSECONDS_PER_SECOND */
static PyObject *
_time_machine_traveller_seconds(PyObject *traveller, _time_machine_state *state)
{
PyObject *time_ns = _time_machine_traveller_time_ns(traveller, state);
if (time_ns == NULL) {
return NULL;
}
PyObject *result = PyNumber_FloorDivide(time_ns, state->nanoseconds_per_second);
Py_DECREF(time_ns);
return result;
}
/*
Build the exact datetime for the traveller's current time:
cls.fromtimestamp(seconds, tz).replace(microsecond=microseconds)
*/
static PyObject *
_time_machine_traveller_datetime(
PyObject *cls, PyObject *tz, PyObject *traveller, _time_machine_state *state)
{
PyObject *time_ns = _time_machine_traveller_time_ns(traveller, state);
if (time_ns == NULL) {
return NULL;
}
PyObject *total_microseconds =
PyNumber_FloorDivide(time_ns, state->nanoseconds_per_microsecond);
Py_DECREF(time_ns);
if (total_microseconds == NULL) {
return NULL;
}
PyObject *seconds_microseconds =
PyNumber_Divmod(total_microseconds, state->microseconds_per_second);
Py_DECREF(total_microseconds);
if (seconds_microseconds == NULL) {
return NULL;
}
PyObject *seconds = PyTuple_GET_ITEM(seconds_microseconds, 0);
PyObject *microseconds = PyTuple_GET_ITEM(seconds_microseconds, 1);
PyObject *fromtimestamp_stack[3] = {cls, seconds, tz};
PyObject *whole_second = PyObject_VectorcallMethod(state->str_fromtimestamp,
fromtimestamp_stack,
3 | PY_VECTORCALL_ARGUMENTS_OFFSET,
NULL);
if (whole_second == NULL) {
Py_DECREF(seconds_microseconds);
return NULL;
}
PyObject *replace_stack[2] = {whole_second, microseconds};
PyObject *result = PyObject_VectorcallMethod(state->str_replace,
replace_stack,
1 | PY_VECTORCALL_ARGUMENTS_OFFSET,
state->microsecond_kwnames);
Py_DECREF(whole_second);
Py_DECREF(seconds_microseconds);
return result;
}
/* datetime.datetime.now() */
static PyObject *
_time_machine_now(
PyTypeObject *type, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller == NULL) {
return original_now((PyObject *)type, args, nargs, kwnames);
}
PyObject *tz = Py_None;
Py_ssize_t nkwargs = (kwnames != NULL) ? PyTuple_GET_SIZE(kwnames) : 0;
for (Py_ssize_t i = 0; i < nkwargs; i++) {
PyObject *name = PyTuple_GET_ITEM(kwnames, i);
if (PyUnicode_CompareWithASCIIString(name, "tz") != 0) {
PyErr_Format(
PyExc_TypeError, "now() got an unexpected keyword argument '%U'", name);
Py_DECREF(traveller);
return NULL;
}
tz = args[nargs + i];
}
if (nargs + nkwargs > 1) {
PyErr_Format(
PyExc_TypeError, "now() takes at most 1 argument (%zd given)", nargs + nkwargs);
Py_DECREF(traveller);
return NULL;
}
if (nargs == 1) {
tz = args[0];
}
PyObject *result =
_time_machine_traveller_datetime((PyObject *)type, tz, traveller, state);
Py_DECREF(traveller);
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);
if (!state->patched) {
PyErr_SetString(PyExc_ValueError, "Not currently time-travelling.");
return NULL;
}
PyObject *result = 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() */
/* Return aware.replace(tzinfo=None), stealing the reference to aware. */
static PyObject *
_time_machine_drop_tzinfo(PyObject *aware, _time_machine_state *state)
{
PyObject *stack[2] = {aware, Py_None};
PyObject *result = PyObject_VectorcallMethod(
state->str_replace, stack, 1 | PY_VECTORCALL_ARGUMENTS_OFFSET, state->tzinfo_kwnames);
Py_DECREF(aware);
return result;
}
/*
Copy Python 3.12’s DeprecationWarning for datetime.datetime.utcnow().
Returns 0 on success, -1 with an exception set, like PyErr_WarnEx().
*/
static int
_time_machine_warn_utcnow_deprecated(Py_ssize_t stacklevel)
{
#if PY_VERSION_HEX >= 0x030c0000
return PyErr_WarnEx(PyExc_DeprecationWarning,
"datetime.datetime.utcnow() is deprecated and scheduled for removal in "
"a future version. Use timezone-aware objects to represent datetimes "
"in UTC: datetime.datetime.now(datetime.UTC).",
stacklevel);
#else
(void)stacklevel;
return 0;
#endif
}
static PyObject *
_time_machine_utcnow(PyObject *cls, PyObject *args)
{
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller == NULL) {
return original_utcnow(cls, args);
}
// Warn as the original function would, pointing at its caller.
if (_time_machine_warn_utcnow_deprecated(1) < 0) {
Py_DECREF(traveller);
return NULL;
}
PyObject *aware =
_time_machine_traveller_datetime(cls, state->timezone_utc, traveller, state);
Py_DECREF(traveller);
if (aware == NULL) {
return NULL;
}
// aware.replace(tzinfo=None)
return _time_machine_drop_tzinfo(aware, state);
}
static PyObject *
_time_machine_original_utcnow(PyObject *module, PyObject *args)
{
_time_machine_state *state = get_time_machine_state(module);
if (!state->patched) {
PyErr_SetString(PyExc_ValueError, "Not currently time-travelling.");
return NULL;
}
/*
Warn pointing at the caller of the Python wrapper in time_machine that
calls this function (stacklevel 2), rather than the wrapper itself.
*/
if (_time_machine_warn_utcnow_deprecated(2) < 0) {
return NULL;
}
/*
Calling the original datetime.datetime.utcnow() would raise a second,
misattributed DeprecationWarning on Python 3.12+. Use the original
datetime.datetime.now(timezone.utc) instead, which is equivalent apart
from returning an aware datetime.
*/
PyObject *now_args[1] = {state->timezone_utc};
PyObject *aware = original_now(state->datetime_class, now_args, 1, NULL);
if (aware == NULL) {
return NULL;
}
return _time_machine_drop_tzinfo(aware, state);
}
PyDoc_STRVAR(original_utcnow_doc,
"original_utcnow() -> datetime\n\
\n\
Return what datetime.datetime.utcnow() would, after patching.");
/* datetime.date.today() and datetime.datetime.today()
* Note: datetime.datetime doesn't define its own today(), it inherits from date.
* So we patch date.today() with a single wrapper that handles cls being date,
* datetime, or a subclass of either.
*/
static PyObject *
_time_machine_today(PyObject *cls, PyObject *args)
{
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller == NULL) {
return original_date_today(cls, args);
}
// cls is always a type, since today() is a classmethod.
if (PyType_IsSubtype((PyTypeObject *)cls, (PyTypeObject *)state->datetime_class)) {
// datetime.today() is datetime.now() without a timezone. Build it
// exactly, since a float timestamp can round the microseconds.
PyObject *result = _time_machine_traveller_datetime(cls, Py_None, traveller, state);
Py_DECREF(traveller);
return result;
}
// date.fromtimestamp() ignores the fractional part of the timestamp, so
// pass whole seconds, which are exact.
PyObject *timestamp = _time_machine_traveller_seconds(traveller, state);
Py_DECREF(traveller);
if (timestamp == NULL) {
return NULL;
}
PyObject *stack[2] = {cls, timestamp};
PyObject *result = PyObject_VectorcallMethod(
state->str_fromtimestamp, stack, 2 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL);
Py_DECREF(timestamp);
return result;
}
/* time.clock_gettime() */
static PyObject *
_time_machine_clock_gettime(PyObject *self, PyObject *args)
{
#if PY_VERSION_HEX >= 0x030d00a2
// METH_O - args is the clk_id itself
PyObject *clk_id_obj = args;
#else
// METH_VARARGS - args is a tuple
PyObject *clk_id_obj = NULL;
if (PyTuple_GET_SIZE(args) == 1) {
clk_id_obj = PyTuple_GET_ITEM(args, 0);
}
#endif
if (clk_id_obj != NULL && PyLong_Check(clk_id_obj)) {
int overflow = 0;
long clk_id = PyLong_AsLongAndOverflow(clk_id_obj, &overflow);
if (!overflow && have_clock_realtime && clk_id == clock_realtime) {
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller != NULL) {
PyObject *result = _time_machine_traveller_time(traveller, state);
Py_DECREF(traveller);
return result;
}
}
// Fall through: non-realtime clocks, out-of-range values, and
// interpreters that are not travelling get the original function's
// behaviour, including its error messages.
}
return original_clock_gettime(self, args);
}
static PyObject *
_time_machine_original_clock_gettime(PyObject *module, PyObject *args)
{
_time_machine_state *state = get_time_machine_state(module);
if (!state->patched) {
PyErr_SetString(PyExc_ValueError, "Not currently time-travelling.");
return NULL;
}
PyObject *result = 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)
{
#if PY_VERSION_HEX >= 0x030d00a2
// METH_O - args is the clk_id itself
PyObject *clk_id_obj = args;
#else
// METH_VARARGS - args is a tuple
PyObject *clk_id_obj = NULL;
if (PyTuple_GET_SIZE(args) == 1) {
clk_id_obj = PyTuple_GET_ITEM(args, 0);
}
#endif
if (clk_id_obj != NULL && PyLong_Check(clk_id_obj)) {
int overflow = 0;
long clk_id = PyLong_AsLongAndOverflow(clk_id_obj, &overflow);
if (!overflow && have_clock_realtime && clk_id == clock_realtime) {
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller != NULL) {
PyObject *result = _time_machine_traveller_time_ns(traveller, state);
Py_DECREF(traveller);
return result;
}
}
// Fall through: non-realtime clocks, out-of-range values, and
// interpreters that are not travelling get the original function's
// behaviour, including its error messages.
}
return original_clock_gettime_ns(self, args);
}
static PyObject *
_time_machine_original_clock_gettime_ns(PyObject *module, PyObject *args)
{
_time_machine_state *state = get_time_machine_state(module);
if (!state->patched) {
PyErr_SetString(PyExc_ValueError, "Not currently time-travelling.");
return NULL;
}
PyObject *result = 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)
{
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
if (nargs > 1 || (nargs == 1 && PyTuple_GET_ITEM(args, 0) != Py_None)) {
// Pass through, including invalid arguments for their error messages.
return original_gmtime(self, args);
}
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller == NULL) {
return original_gmtime(self, args);
}
PyObject *timestamp = _time_machine_traveller_seconds(traveller, state);
Py_DECREF(traveller);
if (timestamp == NULL) {
return NULL;
}
PyObject *new_args = PyTuple_Pack(1, timestamp);
Py_DECREF(timestamp);
if (new_args == NULL) {
return NULL;
}
PyObject *result = original_gmtime(self, new_args);
Py_DECREF(new_args);
return result;
}
static PyObject *
_time_machine_original_gmtime(PyObject *module, PyObject *args)
{
_time_machine_state *state = get_time_machine_state(module);
if (!state->patched) {
PyErr_SetString(PyExc_ValueError, "Not currently time-travelling.");
return NULL;
}
PyObject *result = 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)
{
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
if (nargs > 1 || (nargs == 1 && PyTuple_GET_ITEM(args, 0) != Py_None)) {
// Pass through, including invalid arguments for their error messages.
return original_localtime(self, args);
}
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller == NULL) {
return original_localtime(self, args);
}
PyObject *timestamp = _time_machine_traveller_seconds(traveller, state);
Py_DECREF(traveller);
if (timestamp == NULL) {
return NULL;
}
PyObject *new_args = PyTuple_Pack(1, timestamp);
Py_DECREF(timestamp);
if (new_args == NULL) {
return NULL;
}
PyObject *result = original_localtime(self, new_args);
Py_DECREF(new_args);
return result;
}
static PyObject *
_time_machine_original_localtime(PyObject *module, PyObject *args)
{
_time_machine_state *state = get_time_machine_state(module);
if (!state->patched) {
PyErr_SetString(PyExc_ValueError, "Not currently time-travelling.");
return NULL;
}
PyObject *result = 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.strftime() */
static PyObject *
_time_machine_strftime(PyObject *self, PyObject *args)
{
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
if (nargs != 1) {
// Pass through when a time tuple is given, including invalid
// arguments for their error messages. Unlike gmtime() and
// localtime(), strftime() does not accept None to mean the current
// time, so an explicit None is passed through for its TypeError.
return original_strftime(self, args);
}
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller == NULL) {
return original_strftime(self, args);
}
// time.strftime(format, time.localtime(traveller_seconds))
PyObject *timestamp = _time_machine_traveller_seconds(traveller, state);
Py_DECREF(traveller);
if (timestamp == NULL) {
return NULL;
}
PyObject *localtime_args = PyTuple_Pack(1, timestamp);
Py_DECREF(timestamp);
if (localtime_args == NULL) {
return NULL;
}
PyObject *local_time = original_localtime(self, localtime_args);
Py_DECREF(localtime_args);
if (local_time == NULL) {
return NULL;
}
PyObject *new_args = PyTuple_Pack(2, PyTuple_GET_ITEM(args, 0), local_time);
Py_DECREF(local_time);
if (new_args == NULL) {
return NULL;
}
PyObject *result = original_strftime(self, new_args);
Py_DECREF(new_args);
return result;
}
static PyObject *
_time_machine_original_strftime(PyObject *module, PyObject *args)
{
_time_machine_state *state = get_time_machine_state(module);
if (!state->patched) {
PyErr_SetString(PyExc_ValueError, "Not currently time-travelling.");
return NULL;
}
PyObject *result = 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)
{
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller == NULL) {
return original_time(self, args);
}
PyObject *result = _time_machine_traveller_time(traveller, state);
Py_DECREF(traveller);
return result;
}
static PyObject *
_time_machine_original_time(PyObject *module, PyObject *args)
{
_time_machine_state *state = get_time_machine_state(module);
if (!state->patched) {
PyErr_SetString(PyExc_ValueError, "Not currently time-travelling.");
return NULL;
}
PyObject *result = 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)
{
_time_machine_state *state;
PyObject *traveller = _time_machine_current_traveller(&state);
if (traveller == NULL) {
return original_time_ns(self, args);
}
PyObject *result = _time_machine_traveller_time_ns(traveller, state);
Py_DECREF(traveller);
return result;
}
static PyObject *
_time_machine_original_time_ns(PyObject *module, PyObject *args)
{
_time_machine_state *state = get_time_machine_state(module);
if (!state->patched) {
PyErr_SetString(PyExc_ValueError, "Not currently time-travelling.");
return NULL;
}
PyObject *result = 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;
}
patch_mutex_lock();
if (state->patched) {
patch_mutex_unlock();
Py_RETURN_NONE;
}
/*
Capture the original method pointers into the process-wide statics
before swapping in the patched functions. The ml_meth checks make
capture idempotent: if another interpreter has already patched, the
patched functions are never captured as originals.
*/
if (state->date_today->m_ml->ml_meth != _time_machine_today) {
original_date_today = state->date_today->m_ml->ml_meth;
}
state->date_today->m_ml->ml_meth = _time_machine_today;
if (state->datetime_datetime_now->m_ml->ml_meth != (PyCFunction)_time_machine_now) {
#if PY_VERSION_HEX >= 0x030d00a4
original_now =
(PyCFunctionFastWithKeywords)state->datetime_datetime_now->m_ml->ml_meth;
#else
original_now =
(_PyCFunctionFastWithKeywords)state->datetime_datetime_now->m_ml->ml_meth;
#endif
}
state->datetime_datetime_now->m_ml->ml_meth = (PyCFunction)_time_machine_now;
if (state->datetime_datetime_utcnow->m_ml->ml_meth != _time_machine_utcnow) {
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) {
if (state->time_clock_gettime->m_ml->ml_meth != _time_machine_clock_gettime) {
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) {
if (state->time_clock_gettime_ns->m_ml->ml_meth != _time_machine_clock_gettime_ns) {
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;
}
if (state->time_gmtime->m_ml->ml_meth != _time_machine_gmtime) {
original_gmtime = state->time_gmtime->m_ml->ml_meth;
}
state->time_gmtime->m_ml->ml_meth = _time_machine_gmtime;
if (state->time_localtime->m_ml->ml_meth != _time_machine_localtime) {
original_localtime = state->time_localtime->m_ml->ml_meth;
}
state->time_localtime->m_ml->ml_meth = _time_machine_localtime;
if (state->time_strftime->m_ml->ml_meth != _time_machine_strftime) {
original_strftime = state->time_strftime->m_ml->ml_meth;
}
state->time_strftime->m_ml->ml_meth = _time_machine_strftime;
if (state->time_time->m_ml->ml_meth != _time_machine_time) {
original_time = state->time_time->m_ml->ml_meth;
}
state->time_time->m_ml->ml_meth = _time_machine_time;
if (state->time_time_ns->m_ml->ml_meth != _time_machine_time_ns) {
original_time_ns = state->time_time_ns->m_ml->ml_meth;
}
state->time_time_ns->m_ml->ml_meth = _time_machine_time_ns;
state->patched = 1;
patch_mutex_unlock();
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;
}
patch_mutex_lock();
if (!state->patched) {
patch_mutex_unlock();
Py_RETURN_NONE;
}
/*
Restore the original method pointers from the process-wide statics,
which are deliberately not reset: the patched functions use them to
fall back for interpreters that are not travelling, which may be
while another interpreter remains patched.
*/
state->datetime_datetime_now->m_ml->ml_meth = (PyCFunction)original_now;
state->datetime_datetime_utcnow->m_ml->ml_meth = original_utcnow;
state->date_today->m_ml->ml_meth = original_date_today;
/*
time.clock_gettime(), only available on Unix platforms.
*/
if (state->time_clock_gettime != NULL) {
state->time_clock_gettime->m_ml->ml_meth = original_clock_gettime;
}
/*
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 = original_clock_gettime_ns;
}
state->time_gmtime->m_ml->ml_meth = original_gmtime;
state->time_localtime->m_ml->ml_meth = original_localtime;
state->time_strftime->m_ml->ml_meth = original_strftime;
state->time_time->m_ml->ml_meth = original_time;
state->time_time_ns->m_ml->ml_meth = original_time_ns;
state->patched = 0;
patch_mutex_unlock();
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_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->str_traveller_stack = PyUnicode_InternFromString("traveller_stack");
if (state->str_traveller_stack == NULL) {
goto error;
}
state->str_time_ns = PyUnicode_InternFromString("time_ns");
if (state->str_time_ns == NULL) {
goto error;
}
state->str_replace = PyUnicode_InternFromString("replace");
if (state->str_replace == NULL) {
goto error;
}
state->str_fromtimestamp = PyUnicode_InternFromString("fromtimestamp");
if (state->str_fromtimestamp == NULL) {
goto error;
}
PyObject *str_tzinfo = PyUnicode_InternFromString("tzinfo");
if (str_tzinfo == NULL) {
goto error;
}
state->tzinfo_kwnames = PyTuple_Pack(1, str_tzinfo);
Py_DECREF(str_tzinfo);
if (state->tzinfo_kwnames == NULL) {
goto error;
}
state->nanoseconds_per_second = PyLong_FromLong(1000000000L);
if (state->nanoseconds_per_second == NULL) {
goto error;
}
PyObject *str_microsecond = PyUnicode_InternFromString("microsecond");
if (str_microsecond == NULL) {
goto error;
}
state->microsecond_kwnames = PyTuple_Pack(1, str_microsecond);
Py_DECREF(str_microsecond);
if (state->microsecond_kwnames == NULL) {
goto error;
}
state->nanoseconds_per_microsecond = PyLong_FromLong(1000L);
if (state->nanoseconds_per_microsecond == NULL) {
goto error;
}
state->microseconds_per_second = PyLong_FromLong(1000000L);
if (state->microseconds_per_second == NULL) {
goto error;
}
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;
}
PyObject *timezone_class = PyObject_GetAttrString(state->datetime_module, "timezone");
if (timezone_class == NULL) {
goto error;
}
state->timezone_utc = PyObject_GetAttrString(timezone_class, "utc");
Py_DECREF(timezone_class);
if (state->timezone_utc == NULL) {
goto error;
}
PyObject *date_class = PyObject_GetAttrString(state->datetime_module, "date");
if (date_class == NULL) {
goto error;
}
state->date_today = (PyCFunctionObject *)PyObject_GetAttrString(date_class, "today");
Py_DECREF(date_class);
if (state->date_today == NULL) {
goto error;
}
state->time_module = PyImport_ImportModule("time");
if (state->time_module == NULL) {
goto error;
}
PyObject *clock_realtime_obj =
PyObject_GetAttrString(state->time_module, "CLOCK_REALTIME");
if (clock_realtime_obj == NULL) {
if (!PyErr_ExceptionMatches(PyExc_AttributeError)) {
goto error;
}
// time.CLOCK_REALTIME is not always available, per the comment on
// the have_clock_realtime static.
PyErr_Clear();
}
else {
clock_realtime = PyLong_AsLong(clock_realtime_obj);
Py_DECREF(clock_realtime_obj);
if (clock_realtime == -1 && PyErr_Occurred()) {
goto error;
}
have_clock_realtime = 1;
}
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_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->timezone_utc);
Py_CLEAR(state->str_traveller_stack);
Py_CLEAR(state->str_time_ns);
Py_CLEAR(state->str_replace);
Py_CLEAR(state->str_fromtimestamp);
Py_CLEAR(state->tzinfo_kwnames);
Py_CLEAR(state->microsecond_kwnames);
Py_CLEAR(state->nanoseconds_per_second);
Py_CLEAR(state->nanoseconds_per_microsecond);
Py_CLEAR(state->microseconds_per_second);
Py_CLEAR(state->datetime_datetime_now);
Py_CLEAR(state->datetime_datetime_utcnow);
Py_CLEAR(state->date_today);
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_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->timezone_utc);
Py_VISIT(state->str_traveller_stack);
Py_VISIT(state->str_time_ns);
Py_VISIT(state->str_replace);
Py_VISIT(state->str_fromtimestamp);
Py_VISIT(state->tzinfo_kwnames);
Py_VISIT(state->microsecond_kwnames);
Py_VISIT(state->nanoseconds_per_second);
Py_VISIT(state->nanoseconds_per_microsecond);
Py_VISIT(state->microseconds_per_second);
Py_VISIT(state->datetime_datetime_now);
Py_VISIT(state->datetime_datetime_utcnow);
Py_VISIT(state->date_today);
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_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->timezone_utc);
Py_CLEAR(state->str_traveller_stack);
Py_CLEAR(state->str_time_ns);
Py_CLEAR(state->str_replace);
Py_CLEAR(state->str_fromtimestamp);
Py_CLEAR(state->tzinfo_kwnames);
Py_CLEAR(state->microsecond_kwnames);
Py_CLEAR(state->nanoseconds_per_second);
Py_CLEAR(state->nanoseconds_per_microsecond);
Py_CLEAR(state->microseconds_per_second);
Py_CLEAR(state->datetime_datetime_now);
Py_CLEAR(state->datetime_datetime_utcnow);
Py_CLEAR(state->date_today);
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_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.12+, declare support for isolated subinterpreters, which may
// each import this module.
#if PY_VERSION_HEX >= 0x030c0000
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
#endif
// 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)
{
return PyModuleDef_Init(&_time_machine_module);
}
adamchainz-time-machine-18d4852/src/time_machine/ 0000775 0000000 0000000 00000000000 15250103717 0021642 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/src/time_machine/__init__.py 0000664 0000000 0000000 00000045147 15250103717 0023766 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, Callable, Generator
from collections.abc import Generator as TypingGenerator
from enum import Enum
from time import gmtime as orig_gmtime
from time import struct_time
from types import TracebackType
from typing import Any, TypeAlias, TypeVar, cast, overload
from unittest import TestCase
from zoneinfo import ZoneInfo
import _time_machine
if sys.version_info >= (3, 11):
from typing import assert_never
else:
def assert_never(_: Any) -> None: # pragma: no cover
pass
try:
from time import tzset
except ImportError: # pragma: no cover
HAVE_TZSET = False # Windows
else:
HAVE_TZSET = True
try:
from dateutil.parser import parse as parse_datetime
except ImportError: # pragma: no cover
HAVE_DATEUTIL = False
else:
HAVE_DATEUTIL = True
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
)
# uuid1(), uuid6(), and uuid7() cache the timestamp of the last value they
# generated in a module-level global, and never generate a value before it.
# We therefore clear those caches when we travel backwards in time, so that
# those functions generate values with correct timestamps.
_uuid_dict = uuid.__dict__
_uuid_reset = {"_last_timestamp": None}
if sys.version_info >= (3, 14):
_uuid_reset["_last_timestamp_v6"] = None
_uuid_reset["_last_timestamp_v7"] = None
def _reset_uuid_timestamps() -> None:
_uuid_dict.update(_uuid_reset)
DestinationBaseType: TypeAlias = (
int | float | dt.datetime | dt.timedelta | dt.date | str | None
)
DestinationType: TypeAlias = (
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]
class NaiveMode(Enum):
MIXED = 1
UTC = 2
LOCAL = 3
ERROR = 4
naive_mode = NaiveMode.MIXED
def _datetime_to_ns(destination: dt.datetime) -> int:
# timestamp() on a whole-second datetime returns an integer-valued float,
# exact for any supported year since UTC offsets are whole seconds, so the
# nanosecond timestamp can be computed with exact integer arithmetic.
seconds = int(destination.replace(microsecond=0).timestamp())
return seconds * NANOSECONDS_PER_SECOND + destination.microsecond * 1_000
def _timedelta_to_ns(delta: dt.timedelta) -> int:
return (
delta.days * 86_400 + delta.seconds
) * NANOSECONDS_PER_SECOND + delta.microseconds * 1_000
def extract_timestamp_tzname(
destination: DestinationType,
) -> tuple[int, str | None]:
dest: DestinationBaseType
if isinstance(destination, Generator):
dest = next(destination)
elif callable(destination):
dest = destination()
else:
dest = destination
timestamp_ns: int
tzname: str | None = None
if dest is None:
timestamp_ns = time_module.time_ns()
elif isinstance(dest, int):
timestamp_ns = dest * NANOSECONDS_PER_SECOND
elif isinstance(dest, float):
timestamp_ns = round(dest * NANOSECONDS_PER_SECOND)
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:
if naive_mode == NaiveMode.MIXED or naive_mode == NaiveMode.UTC:
dest = dest.replace(tzinfo=dt.timezone.utc)
elif naive_mode == NaiveMode.LOCAL:
pass
elif naive_mode == NaiveMode.ERROR:
raise RuntimeError(
"Naive datetime provided while time_machine.naive_mode is set to ERROR. "
"Please provide a timezone-aware datetime."
)
else: # pragma: no cover
assert_never(naive_mode)
timestamp_ns = _datetime_to_ns(dest)
elif isinstance(dest, dt.timedelta):
timestamp_ns = time_module.time_ns() + _timedelta_to_ns(dest)
elif isinstance(dest, dt.date):
if naive_mode == NaiveMode.MIXED or naive_mode == NaiveMode.UTC:
timestamp_ns = _datetime_to_ns(
dt.datetime.combine(dest, dt.time(0, 0), tzinfo=dt.timezone.utc)
)
elif naive_mode == NaiveMode.LOCAL:
timestamp_ns = _datetime_to_ns(dt.datetime.combine(dest, dt.time(0, 0)))
elif naive_mode == NaiveMode.ERROR:
raise RuntimeError(
"date object provided while time_machine.naive_mode is set to ERROR. "
"Please provide a timezone-aware datetime."
)
else: # pragma: no cover
assert_never(naive_mode)
elif isinstance(dest, str):
try:
parsed = dt.datetime.fromisoformat(dest)
except ValueError as exc:
if HAVE_DATEUTIL:
try:
parsed = parse_datetime(dest)
except ValueError as dateutil_exc:
raise dateutil_exc from None
else:
raise exc
if parsed.tzinfo is None:
if naive_mode == NaiveMode.MIXED:
# Keep as naive, for backwards compatibility
pass
elif naive_mode == NaiveMode.UTC:
parsed = parsed.replace(tzinfo=dt.timezone.utc)
elif naive_mode == NaiveMode.LOCAL:
pass
elif naive_mode == NaiveMode.ERROR:
raise RuntimeError(
"Naive datetime string provided while time_machine.naive_mode is set to ERROR. "
"Please provide a timezone-aware datetime string."
)
else: # pragma: no cover
assert_never(naive_mode)
timestamp_ns = _datetime_to_ns(parsed)
else:
raise TypeError(f"Unsupported destination {dest!r}")
return timestamp_ns, tzname
class Traveller:
def __init__(
self,
destination_timestamp_ns: int,
destination_tzname: str | None,
tick: bool,
) -> None:
self._destination_timestamp_ns = destination_timestamp_ns
self._destination_tzname = destination_tzname
self._tick = tick
self._requested = False
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):
delta_ns = _timedelta_to_ns(delta)
elif isinstance(delta, int):
delta_ns = delta * NANOSECONDS_PER_SECOND
elif isinstance(delta, float):
delta_ns = round(delta * NANOSECONDS_PER_SECOND)
else:
raise TypeError(f"Unsupported type for delta argument: {delta!r}")
self._destination_timestamp_ns += delta_ns
if delta_ns < 0:
# Moving forwards leaves the cached uuid timestamps in the past, so
# only pay for resetting them when moving backwards.
_reset_uuid_timestamps()
def move_to(
self,
destination: DestinationType,
tick: bool | None = None,
) -> None:
self._stop()
try:
self._destination_timestamp_ns, self._destination_tzname = (
extract_timestamp_tzname(destination)
)
except BaseException:
# Keep travelling to the current destination, including its
# timezone, rather than leaving the timezone unmocked.
self._start()
raise
self._requested = False
self._start()
if tick is not None:
self._tick = tick
def _start(self) -> None:
_reset_uuid_timestamps()
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()
traveller_stack: list[Traveller] = []
original_uuid_generate_time_safe = None
original_uuid_uuid_create = None
class travel:
def __init__(self, destination: DestinationType, *, tick: bool = True) -> None:
self.destination_timestamp_ns, self.destination_tzname = (
extract_timestamp_tzname(destination)
)
self.tick = tick
def start(self) -> Traveller:
if "freezegun" in sys.modules and dt.datetime.__name__ == "FakeDatetime":
raise RuntimeError("time-machine cannot start when freezegun is active.")
if not traveller_stack:
_time_machine.patch()
# 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.
global original_uuid_generate_time_safe
global original_uuid_uuid_create
original_uuid_generate_time_safe = uuid._generate_time_safe # type: ignore[attr-defined]
original_uuid_uuid_create = uuid._UuidCreate # type: ignore[attr-defined]
uuid._generate_time_safe = None # type: ignore[attr-defined]
uuid._UuidCreate = None # type: ignore[attr-defined]
traveller = Traveller(
destination_timestamp_ns=self.destination_timestamp_ns,
destination_tzname=self.destination_tzname,
tick=self.tick,
)
traveller_stack.append(traveller)
traveller._start()
return traveller
def stop(self) -> None:
traveller_stack.pop()._stop()
_reset_uuid_timestamps()
if not traveller_stack:
_time_machine.unpatch()
global original_uuid_generate_time_safe
global original_uuid_uuid_create
uuid._generate_time_safe = original_uuid_generate_time_safe # type: ignore[attr-defined]
uuid._UuidCreate = original_uuid_uuid_create # type: ignore[attr-defined]
original_uuid_generate_time_safe = None
original_uuid_uuid_create = None
def __enter__(self) -> Traveller:
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) -> Traveller:
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 BaseException:
# Stop travlling for any BaseException, not just Exception,
# because pytest.skip() works by raising a BaseException
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:
try:
orig_tearDownClass(cls)
finally:
self.__exit__(*sys.exc_info())
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)
# pytest plugin
class TimeMachineFixture:
traveller: travel | None
traveller_obj: Traveller | None
def __init__(self) -> None:
self.traveller = None
self.traveller_obj = None
def move_to(
self,
destination: DestinationType,
tick: bool | None = None,
) -> None:
if self.traveller is None:
if tick is None:
tick = True
traveller = travel(destination, tick=tick)
self.traveller_obj = traveller.start()
self.traveller = traveller
else:
assert self.traveller_obj is not None
self.traveller_obj.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.traveller_obj is not None
self.traveller_obj.shift(delta=delta)
def stop(self) -> None:
if self.traveller is not None:
self.traveller.stop()
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"
)
@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 _EscapeHatchDatetimeDate:
def today(self) -> dt.date:
# date.today() is equivalent to datetime.now().date().
result: dt.datetime = _time_machine.original_now(None)
return result.date()
class _EscapeHatchDatetimeDatetime:
def now(self, tz: dt.tzinfo | None = None) -> dt.datetime:
result: dt.datetime = _time_machine.original_now(tz)
return result
def today(self) -> dt.datetime:
# datetime.today() is equivalent to datetime.now() without a timezone.
result: dt.datetime = _time_machine.original_now(None)
return result
def utcnow(self) -> dt.datetime:
result: dt.datetime = _time_machine.original_utcnow()
return result
class _EscapeHatchDatetime:
def __init__(self) -> None:
self.date = _EscapeHatchDatetimeDate()
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 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(traveller_stack)
escape_hatch = _EscapeHatch()
adamchainz-time-machine-18d4852/src/time_machine/__main__.py 0000664 0000000 0000000 00000000223 15250103717 0023731 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-18d4852/src/time_machine/cli.py 0000664 0000000 0000000 00000142120 15250103717 0022763 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import argparse
import ast
import re
import sys
import warnings
from bisect import bisect_right
from collections import defaultdict
from collections.abc import Callable, Generator, Mapping, MutableMapping, Sequence
from functools import partial
from operator import itemgetter
from typing import NamedTuple
from tokenize_rt import (
NON_CODING_TOKENS,
UNIMPORTANT_WS,
Offset,
Token,
reversed_enumerate,
src_to_tokens,
tokens_to_src,
)
CODE = "CODE"
DEDENT = "DEDENT"
INDENT = "INDENT"
# The class of freezegun’s pytest fixture, often imported to annotate the
# fixture argument, migrated to time-machine’s equivalent.
FIXTURE_FACTORY = "FrozenDateTimeFactory"
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, reports = 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)
for report in reports:
print(
f"{filename}:{report.lineno}:{report.col}: {report.message}",
file=sys.stderr,
)
return contents_text != contents_text_orig
class Report(NamedTuple):
"""A freezegun-related usage that could not be migrated."""
lineno: int
col: int
message: str
def migrate_contents(contents_text: str) -> tuple[str, list[Report]]:
"""Migrate a single text from freezegun to time-machine."""
try:
ast_obj = ast_parse(contents_text)
except (SyntaxError, ValueError) as exc:
# ValueError comes from e.g. null bytes in the source.
lineno = getattr(exc, "lineno", None) or 1
col = getattr(exc, "offset", None) or 1
msg = getattr(exc, "msg", None) or str(exc)
return contents_text, [Report(lineno, col, f"could not parse file: {msg}")]
callbacks, reports = visit(ast_obj)
if not callbacks:
return contents_text, reports
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
new_text: str = tokens_to_src(tokens)
return new_text, relocate_reports(reports, tokens)
def relocate_reports(reports: list[Report], tokens: list[Token]) -> list[Report]:
"""
Move the given reports, positioned in the original source, to their
positions in the rewritten source, as the user sees them alongside the
rewritten file.
"""
# Map the original positions of the surviving tokens to their new ones,
# per original line, in order.
positions: defaultdict[int, list[tuple[int, int, int]]] = defaultdict(list)
lineno = 1
utf8_byte_offset = 0
for token in tokens:
if token.line is not None:
positions[token.line].append(
(token.utf8_byte_offset, lineno, utf8_byte_offset)
)
src: str = token.src
newlines = src.count("\n")
if newlines:
lineno += newlines
utf8_byte_offset = len(src.rpartition("\n")[2].encode())
else:
utf8_byte_offset += len(src.encode())
relocated = []
for report in reports:
col_offset = report.col - 1
# Find the token containing the reported position. Reported usages
# are not rewritten, so their tokens survive with the same source,
# but on Python < 3.12 names within f-strings have no tokens of
# their own, so the position may lie within a string token.
line_positions = positions[report.lineno]
index = bisect_right(line_positions, col_offset, key=itemgetter(0)) - 1
if index < 0: # pragma: no cover
# No token starts on the line, which can only happen within a
# multi-line string. Leave the report at its original position.
relocated.append(report)
continue
token_offset, new_lineno, new_offset = line_positions[index]
relocated.append(
Report(
new_lineno,
new_offset + (col_offset - token_offset) + 1,
report.message,
)
)
relocated.sort()
return relocated
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]
class FreezerFunction:
"""
Details of a function taking pytest-freezegun / pytest-freezer’s
``freezer`` fixture as an argument.
"""
__slots__ = ("lineno", "end_lineno", "marker_seen")
def __init__(
self, node: ast.FunctionDef | ast.AsyncFunctionDef, *, marker_seen: bool
) -> None:
assert node.end_lineno is not None
self.lineno = node.lineno
self.end_lineno = node.end_lineno
self.marker_seen = marker_seen
class TravellerVar:
"""
Details of a variable bound with ``as`` to a migrated freeze_time()
context manager.
"""
__slots__ = ("name", "lineno", "end_lineno")
def __init__(self, name: str, node: ast.With) -> None:
assert node.end_lineno is not None
self.name = name
self.lineno = node.lineno
self.end_lineno = node.end_lineno
def visit(
tree: ast.Module,
) -> tuple[Mapping[Offset, list[TokenFunc]], list[Report]]:
"""
Visit the AST and return a mapping of callbacks to apply to the tokens,
plus reports of freezegun-related usages that could not be migrated.
"""
ret: defaultdict[Offset, list[TokenFunc]] = defaultdict(list)
freezegun_module_names: set[str] = set()
freeze_time_names: set[str] = set()
report_module_names: set[str] = set()
freezegun_from_imports: list[ast.ImportFrom] = []
freezer_functions: list[FreezerFunction] = []
marker_class_methods: set[ast.FunctionDef | ast.AsyncFunctionDef] = set()
module_marker_seen = False
traveller_vars: list[TravellerVar] = []
module_stmts = module_scope_stmts(tree)
for node in ast.walk(tree):
match node:
case ast.Module():
for stmt in node.body:
if maybe_migrate_pytestmark(ret, stmt):
module_marker_seen = True
case ast.Import():
if (
len(node.names) == 1
and (alias := node.names[0]).name == "freezegun"
):
freezegun_module_names.add(alias.asname or "freezegun")
if alias.asname is None:
ret[ast_start_offset(node)].append(replace_import)
else:
ret[ast_start_offset(node)].append(
partial(replace_aliased_import, node=node)
)
else:
# Imports of freezegun that cannot be migrated, like
# `import freezegun, os` or `import freezegun.config`,
# still have their bound names tracked for reporting.
report_module_names.update(
alias.asname or "freezegun"
for alias in node.names
if alias.name == "freezegun"
or alias.name.startswith("freezegun.")
)
case ast.ImportFrom() if (
node.level == 0
and node.module in ("freezegun", "freezegun.api")
and any(
alias.name in ("freeze_time", FIXTURE_FACTORY)
for alias in node.names
)
):
freeze_time_names.update(
alias.asname or alias.name
for alias in node.names
if alias.name == "freeze_time"
)
freezegun_from_imports.append(node)
case ast.FunctionDef() | ast.AsyncFunctionDef():
marker_seen = module_marker_seen or node in marker_class_methods
for decorator in node.decorator_list:
if maybe_migrate_marker(ret, decorator):
marker_seen = True
else:
maybe_migrate_call(
ret,
decorator,
freezegun_module_names=freezegun_module_names,
freeze_time_names=freeze_time_names,
freezer_functions=freezer_functions,
)
freezer_args = [
arg
for arg in (*node.args.args, *node.args.kwonlyargs)
if arg.arg == "freezer"
]
if freezer_args and not any(
arg.arg == "time_machine" for arg in all_arguments(node)
):
for arg in freezer_args:
ret[ast_start_offset(arg)].append(replace_freezer)
freezer_functions.append(
FreezerFunction(node, marker_seen=marker_seen)
)
case ast.ClassDef():
class_marker_seen = False
if node.decorator_list:
unittest_class = looks_like_unittest_class(node)
for decorator in node.decorator_list:
if maybe_migrate_marker(ret, decorator):
class_marker_seen = True
elif unittest_class:
maybe_migrate_call(
ret,
decorator,
freezegun_module_names=freezegun_module_names,
freeze_time_names=freeze_time_names,
freezer_functions=freezer_functions,
)
for stmt in node.body:
if maybe_migrate_pytestmark(ret, stmt):
class_marker_seen = True
if class_marker_seen:
marker_class_methods.update(
stmt
for stmt in node.body
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef))
)
case ast.With():
for item in node.items:
match item.optional_vars:
case None:
maybe_migrate_call(
ret,
item.context_expr,
freezegun_module_names=freezegun_module_names,
freeze_time_names=freeze_time_names,
freezer_functions=freezer_functions,
)
case ast.Name(id=name) as binding:
if traveller_var_uses_compatible(
node, binding
) and maybe_migrate_call(
ret,
item.context_expr,
freezegun_module_names=freezegun_module_names,
freeze_time_names=freeze_time_names,
freezer_functions=freezer_functions,
):
traveller_vars.append(TravellerVar(name, node))
case ast.Expr(
value=ast.Call(
func=ast.Attribute(
value=ast.Name(id=receiver) as receiver_node,
attr="tick",
)
) as call_node
) if migratable_tick_call(call_node):
# tick() is only migrated as a statement, since shift() does
# not return the new time.
if receiver == "freezer" and find_freezer_function(
freezer_functions, call_node
):
ret[ast_start_offset(receiver_node)].append(replace_freezer)
ret[ast_start_offset(receiver_node)].append(
partial(replace_tick_with_shift, node=call_node)
)
elif any(
traveller_var.name == receiver
and (
traveller_var.lineno
<= call_node.lineno
<= traveller_var.end_lineno
)
for traveller_var in traveller_vars
):
ret[ast_start_offset(receiver_node)].append(
partial(replace_tick_with_shift, node=call_node)
)
case ast.Call(
func=ast.Attribute(
value=ast.Name(id="freezer") as receiver_node,
attr="move_to",
)
) if (
len(node.args) == 1
and not node.keywords
and (freezer_function := find_freezer_function(freezer_functions, node))
is not None
):
ret[ast_start_offset(receiver_node)].append(replace_freezer)
if not freezer_function.marker_seen:
ret[ast_start_offset(node)].append(
partial(add_tick_false, node=node)
)
# Process from-imports of freezegun after the main walk, since migrating
# FrozenDateTimeFactory uses depends on the rebindings and imports of the
# whole module.
fixture_bound: set[str] = set()
for import_node in freezegun_from_imports:
for alias in import_node.names:
if alias.name == FIXTURE_FACTORY:
fixture_bound.add(alias.asname or alias.name)
fixture_uses: dict[str, list[ast.Name | ast.Constant]] = {
name: [] for name in fixture_bound
}
fixture_blocked: set[str] = set()
# String annotations that mention a bound name without being exactly it,
# like "FrozenDateTimeFactory | None", which cannot be rewritten.
fixture_string_blockers: list[tuple[str, ast.Constant]] = []
if fixture_bound:
fixture_blocked = fixture_rebindings(tree, fixture_bound)
for node in ast.walk(tree):
if isinstance(node, ast.Name) and node.id in fixture_bound:
if isinstance(node.ctx, ast.Load):
fixture_uses[node.id].append(node)
else:
fixture_blocked.add(node.id)
elif isinstance(node, ast.JoinedStr):
# On Python < 3.12, names within f-strings have no tokens to
# rewrite.
fixture_blocked.update(
subnode.id
for subnode in ast.walk(node)
if isinstance(subnode, ast.Name) and subnode.id in fixture_bound
)
for constant in annotation_strings(tree):
string_value = constant.value
assert isinstance(string_value, str)
if string_value in fixture_bound:
fixture_uses[string_value].append(constant)
else:
for name in fixture_bound:
if re.search(rf"\b{re.escape(name)}\b", string_value):
fixture_blocked.add(name)
fixture_string_blockers.append((name, constant))
fixture_migratable: set[str] = set()
fixture_used: set[str] = set()
for name in fixture_bound:
if name in fixture_blocked:
continue
if fixture_uses[name]:
# Rewriting uses requires an import of the replacement, so needs a
# module-level carrier import (checked below).
fixture_used.add(name)
else:
# Imported but unused: drop from the import without rewrites.
fixture_migratable.add(name)
import_carrier: ast.ImportFrom | None = None
if fixture_used:
# The added `from time_machine import TimeMachineFixture` has to go at
# module level, replacing part of a rewritten module-level freezegun
# import.
for import_node in freezegun_from_imports:
if import_node in module_stmts and any(
alias.name == "freeze_time"
or (alias.asname or alias.name) in fixture_migratable
or (alias.asname or alias.name) in fixture_used
for alias in import_node.names
):
import_carrier = import_node
break
if import_carrier is not None:
for name in fixture_used:
fixture_migratable.add(name)
for use in fixture_uses[name]:
if isinstance(use, ast.Name):
ret[ast_start_offset(use)].append(
partial(replace_name, src="TimeMachineFixture")
)
else:
ret[ast_start_offset(use)].append(
partial(
replace_string_constant,
node=use,
value="TimeMachineFixture",
)
)
removed_imports: list[ast.ImportFrom] = []
for import_node in freezegun_from_imports:
has_freeze_time = any(
alias.name == "freeze_time" for alias in import_node.names
)
if not has_freeze_time and not any(
(alias.asname or alias.name) in fixture_migratable
for alias in import_node.names
):
# Nothing migrated from this import: leave it unchanged.
continue
keep = [
unparse_alias(alias)
for alias in import_node.names
if alias.name != "freeze_time"
and (alias.asname or alias.name) not in fixture_migratable
]
new_stmts = []
if has_freeze_time:
new_stmts.append("import time_machine")
if import_node is import_carrier:
new_stmts.append("from time_machine import TimeMachineFixture")
if keep:
new_stmts.append(f"from {import_node.module} import {', '.join(keep)}")
if new_stmts:
ret[ast_start_offset(import_node)].append(
partial(replace_import_from, node=import_node, new_stmts=new_stmts)
)
else:
removed_imports.append(import_node)
for import_node in removed_imports:
block = containing_block(tree, import_node)
remaining = [stmt for stmt in block if stmt not in removed_imports]
if remaining or block[-1] is not import_node:
ret[ast_start_offset(import_node)].append(
partial(remove_statement, node=import_node)
)
else:
# Removing every statement in the block would leave invalid
# syntax, so replace the last with `pass`.
ret[ast_start_offset(import_node)].append(
partial(replace_import_from, node=import_node, new_stmts=["pass"])
)
if freeze_time_names or freezegun_module_names:
for assignment, use_scope in find_candidate_assignments(tree):
value = assignment.value
assert isinstance(value, ast.Call)
if not is_freeze_time_call(
value,
freezegun_module_names=freezegun_module_names,
freeze_time_names=freeze_time_names,
):
continue
target = assignment.targets[0]
if isinstance(target, ast.Name):
compatible = name_target_uses_compatible(use_scope, target)
else:
assert isinstance(target, ast.Attribute)
compatible = self_attr_target_uses_compatible(use_scope, target)
if compatible:
maybe_migrate_call(
ret,
value,
freezegun_module_names=freezegun_module_names,
freeze_time_names=freeze_time_names,
freezer_functions=freezer_functions,
)
unmigrated_fixture_names = fixture_bound - fixture_migratable
# Names within migrated attribute accesses, which may start after them
# when parenthesized, like `(freezegun).freeze_time`.
migrated_attribute_names = {
node.value
for node in ast.walk(tree)
if isinstance(node, ast.Attribute)
and isinstance(node.value, ast.Name)
and ast_start_offset(node) in ret
}
reports = []
for node in ast.walk(tree):
match node:
case ast.Name(id=name) if (
name in freeze_time_names
or name in freezegun_module_names
or name in report_module_names
or name in unmigrated_fixture_names
) and (
ast_start_offset(node) not in ret
and node not in migrated_attribute_names
):
reports.append(
Report(
node.lineno,
node.col_offset + 1,
f"{name} usage not migrated",
)
)
case ast.Attribute(
attr="freeze_time",
value=ast.Attribute(
attr="mark",
value=ast.Name(id="pytest"),
),
) if ast_start_offset(node) not in ret:
reports.append(
Report(
node.lineno,
node.col_offset + 1,
"pytest.mark.freeze_time usage not migrated",
)
)
for name in unmigrated_fixture_names:
unmigrated_strings = [
*(use for use in fixture_uses[name] if isinstance(use, ast.Constant)),
*(
constant
for blocker_name, constant in fixture_string_blockers
if blocker_name == name
),
]
for constant in unmigrated_strings:
reports.append(
Report(
constant.lineno,
constant.col_offset + 1,
f"{name} usage not migrated",
)
)
reports.sort()
return ret, reports
def all_arguments(node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[ast.arg]:
"""
Return all the arguments of the given function definition.
"""
arguments = [
*node.args.posonlyargs,
*node.args.args,
*node.args.kwonlyargs,
]
if node.args.vararg is not None:
arguments.append(node.args.vararg)
if node.args.kwarg is not None:
arguments.append(node.args.kwarg)
return arguments
def annotation_strings(tree: ast.Module) -> Generator[ast.Constant, None, None]:
"""
Yield the string constants within annotations, which may name types that
are not defined until runtime, like ``"FrozenDateTimeFactory"`` or
``Optional["FrozenDateTimeFactory"]``.
"""
for node in ast.walk(tree):
annotations: list[ast.expr | None]
match node:
case ast.FunctionDef() | ast.AsyncFunctionDef():
annotations = [node.returns]
case ast.arg():
annotations = [node.annotation]
case ast.AnnAssign():
annotations = [node.annotation]
case _:
continue
for annotation in annotations:
if annotation is None:
continue
for subnode in ast.walk(annotation):
if isinstance(subnode, ast.Constant) and isinstance(subnode.value, str):
yield subnode
def find_freezer_function(
freezer_functions: list[FreezerFunction], node: ast.expr
) -> FreezerFunction | None:
"""
Find the innermost function with a freezer fixture argument containing the
given node, if any.
"""
for function in reversed(freezer_functions):
if function.lineno <= node.lineno <= function.end_lineno:
return function
return None
def module_scope_stmts(tree: ast.Module) -> set[ast.stmt]:
"""
Collect the statements that execute at module scope, including within
module-level compound statements like `if` and `try`, but not within
functions or classes.
"""
result: set[ast.stmt] = set()
def add(body: list[ast.stmt]) -> None:
for stmt in body:
result.add(stmt)
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
continue
for field in ("body", "orelse", "finalbody"):
sub_body = getattr(stmt, field, None)
if isinstance(sub_body, list):
add(sub_body)
if isinstance(stmt, ast.Try):
for handler in stmt.handlers:
add(handler.body)
add(tree.body)
return result
def fixture_rebindings(tree: ast.Module, names: set[str]) -> set[str]:
"""
Find which of the given FrozenDateTimeFactory names are also bound by
something other than their freezegun imports, such as function parameters,
def or class statements, or imports from other modules. Rewriting uses of
such names would be unsafe. (Heuristic: some rarer rebindings, like
``except ... as`` names, are not detected.)
"""
rebound = set()
for node in ast.walk(tree):
match node:
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.Lambda():
if not isinstance(node, ast.Lambda) and node.name in names:
rebound.add(node.name)
rebound.update(
arg.arg
for arg in (
*node.args.posonlyargs,
*node.args.args,
*node.args.kwonlyargs,
)
if arg.arg in names
)
case ast.ClassDef(name=name) if name in names:
rebound.add(name)
case ast.Import() | ast.ImportFrom():
from_freezegun = isinstance(node, ast.ImportFrom) and node.module in (
"freezegun",
"freezegun.api",
)
for alias in node.names:
bound = alias.asname or alias.name.split(".")[0]
if bound in names and not (
from_freezegun and alias.name == FIXTURE_FACTORY
):
rebound.add(bound)
return rebound
def is_freeze_time_call(
node: ast.Call,
*,
freezegun_module_names: set[str],
freeze_time_names: set[str],
) -> bool:
"""
Check if the given call is of freezegun’s freeze_time(), per the names
bound by the module’s imports.
"""
func = node.func
return (
isinstance(func, ast.Attribute)
and func.attr == "freeze_time"
and isinstance(func.value, ast.Name)
and func.value.id in freezegun_module_names
) or (isinstance(func, ast.Name) and func.id in freeze_time_names)
def find_candidate_assignments(
tree: ast.Module,
) -> Generator[tuple[ast.Assign, ast.AST], None, None]:
"""
Yield assignments of call results that may be freeze_time() calls bound
for "raw use" with start() and stop(), paired with the node to search for
uses of the bound name: the enclosing function or module for plain names,
or the enclosing class for `self.` attributes.
"""
def recurse(
node: ast.AST, scope: ast.AST | None, class_node: ast.ClassDef | None
) -> Generator[tuple[ast.Assign, ast.AST], None, None]:
for child in ast.iter_child_nodes(node):
match child:
case ast.Assign(
targets=[ast.Name()],
value=ast.Call(),
) if scope is not None:
yield (child, scope)
case ast.Assign(
targets=[ast.Attribute(value=ast.Name(id="self"))],
value=ast.Call(),
) if class_node is not None:
yield (child, class_node)
match child:
case ast.FunctionDef() | ast.AsyncFunctionDef():
yield from recurse(child, child, class_node)
case ast.ClassDef():
# Plain-name assignments directly in a class body are not
# tracked, since their uses cannot be checked simply.
yield from recurse(child, None, child)
case _:
yield from recurse(child, scope, class_node)
yield from recurse(tree, tree, None)
def name_target_uses_compatible(scope: ast.AST, target: ast.Name) -> bool:
"""
Check that a plain variable assigned a freeze_time() call is only used in
ways that work the same on time-machine's travel(): start() and stop()
calls as statements, and bare start / stop references passed as call
arguments, like `atexit.register(freezer.stop)`.
"""
name = target.id
allowed: set[ast.AST] = {target}
for subnode in ast.walk(scope):
match subnode:
case ast.Expr(
value=ast.Call(
func=ast.Attribute(
value=ast.Name(id=receiver) as receiver_node,
attr="start" | "stop",
),
args=[],
keywords=[],
)
) if receiver == name:
allowed.add(receiver_node)
case ast.Call(args=args):
for arg in args:
match arg:
case ast.Attribute(
value=ast.Name(id=receiver) as receiver_node,
attr="start" | "stop",
) if receiver == name:
allowed.add(receiver_node)
return all(
subnode in allowed
for subnode in ast.walk(scope)
if isinstance(subnode, ast.Name) and subnode.id == name
)
def self_attr_target_uses_compatible(scope: ast.AST, target: ast.Attribute) -> bool:
"""
As for name_target_uses_compatible(), but for a `self.` attribute
assigned a freeze_time() call, checked across the enclosing class, so
unittest setUp() / tearDown() / addCleanup() patterns are covered.
"""
name = target.attr
allowed: set[ast.AST] = {target}
for subnode in ast.walk(scope):
match subnode:
case ast.Expr(
value=ast.Call(
func=ast.Attribute(
value=ast.Attribute(
value=ast.Name(id="self"),
attr=receiver,
) as receiver_node,
attr="start" | "stop",
),
args=[],
keywords=[],
)
) if receiver == name:
allowed.add(receiver_node)
case ast.Call(args=args):
for arg in args:
match arg:
case ast.Attribute(
value=ast.Attribute(
value=ast.Name(id="self"),
attr=receiver,
) as receiver_node,
attr="start" | "stop",
) if receiver == name:
allowed.add(receiver_node)
return all(
subnode in allowed
for subnode in ast.walk(scope)
if isinstance(subnode, ast.Attribute)
and isinstance(subnode.value, ast.Name)
and subnode.value.id == "self"
and subnode.attr == name
)
def maybe_migrate_call(
ret: MutableMapping[Offset, list[TokenFunc]],
node: ast.expr,
*,
freezegun_module_names: set[str],
freeze_time_names: set[str],
freezer_functions: list[FreezerFunction],
) -> bool:
"""
Add the callbacks to rewrite the given expression, if it is a migratable
call to freezegun’s freeze_time(), returning whether that was the case.
"""
if (
not isinstance(node, ast.Call)
or not is_freeze_time_call(
node,
freezegun_module_names=freezegun_module_names,
freeze_time_names=freeze_time_names,
)
or not migratable_call(node)
):
return False
if find_freezer_function(freezer_functions, node) is not None:
# Inside a function whose freezer argument is renamed to
# time_machine, shadowing the module, so a rewritten call would not
# resolve to it.
return False
func = node.func
assert isinstance(func, (ast.Attribute, ast.Name))
ret[ast_start_offset(func)].append(partial(switch_to_travel, node=func))
migrate_arguments(ret, node)
return True
def traveller_var_uses_compatible(node: ast.With, binding: ast.Name) -> bool:
"""
Check that a variable bound with ``as`` to a freeze_time() context manager
is only used in ways that work the same on time-machine’s Traveller:
move_to() calls, and tick() calls as statements, since shift() does not
return the new time.
"""
name = binding.id
allowed = {binding}
for subnode in ast.walk(node):
match subnode:
case ast.Expr(
value=ast.Call(
func=ast.Attribute(
value=ast.Name(id=receiver) as receiver_node,
attr="tick",
)
)
) if receiver == name:
allowed.add(receiver_node)
case ast.Call(
func=ast.Attribute(
value=ast.Name(id=receiver) as receiver_node,
attr="move_to",
)
) if receiver == name:
allowed.add(receiver_node)
return all(
subnode in allowed
for subnode in ast.walk(node)
if isinstance(subnode, ast.Name) and subnode.id == name
)
def maybe_migrate_pytestmark(
ret: MutableMapping[Offset, list[TokenFunc]],
node: ast.stmt,
) -> bool:
"""
Add the callbacks to rewrite any migratable pytest.mark.freeze_time()
markers in the given statement, if it is a ``pytestmark`` assignment,
returning whether any were migrated.
"""
elements: list[ast.expr]
match node:
case ast.Assign(
targets=[ast.Name(id="pytestmark")],
value=ast.Call() as single,
):
elements = [single]
case ast.Assign(
targets=[ast.Name(id="pytestmark")],
value=ast.List(elts=elements) | ast.Tuple(elts=elements),
):
pass
case _:
return False
migrated = False
for element in elements:
if maybe_migrate_marker(ret, element):
migrated = True
return migrated
def maybe_migrate_marker(
ret: MutableMapping[Offset, list[TokenFunc]],
node: ast.expr,
) -> bool:
"""
Add the callbacks to rewrite the given decorator, if it is a migratable
use of pytest-freezegun / pytest-freezer’s pytest.mark.freeze_time()
marker, returning whether that was the case.
"""
if not (
isinstance(node, ast.Call)
and migratable_call(node)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "freeze_time"
and isinstance(node.func.value, ast.Attribute)
and node.func.value.attr == "mark"
and isinstance(node.func.value.value, ast.Name)
and node.func.value.value.id == "pytest"
):
return False
ret[ast_start_offset(node.func)].append(partial(switch_to_marker, node=node.func))
migrate_arguments(ret, node)
return True
def migrate_arguments(
ret: MutableMapping[Offset, list[TokenFunc]],
node: ast.Call,
) -> None:
"""
Add the callbacks to adjust the arguments of a migratable freeze_time()
call: add a None destination if there is no positional argument, add
tick=False if tick is not passed, and remove droppable keyword arguments.
"""
tick_kwargs = [kw for kw in node.keywords if kw.arg == "tick"]
if not node.args:
if tick_kwargs:
ret[ast_start_offset(tick_kwargs[0])].append(insert_none_arg)
else:
ret[ast_start_offset(node)].append(partial(add_none_arg, node=node))
if not tick_kwargs:
ret[ast_start_offset(node)].append(partial(add_tick_false, node=node))
for kw in node.keywords:
if droppable_kwarg(kw):
ret[ast_start_offset(kw)].append(partial(remove_kwarg, node=kw))
def migratable_call(node: ast.Call) -> bool:
return len(node.args) <= 1 and all(
kw.arg == "tick" or droppable_kwarg(kw) for kw in node.keywords
)
def droppable_kwarg(kw: ast.keyword) -> bool:
"""
Check if the given freeze_time() keyword argument can be dropped when
migrating, because its value makes it have no effect or requests
behaviour that time-machine always provides.
"""
match kw.arg:
case "tz_offset":
# A zero offset has no effect.
return (
isinstance(kw.value, ast.Constant)
and type(kw.value.value) in (int, float)
and kw.value.value == 0
)
case "real_asyncio":
# time-machine does not mock time.monotonic(), so asyncio event
# loops always see real time, whatever the value.
return True
case "ignore":
# time-machine’s C-level mocking does not need workarounds, so
# ignores are hopefully unnecessary.
return True
case _:
return False
def migratable_tick_call(node: ast.Call) -> bool:
"""
freezegun’s tick() takes a single optional argument, delta.
"""
if node.args:
return len(node.args) == 1 and not node.keywords
return not node.keywords or (
len(node.keywords) == 1 and node.keywords[0].arg == "delta"
)
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.arg | 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_aliased_import(tokens: list[Token], i: int, node: ast.Import) -> None:
"""
Replace an ``import freezegun as `` statement with
``import time_machine``, dropping the alias since calls of
``.freeze_time()`` are rewritten to ``time_machine.travel()``.
"""
j = find_last_token(tokens, i, node=node)
tokens[i : j + 1] = [Token(name=CODE, src="import time_machine")]
def unparse_alias(alias: ast.alias) -> str:
if alias.asname is not None:
return f"{alias.name} as {alias.asname}"
return alias.name
def replace_import_from(
tokens: list[Token], i: int, node: ast.ImportFrom, new_stmts: list[str]
) -> None:
"""
Replace a from-import of freezegun with the given statements, indented to
match.
"""
j = find_last_token(tokens, i, node=node)
k = line_start_index(tokens, i)
if k is None:
# Something else shares the line, like `if ...:` or a statement
# separated with `;`, so new lines would move the following
# statements out of the block.
src = "; ".join(new_stmts)
else:
indent = "".join(token.src for token in tokens[k:i])
src = f"\n{indent}".join(new_stmts)
tokens[i : j + 1] = [Token(name=CODE, src=src)]
def remove_statement(tokens: list[Token], i: int, node: ast.stmt) -> None:
"""
Remove the given statement, including its line when nothing else shares
it, otherwise replacing it with `pass`.
"""
j = find_last_token(tokens, i, node=node)
j2 = j
while tokens[j2 + 1].name in (UNIMPORTANT_WS, "COMMENT"):
j2 += 1
k = line_start_index(tokens, i)
if k is not None and tokens[j2 + 1].name == "NEWLINE":
del tokens[k : j2 + 2]
else:
# Something else shares the line, like statements separated with `;`.
tokens[i : j + 1] = [Token(name=CODE, src="pass")]
def line_start_index(tokens: list[Token], i: int) -> int | None:
"""
Return the index of the first token on the line that the statement
starting at the given token index begins, including any indentation, or
None if the statement does not start its line, like after `if ...:` or
`;`.
"""
k = i
while k > 0 and tokens[k - 1].name in (INDENT, UNIMPORTANT_WS):
k -= 1
if k == 0 or tokens[k - 1].name in ("ENCODING", "NEWLINE", "NL", DEDENT):
return k
return None
def containing_block(tree: ast.Module, stmt: ast.stmt) -> list[ast.stmt]:
"""
Find the list of statements containing the given one.
"""
for parent in ast.walk(tree):
for field in ("body", "orelse", "finalbody"):
block = getattr(parent, field, None)
if isinstance(block, list) and stmt in block:
return block
raise AssertionError(f"Statement not found in tree: {stmt!r}") # pragma: no cover
def replace_name(tokens: list[Token], i: int, *, src: str) -> None:
tokens[i] = Token(name=CODE, src=src)
def replace_string_constant(
tokens: list[Token], i: int, *, node: ast.Constant, value: str
) -> None:
"""
Replace the given string constant with one of the given value, keeping the
quote style of its first token.
"""
j = find_last_token(tokens, i, node=node)
src: str = tokens[i].src
# Skip any prefix, like the `r` in r"...".
quote_start = next(index for index, char in enumerate(src) if char in "\"'")
quote = src[quote_start]
if src.startswith(quote * 3, quote_start):
quote *= 3
tokens[i : j + 1] = [Token(name="STRING", src=f"{quote}{value}{quote}")]
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 switch_to_marker(tokens: list[Token], i: int, node: ast.Attribute) -> None:
j = find_last_token(tokens, i, node=node)
tokens[i : j + 1] = [Token(name=CODE, src="pytest.mark.time_machine")]
def replace_freezer(tokens: list[Token], i: int) -> None:
tokens[i] = Token(name="NAME", src="time_machine")
def replace_tick_with_shift(tokens: list[Token], i: int, node: ast.Call) -> None:
"""
Replace a tick() method call with shift(), making freezegun’s default
delta of one second explicit if no argument was passed.
"""
i += 1 # skip the receiver name
while not (tokens[i].name == "NAME" and tokens[i].src == "tick"):
i += 1
tokens[i] = Token(name="NAME", src="shift")
if not node.args and not node.keywords:
while tokens[i].src != "(":
i += 1
tokens.insert(i + 1, Token(name=CODE, src="1"))
def remove_kwarg(tokens: list[Token], i: int, node: ast.keyword) -> None:
"""
Remove the given keyword argument, along with the comma that precedes it,
or, for a first argument, any comma and space that follow it.
"""
j = find_last_token(tokens, i, node=node)
k = i - 1
while tokens[k].name in NON_CODING_TOKENS:
k -= 1
if tokens[k].src == ",":
del tokens[k : j + 1]
return
k = j + 1
while tokens[k].name in NON_CODING_TOKENS:
k += 1
if tokens[k].src == ",":
j = k
if tokens[j + 1].name == UNIMPORTANT_WS:
j += 1
del tokens[i : j + 1]
def insert_none_arg(tokens: list[Token], i: int) -> None:
"""
Insert a `None` argument before the token at the given index, the start of
the call’s tick keyword argument.
"""
tokens.insert(i, Token(name=CODE, src="None, "))
def add_none_arg(tokens: list[Token], i: int, node: ast.Call) -> None:
"""
Add a `None` argument to the function call, which has no remaining
arguments, since any droppable keyword arguments were already removed.
"""
j = find_last_token(tokens, i, node=node)
k = j - 1
while tokens[k].name in NON_CODING_TOKENS:
k -= 1
assert tokens[k].src == "("
tokens.insert(k + 1, Token(name=CODE, src="None"))
def add_tick_false(tokens: list[Token], i: int, node: ast.Call) -> None:
"""
Add `tick=False` to the function call, unless `tick` is already set.
"""
j = find_last_token(tokens, i, node=node)
k = j - 1
while tokens[k].name in NON_CODING_TOKENS:
k -= 1
if tokens[k].src == ",":
# trailing comma: insert after it
tokens.insert(k + 1, Token(name=CODE, src=" tick=False"))
else:
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-18d4852/src/time_machine/py.typed 0000664 0000000 0000000 00000000000 15250103717 0023327 0 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/tests/ 0000775 0000000 0000000 00000000000 15250103717 0017573 5 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/tests/__init__.py 0000664 0000000 0000000 00000000000 15250103717 0021672 0 ustar 00root root 0000000 0000000 adamchainz-time-machine-18d4852/tests/conftest.py 0000664 0000000 0000000 00000000750 15250103717 0021774 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()
def _have_64_bit_time_t() -> bool:
# Probe with a post-2038 timestamp: platforms with 32-bit time_t raise
# OverflowError (or OSError) when converting it.
try:
time.gmtime(2**31)
except (OverflowError, OSError): # pragma: no cover
return False
return True
HAVE_64_BIT_TIME_T = _have_64_bit_time_t()
adamchainz-time-machine-18d4852/tests/test_cli.py 0000664 0000000 0000000 00000265217 15250103717 0021770 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 == f"{path}:1:5: could not parse file: invalid syntax\n"
assert path.read_text() == "def def def\n"
def test_migrate_null_byte(self, capsys, tmp_path):
path = tmp_path / "example.py"
path.write_bytes(b"x = 1\x00\n")
result = main(["migrate", str(path)])
assert result == 0
out, err = capsys.readouterr()
assert out == ""
assert err == (
f"{path}:1:1: could not parse file: "
+ "source code string cannot contain null bytes\n"
)
assert path.read_bytes() == b"x = 1\x00\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 test_migrate_reports(self, capsys, tmp_path):
path = tmp_path / "example.py"
path.write_text(
"from freezegun import freeze_time\n"
"@freeze_time\n"
"def test_function():\n"
" pass\n"
)
result = main(["migrate", str(path)])
assert result == 1
out, err = capsys.readouterr()
assert out == ""
assert err == (
f"Rewriting {path}\n" + f"{path}:2:2: freeze_time usage not migrated\n"
)
assert path.read_text() == (
"import time_machine\n@freeze_time\ndef test_function():\n pass\n"
)
def test_migrate_reports_positioned_in_rewritten_file(self, capsys, tmp_path):
path = tmp_path / "example.py"
path.write_text(
"from freezegun import freeze_time, FakeDate\n"
"@freeze_time\n"
"def test_function():\n"
" pass\n"
)
result = main(["migrate", str(path)])
assert result == 1
out, err = capsys.readouterr()
assert out == ""
# The rewritten import spans two lines, moving the usage down one.
assert err == (
f"Rewriting {path}\n" + f"{path}:3:2: freeze_time usage not migrated\n"
)
assert path.read_text() == (
"import time_machine\n"
"from freezegun import FakeDate\n"
"@freeze_time\n"
"def test_function():\n"
" pass\n"
)
def check_noop(
given: str,
reports: list[tuple[int, int, str]] | None = None,
) -> None:
given = dedent(given)
result, result_reports = migrate_contents(given)
assert result == given
assert result_reports == (reports or [])
def check_transformed(
given: str,
expected: str,
reports: list[tuple[int, int, str]] | None = None,
) -> None:
given = dedent(given)
expected = dedent(expected)
result, result_reports = migrate_contents(given)
assert result == expected
assert result_reports == (reports or [])
class TestMigrateContents:
def test_import_unrelated(self):
check_noop(
"import libfaketime",
)
def test_aliased(self):
check_transformed(
"import freezegun as fg",
"import time_machine",
)
def test_aliased_used(self):
check_transformed(
"""
import freezegun as fg
@fg.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_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_relative(self):
check_noop(
"from .freezegun import freeze_time",
)
def test_import_from_freezegun_aliased(self):
check_transformed(
"from freezegun import freeze_time as ft",
"import time_machine",
)
def test_import_from_freezegun_aliased_used(self):
check_transformed(
"""
from freezegun import freeze_time as ft
@ft("2023-01-01")
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_import_from_freezegun_aliased_used_with(self):
check_transformed(
"""
from freezegun import freeze_time as ft
with ft("2023-01-01") as t:
t.tick()
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False) as t:
t.shift(1)
""",
)
def test_import_from_freezegun_multiple(self):
check_transformed(
"from freezegun import freeze_time, FakeDate",
"import time_machine\nfrom freezegun import FakeDate",
)
def test_import_from_freezegun_multiple_aliased(self):
check_transformed(
"from freezegun import freeze_time, FakeDate as FD",
"import time_machine\nfrom freezegun import FakeDate as FD",
)
def test_import_from_freezegun_multiple_only_aliased_freeze_time(self):
check_transformed(
"from freezegun import freeze_time as ft, FakeDate",
"import time_machine\nfrom freezegun import FakeDate",
)
def test_import_from_freezegun_multiple_parenthesized(self):
check_transformed(
"""\
from freezegun import (
freeze_time,
FakeDate,
)
""",
"""\
import time_machine
from freezegun import FakeDate
""",
)
def test_import_from_freezegun_multiple_indented(self):
check_transformed(
"""\
def f():
from freezegun import freeze_time, FakeDate
""",
"""\
def f():
import time_machine
from freezegun import FakeDate
""",
)
def test_import_from_freezegun_multiple_indented_not_first_statement(self):
check_transformed(
"""\
def f():
x = 1
from freezegun import freeze_time, FakeDate
""",
"""\
def f():
x = 1
import time_machine
from freezegun import FakeDate
""",
)
def test_import_from_freezegun_multiple_compound_statement(self):
# The new statements must stay within the block.
check_transformed(
"if True: from freezegun import freeze_time, FakeDate\n",
"if True: import time_machine; from freezegun import FakeDate\n",
)
def test_import_from_freezegun_multiple_compound_statement_fixture_factory(
self,
):
check_transformed(
"""
if TYPE_CHECKING: from freezegun import freeze_time, FrozenDateTimeFactory
def test_function(freezer: FrozenDateTimeFactory):
pass
""",
"""
if TYPE_CHECKING: import time_machine; from time_machine import TimeMachineFixture
def test_function(time_machine: TimeMachineFixture):
pass
""",
)
def test_import_from_freezegun_multiple_semicolon(self):
check_transformed(
"import os; from freezegun import freeze_time, FakeDate; import sys\n",
"import os; import time_machine; from freezegun import FakeDate; import sys\n",
)
def test_import_from_freezegun_multiple_indented_after_dedent(self):
check_transformed(
"""\
def f():
if True:
pass
from freezegun import freeze_time, FakeDate
""",
"""\
def f():
if True:
pass
import time_machine
from freezegun import FakeDate
""",
)
def test_import_from_freezegun_multiple_used(self):
check_transformed(
"""\
from freezegun import freeze_time, FakeDate
@freeze_time("2023-01-01")
def test_function():
pass
""",
"""\
import time_machine
from freezegun import FakeDate
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_fixture_factory_annotation(self):
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory
def test_function(freezer: FrozenDateTimeFactory):
freezer.move_to("2023-01-01")
""",
"""
from time_machine import TimeMachineFixture
def test_function(time_machine: TimeMachineFixture):
time_machine.move_to("2023-01-01", tick=False)
""",
)
def test_fixture_factory_annotation_aliased(self):
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory as FDF
def test_function(freezer: FDF):
freezer.move_to("2023-01-01")
""",
"""
from time_machine import TimeMachineFixture
def test_function(time_machine: TimeMachineFixture):
time_machine.move_to("2023-01-01", tick=False)
""",
)
def test_fixture_factory_annotation_helper(self):
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory
def advance(freezer: FrozenDateTimeFactory, days: int):
pass
""",
"""
from time_machine import TimeMachineFixture
def advance(time_machine: TimeMachineFixture, days: int):
pass
""",
)
def test_fixture_factory_string_annotations(self):
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory
def test_function(freezer: "FrozenDateTimeFactory") -> 'FrozenDateTimeFactory':
fixture: "FrozenDateTimeFactory" = None
return fixture
""",
"""
from time_machine import TimeMachineFixture
def test_function(time_machine: "TimeMachineFixture") -> 'TimeMachineFixture':
fixture: "TimeMachineFixture" = None
return fixture
""",
)
def test_fixture_factory_string_annotation_aliased(self):
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory as FDF
def test_function(freezer: "FDF"):
pass
""",
"""
from time_machine import TimeMachineFixture
def test_function(time_machine: "TimeMachineFixture"):
pass
""",
)
def test_fixture_factory_string_annotation_nested(self):
check_transformed(
"""
from typing import Optional
from freezegun.api import FrozenDateTimeFactory
def test_function(freezer: Optional["FrozenDateTimeFactory"] = None):
pass
""",
"""
from typing import Optional
from time_machine import TimeMachineFixture
def test_function(time_machine: Optional["TimeMachineFixture"] = None):
pass
""",
)
def test_fixture_factory_string_annotation_quote_styles(self):
check_transformed(
'''
from freezegun.api import FrozenDateTimeFactory
a: r"FrozenDateTimeFactory" = None
b: """FrozenDateTimeFactory""" = None
c: "Frozen" "DateTimeFactory" = None
''',
'''
from time_machine import TimeMachineFixture
a: "TimeMachineFixture" = None
b: """TimeMachineFixture""" = None
c: "TimeMachineFixture" = None
''',
)
def test_fixture_factory_string_annotation_containing_kept(self):
# The import cannot be removed whilst the annotation refers to it.
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory
def test_function(freezer: "FrozenDateTimeFactory | None"):
fixture: "FrozenDateTimeFactory" = None
""",
"""
from freezegun.api import FrozenDateTimeFactory
def test_function(time_machine: "FrozenDateTimeFactory | None"):
fixture: "FrozenDateTimeFactory" = None
""",
reports=[
(4, 33, "FrozenDateTimeFactory usage not migrated"),
(5, 14, "FrozenDateTimeFactory usage not migrated"),
],
)
def test_fixture_factory_string_annotation_non_annotation_string_ignored(self):
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory
def test_function(freezer: FrozenDateTimeFactory):
'''Uses FrozenDateTimeFactory.'''
name: "str" = "FrozenDateTimeFactory"
""",
"""
from time_machine import TimeMachineFixture
def test_function(time_machine: TimeMachineFixture):
'''Uses FrozenDateTimeFactory.'''
name: "str" = "FrozenDateTimeFactory"
""",
)
def test_fixture_factory_string_annotation_function_local_kept(self):
check_noop(
"""
def test_function():
from freezegun.api import FrozenDateTimeFactory
fixture: "FrozenDateTimeFactory" = None
""",
reports=[(4, 14, "FrozenDateTimeFactory usage not migrated")],
)
def test_fixture_factory_with_freeze_time_import(self):
check_transformed(
"""
from freezegun import freeze_time
from freezegun.api import FrozenDateTimeFactory
def test_function(freezer: FrozenDateTimeFactory):
freezer.move_to("2023-01-01")
@freeze_time("2023-06-01")
def test_function2():
pass
""",
"""
import time_machine
from time_machine import TimeMachineFixture
def test_function(time_machine: TimeMachineFixture):
time_machine.move_to("2023-01-01", tick=False)
@time_machine.travel("2023-06-01", tick=False)
def test_function2():
pass
""",
)
def test_fixture_factory_unused(self):
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory
x = 1
""",
"""
x = 1
""",
)
def test_fixture_factory_function_local_kept(self):
check_noop(
"""
def test_function():
from freezegun.api import FrozenDateTimeFactory
return FrozenDateTimeFactory
""",
reports=[(4, 12, "FrozenDateTimeFactory usage not migrated")],
)
def test_fixture_factory_store_kept(self):
check_transformed(
"""
from freezegun import freeze_time, FrozenDateTimeFactory
FrozenDateTimeFactory = None
@freeze_time("2023-01-01")
def test_function():
pass
""",
"""
import time_machine
from freezegun import FrozenDateTimeFactory
FrozenDateTimeFactory = None
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
# Positioned in the migrated text, after the import expanded.
reports=[(5, 1, "FrozenDateTimeFactory usage not migrated")],
)
def test_fixture_factory_fstring_kept(self):
check_noop(
"""
from freezegun import FrozenDateTimeFactory
x = f"{FrozenDateTimeFactory} here"
""",
reports=[(4, 8, "FrozenDateTimeFactory usage not migrated")],
)
def test_fixture_factory_shadow_parameter_kept(self):
check_noop(
"""
from freezegun import FrozenDateTimeFactory
def check(FrozenDateTimeFactory):
return FrozenDateTimeFactory
""",
reports=[(5, 12, "FrozenDateTimeFactory usage not migrated")],
)
def test_fixture_factory_shadow_def_kept(self):
check_noop(
"""
from freezegun import FrozenDateTimeFactory
def FrozenDateTimeFactory():
pass
x = FrozenDateTimeFactory()
""",
reports=[(7, 5, "FrozenDateTimeFactory usage not migrated")],
)
def test_fixture_factory_shadow_class_kept(self):
check_noop(
"""
from freezegun import FrozenDateTimeFactory
class FrozenDateTimeFactory:
pass
""",
)
def test_fixture_factory_shadow_import_kept(self):
check_noop(
"""
from freezegun import FrozenDateTimeFactory
from othermod import FrozenDateTimeFactory
x: FrozenDateTimeFactory = None
""",
reports=[(5, 4, "FrozenDateTimeFactory usage not migrated")],
)
def test_fixture_factory_shadow_lambda_kept(self):
check_noop(
"""
from freezegun import FrozenDateTimeFactory
f = lambda FrozenDateTimeFactory: FrozenDateTimeFactory
""",
reports=[(4, 35, "FrozenDateTimeFactory usage not migrated")],
)
def test_fixture_factory_function_local_import_module_carrier(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01")
def test_function():
from freezegun.api import FrozenDateTimeFactory
x: FrozenDateTimeFactory = None
""",
"""
import time_machine
from time_machine import TimeMachineFixture
@time_machine.travel("2023-01-01", tick=False)
def test_function():
x: TimeMachineFixture = None
""",
)
def test_fixture_factory_import_in_try(self):
check_transformed(
"""
try:
import zoneinfo
except ImportError:
pass
from freezegun import FrozenDateTimeFactory
x: FrozenDateTimeFactory = None
""",
"""
try:
import zoneinfo
except ImportError:
pass
from time_machine import TimeMachineFixture
x: TimeMachineFixture = None
""",
)
def test_fixture_factory_unused_removed_trailing_comment(self):
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory # noqa
x = 1
""",
"""
x = 1
""",
)
def test_fixture_factory_unused_semicolon_before(self):
check_transformed(
"""
x = 1; from freezegun import FrozenDateTimeFactory
y = 2
""",
"""
x = 1; pass
y = 2
""",
)
def test_fixture_factory_unused_only_statement_in_block(self):
check_transformed(
"""
if True: from freezegun.api import FrozenDateTimeFactory
x = 1
""",
"""
if True: pass
x = 1
""",
)
def test_fixture_factory_unused_all_statements_in_block(self):
check_transformed(
"""
def test_function():
from freezegun.api import FrozenDateTimeFactory
from freezegun.api import FrozenDateTimeFactory
""",
"""
def test_function():
pass
""",
)
def test_fixture_factory_unused_all_statements_in_module(self):
check_transformed(
"""
from freezegun.api import FrozenDateTimeFactory
from freezegun import FrozenDateTimeFactory as FDF
""",
"""
pass
""",
)
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
""",
reports=[(4, 2, "freezegun usage not migrated")],
)
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_async_function_decorator_attr(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time("2023-01-01")
async def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
async def test_function():
pass
""",
)
def test_function_decorator_attr_tick(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time("2023-01-01", tick=True)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=True)
def test_function():
pass
""",
)
def test_function_decorator_attr_tick_false(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time("2023-01-01", tick=False)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_function_decorator_datetime_tzinfo(self):
check_transformed(
"""
from datetime import datetime
from zoneinfo import ZoneInfo
from freezegun import freeze_time
@freeze_time(datetime(2024, 1, 1, tzinfo=ZoneInfo("UTC")))
def test_function():
pass
""",
"""
from datetime import datetime
from zoneinfo import ZoneInfo
import time_machine
@time_machine.travel(datetime(2024, 1, 1, tzinfo=ZoneInfo("UTC")), tick=False)
def test_function():
pass
""",
)
def test_function_decorator_date(self):
check_transformed(
"""
from datetime import date
from freezegun import freeze_time
@freeze_time(date(2024, 1, 1))
def test_function():
pass
""",
"""
from datetime import date
import time_machine
@time_machine.travel(date(2024, 1, 1), tick=False)
def test_function():
pass
""",
)
def test_with_date(self):
check_transformed(
"""
from datetime import date
from freezegun import freeze_time
def test_function():
with freeze_time(date(2024, 1, 1)):
pass
""",
"""
from datetime import date
import time_machine
def test_function():
with time_machine.travel(date(2024, 1, 1), tick=False):
pass
""",
)
def test_with_expression(self):
check_transformed(
"""
from datetime import timedelta
from freezegun import freeze_time
def test_function(self):
with freeze_time(self.run_details.approve_by + timedelta(hours=1)):
pass
""",
"""
from datetime import timedelta
import time_machine
def test_function(self):
with time_machine.travel(self.run_details.approve_by + timedelta(hours=1), tick=False):
pass
""",
)
def test_with_datetime_tzinfo(self):
check_transformed(
"""
from datetime import datetime
from zoneinfo import ZoneInfo
from freezegun import freeze_time
def test_function():
with freeze_time(datetime(2024, 1, 1, tzinfo=ZoneInfo("UTC"))):
pass
""",
"""
from datetime import datetime
from zoneinfo import ZoneInfo
import time_machine
def test_function():
with time_machine.travel(datetime(2024, 1, 1, tzinfo=ZoneInfo("UTC")), tick=False):
pass
""",
)
def test_function_decorator_no_arguments(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time()
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel(None, tick=False)
def test_function():
pass
""",
)
def test_function_decorator_no_arguments_attr(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time()
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel(None, tick=False)
def test_function():
pass
""",
)
def test_function_decorator_no_arguments_tick(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time(tick=True)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel(None, tick=True)
def test_function():
pass
""",
)
def test_function_decorator_no_arguments_tz_offset_zero(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time(tz_offset=0)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel(None, tick=False)
def test_function():
pass
""",
)
def test_function_decorator_no_arguments_tz_offset_zero_trailing_comma(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time(tz_offset=0,)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel(None, tick=False)
def test_function():
pass
""",
)
def test_function_decorator_no_arguments_tz_offset_zero_before_tick(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time(tz_offset=0, tick=True)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel(None, tick=True)
def test_function():
pass
""",
)
def test_function_decorator_no_arguments_tz_offset_zero_after_tick(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time(tick=True, tz_offset=0)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel(None, tick=True)
def test_function():
pass
""",
)
def test_function_decorator_no_arguments_tz_offset_zero_spaced(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time( tz_offset=0 , tick=True )
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel( None, tick=True )
def test_function():
pass
""",
)
def test_function_decorator_no_arguments_tz_offset_zero_only_spaced(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time( tz_offset=0 )
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel(None , tick=False)
def test_function():
pass
""",
)
def test_with_no_arguments(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time() as ft:
ft.tick()
""",
"""
import time_machine
with time_machine.travel(None, tick=False) as ft:
ft.shift(1)
""",
)
def test_marker_no_arguments(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time()
def test_function():
pass
""",
"""
import pytest
@pytest.mark.time_machine(None, tick=False)
def test_function():
pass
""",
)
def test_function_decorator_tz_offset_zero(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", tz_offset=0)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_function_decorator_tz_offset_zero_float(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", tz_offset=0.0)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_function_decorator_tz_offset_zero_before_tick(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", tz_offset=0, tick=True)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=True)
def test_function():
pass
""",
)
def test_function_decorator_tz_offset_zero_after_tick(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", tick=True, tz_offset=0)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=True)
def test_function():
pass
""",
)
def test_function_decorator_tz_offset_zero_trailing_comma(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", tz_offset=0,)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_function_decorator_tz_offset_zero_multiline(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time(
"2023-01-01",
tz_offset=0,
)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel(
"2023-01-01", tick=False
)
def test_function():
pass
""",
)
def test_function_decorator_tz_offset_nonzero(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", tz_offset=-4)
def test_function():
pass
""",
"""
import time_machine
@freeze_time("2023-01-01", tz_offset=-4)
def test_function():
pass
""",
reports=[(4, 2, "freeze_time usage not migrated")],
)
def test_function_decorator_tz_offset_false(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", tz_offset=False)
def test_function():
pass
""",
"""
import time_machine
@freeze_time("2023-01-01", tz_offset=False)
def test_function():
pass
""",
reports=[(4, 2, "freeze_time usage not migrated")],
)
def test_function_decorator_tz_offset_variable(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", tz_offset=offset)
def test_function():
pass
""",
"""
import time_machine
@freeze_time("2023-01-01", tz_offset=offset)
def test_function():
pass
""",
reports=[(4, 2, "freeze_time usage not migrated")],
)
def test_function_decorator_real_asyncio_true(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", real_asyncio=True)
async def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
async def test_function():
pass
""",
)
def test_function_decorator_real_asyncio_false(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", real_asyncio=False)
async def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
async def test_function():
pass
""",
)
def test_function_decorator_real_asyncio_variable(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", real_asyncio=real_asyncio)
async def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
async def test_function():
pass
""",
)
def test_function_decorator_ignore(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", ignore=["threading"])
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_function_decorator_ignore_variable(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", ignore=IGNORED_MODULES, tick=True)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=True)
def test_function():
pass
""",
)
def test_marker_ignore(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time("2023-01-01", ignore=["threading"])
def test_function():
pass
""",
"""
import pytest
@pytest.mark.time_machine("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_with_tz_offset_zero(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01", tz_offset=0):
pass
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False):
pass
""",
)
def test_marker_tz_offset_zero(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time("2023-01-01", tz_offset=0)
def test_function():
pass
""",
"""
import pytest
@pytest.mark.time_machine("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
""",
reports=[(4, 2, "freeze_time usage not migrated")],
)
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_function_decorator_name_trailing_comma(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_function_decorator_name_multiline(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_function_decorator_name_tick(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01", tick=True)
def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=True)
def test_function():
pass
""",
)
def test_async_function_decorator_name(self):
check_transformed(
"""
from freezegun import freeze_time
@freeze_time("2023-01-01")
async def test_function():
pass
""",
"""
import time_machine
@time_machine.travel("2023-01-01", tick=False)
async 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
""",
reports=[(4, 2, "freezegun usage not migrated")],
)
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
""",
reports=[(4, 2, "freezegun usage not migrated")],
)
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_attr_unittest_class_base_name_tick(self):
check_transformed(
"""
import freezegun
from django.test import SimpleTestCase
@freezegun.freeze_time("2023-01-01", tick=True)
class TestClass(SimpleTestCase):
pass
""",
"""
import time_machine
from django.test import SimpleTestCase
@time_machine.travel("2023-01-01", tick=True)
class TestClass(SimpleTestCase):
pass
""",
)
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
""",
reports=[(4, 2, "freeze_time usage not migrated")],
)
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
""",
reports=[(4, 2, "freeze_time usage not migrated")],
)
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
""",
reports=[(4, 6, "freezegun usage not migrated")],
)
def test_with_attr_as(self):
check_transformed(
"""
import freezegun
with freezegun.freeze_time("2023-01-01") as ft:
pass
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False) 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_attr_tick(self):
check_transformed(
"""
import freezegun
with freezegun.freeze_time("2023-01-01", tick=True):
pass
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=True):
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
""",
reports=[(4, 6, "freeze_time usage not migrated")],
)
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 time_machine.travel("2023-01-01", tick=False) 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
""",
)
def test_with_as_move_to(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
ft.move_to("2023-06-01")
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False) as ft:
ft.move_to("2023-06-01")
""",
)
def test_with_as_attribute_target(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as obj.ft:
pass
""",
"""
import time_machine
with freeze_time("2023-01-01") as obj.ft:
pass
""",
reports=[(4, 6, "freeze_time usage not migrated")],
)
def test_with_as_tick(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
ft.tick()
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False) as ft:
ft.shift(1)
""",
)
def test_with_as_tick_arguments(self):
check_transformed(
"""
from datetime import timedelta
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
ft.tick(10.0)
ft.tick(timedelta(seconds=100))
ft.tick(delta=timedelta(seconds=100))
""",
"""
from datetime import timedelta
import time_machine
with time_machine.travel("2023-01-01", tick=False) as ft:
ft.shift(10.0)
ft.shift(timedelta(seconds=100))
ft.shift(delta=timedelta(seconds=100))
""",
)
def test_with_as_tick_too_many_arguments(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
ft.tick(1, 2)
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False) as ft:
ft.tick(1, 2)
""",
)
def test_with_as_tick_other_name(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
other.tick()
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False) as ft:
other.tick()
""",
)
def test_with_as_tick_outside_block(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
pass
ft.tick()
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False) as ft:
pass
ft.tick()
""",
)
def test_with_as_tick_assigned(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
now = ft.tick()
""",
"""
import time_machine
with freeze_time("2023-01-01") as ft:
now = ft.tick()
""",
reports=[(4, 6, "freeze_time usage not migrated")],
)
def test_with_as_incompatible_attribute(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
assert ft.time_to_freeze.year == 2023
""",
"""
import time_machine
with freeze_time("2023-01-01") as ft:
assert ft.time_to_freeze.year == 2023
""",
reports=[(4, 6, "freeze_time usage not migrated")],
)
def test_with_as_incompatible_reference(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as ft:
helper(ft)
""",
"""
import time_machine
with freeze_time("2023-01-01") as ft:
helper(ft)
""",
reports=[(4, 6, "freeze_time usage not migrated")],
)
def test_with_as_unmigratable_call(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01", auto_tick_seconds=1) as ft:
ft.tick()
""",
"""
import time_machine
with freeze_time("2023-01-01", auto_tick_seconds=1) as ft:
ft.tick()
""",
reports=[(4, 6, "freeze_time usage not migrated")],
)
def test_assign_start_stop(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function():
freezer = freeze_time("2023-01-01")
freezer.start()
freezer.stop()
""",
"""
import time_machine
def test_function():
freezer = time_machine.travel("2023-01-01", tick=False)
freezer.start()
freezer.stop()
""",
)
def test_assign_start_stop_module_level(self):
check_transformed(
"""
from freezegun import freeze_time
freezer = freeze_time("2023-01-01")
freezer.start()
freezer.stop()
""",
"""
import time_machine
freezer = time_machine.travel("2023-01-01", tick=False)
freezer.start()
freezer.stop()
""",
)
def test_assign_start_stop_attr_call(self):
check_transformed(
"""
import freezegun
def test_function():
freezer = freezegun.freeze_time("2023-01-01")
freezer.start()
freezer.stop()
""",
"""
import time_machine
def test_function():
freezer = time_machine.travel("2023-01-01", tick=False)
freezer.start()
freezer.stop()
""",
)
def test_assign_start_stop_tick(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function():
freezer = freeze_time("2023-01-01", tick=True)
freezer.start()
freezer.stop()
""",
"""
import time_machine
def test_function():
freezer = time_machine.travel("2023-01-01", tick=True)
freezer.start()
freezer.stop()
""",
)
def test_assign_stop_reference(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function():
freezer = freeze_time("2023-01-01")
freezer.start()
atexit.register(freezer.stop)
""",
"""
import time_machine
def test_function():
freezer = time_machine.travel("2023-01-01", tick=False)
freezer.start()
atexit.register(freezer.stop)
""",
)
def test_assign_start_arguments_incompatible(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function():
freezer = freeze_time("2023-01-01")
freezer.start(1)
""",
"""
import time_machine
def test_function():
freezer = freeze_time("2023-01-01")
freezer.start(1)
""",
reports=[(5, 15, "freeze_time usage not migrated")],
)
def test_assign_start_assigned_incompatible(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function():
freezer = freeze_time("2023-01-01")
x = freezer.start()
""",
"""
import time_machine
def test_function():
freezer = freeze_time("2023-01-01")
x = freezer.start()
""",
reports=[(5, 15, "freeze_time usage not migrated")],
)
def test_assign_move_to_incompatible(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function():
freezer = freeze_time("2023-01-01")
freezer.start()
freezer.move_to("2023-06-01")
freezer.stop()
""",
"""
import time_machine
def test_function():
freezer = freeze_time("2023-01-01")
freezer.start()
freezer.move_to("2023-06-01")
freezer.stop()
""",
reports=[(5, 15, "freeze_time usage not migrated")],
)
def test_assign_reference_incompatible(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function():
freezer = freeze_time("2023-01-01")
helper(freezer)
""",
"""
import time_machine
def test_function():
freezer = freeze_time("2023-01-01")
helper(freezer)
""",
reports=[(5, 15, "freeze_time usage not migrated")],
)
def test_assign_reassigned_incompatible(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function():
freezer = freeze_time("2023-01-01")
freezer.start()
freezer = freeze_time("2024-01-01")
freezer.stop()
""",
"""
import time_machine
def test_function():
freezer = freeze_time("2023-01-01")
freezer.start()
freezer = freeze_time("2024-01-01")
freezer.stop()
""",
reports=[
(5, 15, "freeze_time usage not migrated"),
(7, 15, "freeze_time usage not migrated"),
],
)
def test_assign_unmigratable_call(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function():
freezer = freeze_time("2023-01-01", auto_tick_seconds=1)
freezer.start()
freezer.stop()
""",
"""
import time_machine
def test_function():
freezer = freeze_time("2023-01-01", auto_tick_seconds=1)
freezer.start()
freezer.stop()
""",
reports=[(5, 15, "freeze_time usage not migrated")],
)
def test_assign_other_call_noop(self):
check_noop(
"""
def test_function():
freezer = make_freezer("2023-01-01")
freezer.start()
""",
)
def test_assign_class_body_noop(self):
check_transformed(
"""
from freezegun import freeze_time
class TestSomething:
freezer = freeze_time("2023-01-01")
""",
"""
import time_machine
class TestSomething:
freezer = freeze_time("2023-01-01")
""",
reports=[(5, 15, "freeze_time usage not migrated")],
)
def test_assign_self_attr(self):
check_transformed(
"""
from freezegun import freeze_time
class TestSomething(TestCase):
def setUp(self):
self.freezer = freeze_time("2023-01-01")
self.freezer.start()
self.addCleanup(self.freezer.stop)
""",
"""
import time_machine
class TestSomething(TestCase):
def setUp(self):
self.freezer = time_machine.travel("2023-01-01", tick=False)
self.freezer.start()
self.addCleanup(self.freezer.stop)
""",
)
def test_assign_self_attr_teardown(self):
check_transformed(
"""
from freezegun import freeze_time
class TestSomething(TestCase):
def setUp(self):
self.freezer = freeze_time("2023-01-01")
self.freezer.start()
def tearDown(self):
self.freezer.stop()
""",
"""
import time_machine
class TestSomething(TestCase):
def setUp(self):
self.freezer = time_machine.travel("2023-01-01", tick=False)
self.freezer.start()
def tearDown(self):
self.freezer.stop()
""",
)
def test_assign_self_attr_incompatible(self):
check_transformed(
"""
from freezegun import freeze_time
class TestSomething(TestCase):
def setUp(self):
self.freezer = freeze_time("2023-01-01")
self.freezer.start()
def test_function(self):
self.freezer.tick()
""",
"""
import time_machine
class TestSomething(TestCase):
def setUp(self):
self.freezer = freeze_time("2023-01-01")
self.freezer.start()
def test_function(self):
self.freezer.tick()
""",
reports=[(6, 24, "freeze_time usage not migrated")],
)
def test_assign_self_attr_outside_class(self):
check_transformed(
"""
from freezegun import freeze_time
def helper(self):
self.freezer = freeze_time("2023-01-01")
""",
"""
import time_machine
def helper(self):
self.freezer = freeze_time("2023-01-01")
""",
reports=[(5, 20, "freeze_time usage not migrated")],
)
def test_marker(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time("2023-01-01")
def test_function():
pass
""",
"""
import pytest
@pytest.mark.time_machine("2023-01-01", tick=False)
def test_function():
pass
""",
)
def test_marker_async_function(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time("2023-01-01")
async def test_function():
pass
""",
"""
import pytest
@pytest.mark.time_machine("2023-01-01", tick=False)
async def test_function():
pass
""",
)
def test_marker_tick(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time("2023-01-01", tick=True)
def test_function():
pass
""",
"""
import pytest
@pytest.mark.time_machine("2023-01-01", tick=True)
def test_function():
pass
""",
)
def test_marker_not_called(self):
check_noop(
"""
import pytest
@pytest.mark.freeze_time
def test_function():
pass
""",
reports=[(4, 2, "pytest.mark.freeze_time usage not migrated")],
)
def test_marker_unmigratable_call(self):
check_noop(
"""
import pytest
@pytest.mark.freeze_time("2023-01-01", auto_tick_seconds=1)
def test_function():
pass
""",
reports=[(4, 2, "pytest.mark.freeze_time usage not migrated")],
)
def test_marker_unrelated(self):
check_noop(
"""
import pytest
@pytest.mark.slow_time("2023-01-01")
def test_function():
pass
""",
)
def test_marker_class(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time("2023-01-01")
class TestSomething:
def test_function(self):
pass
""",
"""
import pytest
@pytest.mark.time_machine("2023-01-01", tick=False)
class TestSomething:
def test_function(self):
pass
""",
)
def test_pytestmark_module(self):
check_transformed(
"""
import pytest
pytestmark = pytest.mark.freeze_time("2023-01-01")
""",
"""
import pytest
pytestmark = pytest.mark.time_machine("2023-01-01", tick=False)
""",
)
def test_pytestmark_module_tick(self):
check_transformed(
"""
import pytest
pytestmark = pytest.mark.freeze_time("2023-01-01", tick=True)
""",
"""
import pytest
pytestmark = pytest.mark.time_machine("2023-01-01", tick=True)
""",
)
def test_pytestmark_module_list(self):
check_transformed(
"""
import pytest
pytestmark = [
pytest.mark.freeze_time("2023-01-01"),
pytest.mark.django_db,
]
""",
"""
import pytest
pytestmark = [
pytest.mark.time_machine("2023-01-01", tick=False),
pytest.mark.django_db,
]
""",
)
def test_pytestmark_module_tuple(self):
check_transformed(
"""
import pytest
pytestmark = (pytest.mark.freeze_time("2023-01-01"),)
""",
"""
import pytest
pytestmark = (pytest.mark.time_machine("2023-01-01", tick=False),)
""",
)
def test_pytestmark_module_unrelated_marker(self):
check_noop(
"""
import pytest
pytestmark = pytest.mark.django_db()
""",
)
def test_pytestmark_module_not_called(self):
check_noop(
"""
import pytest
pytestmark = pytest.mark.freeze_time
""",
reports=[(4, 14, "pytest.mark.freeze_time usage not migrated")],
)
def test_pytestmark_module_other_value(self):
check_noop(
"""
pytestmark = marks
""",
)
def test_pytestmark_class(self):
check_transformed(
"""
import pytest
class TestSomething:
pytestmark = pytest.mark.freeze_time("2023-01-01")
""",
"""
import pytest
class TestSomething:
pytestmark = pytest.mark.time_machine("2023-01-01", tick=False)
""",
)
def test_pytestmark_module_freezer_fixture(self):
check_transformed(
"""
import pytest
pytestmark = pytest.mark.freeze_time("2000-01-01")
def test_function(freezer):
freezer.move_to("2023-01-01")
""",
"""
import pytest
pytestmark = pytest.mark.time_machine("2000-01-01", tick=False)
def test_function(time_machine):
time_machine.move_to("2023-01-01")
""",
)
def test_pytestmark_module_freezer_fixture_defined_after(self):
check_transformed(
"""
import pytest
def test_function(freezer):
freezer.move_to("2023-01-01")
pytestmark = pytest.mark.freeze_time("2000-01-01")
""",
"""
import pytest
def test_function(time_machine):
time_machine.move_to("2023-01-01")
pytestmark = pytest.mark.time_machine("2000-01-01", tick=False)
""",
)
def test_pytestmark_class_freezer_fixture(self):
check_transformed(
"""
import pytest
class TestSomething:
pytestmark = [pytest.mark.freeze_time("2000-01-01")]
def test_function(self, freezer):
freezer.move_to("2023-01-01")
""",
"""
import pytest
class TestSomething:
pytestmark = [pytest.mark.time_machine("2000-01-01", tick=False)]
def test_function(self, time_machine):
time_machine.move_to("2023-01-01")
""",
)
def test_reports_import_multiple(self):
check_noop(
"""
import freezegun, os
@freezegun.freeze_time("2023-01-01")
def test_function():
pass
""",
reports=[(4, 2, "freezegun usage not migrated")],
)
def test_reports_import_dotted(self):
check_noop(
"""
import freezegun.config
freezegun.config.configure(extend_ignore_list=["tensorflow"])
""",
reports=[(4, 1, "freezegun usage not migrated")],
)
def test_reports_import_dotted_aliased(self):
check_noop(
"""
import freezegun.api as fg_api
x = fg_api.FakeDatetime(2020, 1, 1)
""",
reports=[(4, 5, "fg_api usage not migrated")],
)
def test_reports_sorted(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time
def test_one():
pass
def test_two():
with freezegun.freeze_time("2023-01-01") as ft:
helper(ft)
""",
"""
import time_machine
@freezegun.freeze_time
def test_one():
pass
def test_two():
with freezegun.freeze_time("2023-01-01") as ft:
helper(ft)
""",
reports=[
(4, 2, "freezegun usage not migrated"),
(9, 10, "freezegun usage not migrated"),
],
)
def test_freezer_fixture(self):
check_transformed(
"""
def test_function(freezer):
freezer.move_to("2023-01-01")
""",
"""
def test_function(time_machine):
time_machine.move_to("2023-01-01", tick=False)
""",
)
def test_freezer_fixture_async_function(self):
check_transformed(
"""
async def test_function(freezer):
freezer.move_to("2023-01-01")
""",
"""
async def test_function(time_machine):
time_machine.move_to("2023-01-01", tick=False)
""",
)
def test_freezer_fixture_existing_time_machine_argument(self):
# Renaming would duplicate the argument, giving invalid syntax.
check_noop(
"""
def test_function(freezer, time_machine):
freezer.move_to("2023-01-01")
freezer.tick()
""",
)
def test_freezer_fixture_existing_time_machine_vararg(self):
# Renaming would duplicate the argument, giving invalid syntax.
check_noop(
"""
def test_function(freezer, *time_machine):
freezer.move_to("2023-01-01")
freezer.tick()
""",
)
def test_freezer_fixture_existing_time_machine_kwarg(self):
# Renaming would duplicate the argument, giving invalid syntax.
check_noop(
"""
def test_function(freezer, **time_machine):
freezer.move_to("2023-01-01")
freezer.tick()
""",
)
def test_freezer_fixture_keyword_only(self):
check_transformed(
"""
def test_function(*, freezer):
freezer.move_to("2023-01-01")
""",
"""
def test_function(*, time_machine):
time_machine.move_to("2023-01-01", tick=False)
""",
)
def test_freezer_fixture_move_to_extra_arguments(self):
check_transformed(
"""
def test_function(freezer):
freezer.move_to("2023-01-01", "2024-01-01")
""",
"""
def test_function(time_machine):
freezer.move_to("2023-01-01", "2024-01-01")
""",
)
def test_freezer_fixture_parenthesized_receiver(self):
check_transformed(
"""
def test_function(freezer):
(freezer).move_to("2023-01-01")
(freezer).tick()
""",
"""
def test_function(time_machine):
(time_machine).move_to("2023-01-01", tick=False)
(time_machine).shift(1)
""",
)
def test_with_as_tick_parenthesized_receiver(self):
check_transformed(
"""
from freezegun import freeze_time
with freeze_time("2023-01-01") as tick:
(tick).tick()
""",
"""
import time_machine
with time_machine.travel("2023-01-01", tick=False) as tick:
(tick).shift(1)
""",
)
def test_with_attr_parenthesized_module(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_reports_positioned_after_rewrite_on_same_line(self):
check_transformed(
"""
pytestmark = [pytest.mark.freeze_time(), pytest.mark.freeze_time]
""",
"""
pytestmark = [pytest.mark.time_machine(None, tick=False), pytest.mark.freeze_time]
""",
reports=[(2, 59, "pytest.mark.freeze_time usage not migrated")],
)
def test_reports_positioned_after_rewrite_within_fstring(self):
# On Python < 3.12, names within f-strings have no tokens of their own.
check_transformed(
"""
from freezegun import freeze_time, FrozenDateTimeFactory
print(f"{FrozenDateTimeFactory} here")
""",
"""
import time_machine
from freezegun import FrozenDateTimeFactory
print(f"{FrozenDateTimeFactory} here")
""",
reports=[(5, 10, "FrozenDateTimeFactory usage not migrated")],
)
def test_freezer_fixture_other_method(self):
check_transformed(
"""
def test_function(freezer):
freezer.move_to("2023-01-01")
freezer.start()
""",
"""
def test_function(time_machine):
time_machine.move_to("2023-01-01", tick=False)
freezer.start()
""",
)
def test_freezer_fixture_tick(self):
check_transformed(
"""
from datetime import timedelta
def test_function(freezer):
freezer.tick()
freezer.tick(10.0)
freezer.tick(100)
freezer.tick(timedelta(seconds=100))
freezer.tick(delta=timedelta(seconds=100))
""",
"""
from datetime import timedelta
def test_function(time_machine):
time_machine.shift(1)
time_machine.shift(10.0)
time_machine.shift(100)
time_machine.shift(timedelta(seconds=100))
time_machine.shift(delta=timedelta(seconds=100))
""",
)
def test_freezer_fixture_tick_assigned(self):
check_transformed(
"""
def test_function(freezer):
now = freezer.tick()
""",
"""
def test_function(time_machine):
now = freezer.tick()
""",
)
def test_freezer_fixture_move_to_assigned(self):
check_transformed(
"""
def test_function(freezer):
result = freezer.move_to("2023-01-01")
""",
"""
def test_function(time_machine):
result = time_machine.move_to("2023-01-01", tick=False)
""",
)
def test_freezer_fixture_shadowed_freeze_time(self):
check_transformed(
"""
from freezegun import freeze_time
def test_function(freezer):
freezer.move_to("2023-01-01")
with freeze_time("2024-01-01"):
pass
""",
"""
import time_machine
def test_function(time_machine):
time_machine.move_to("2023-01-01", tick=False)
with freeze_time("2024-01-01"):
pass
""",
reports=[(6, 10, "freeze_time usage not migrated")],
)
def test_freezer_fixture_no_argument_noop(self):
check_noop(
"""
def test_function():
freezer.move_to("2023-01-01")
""",
)
def test_freezer_fixture_module_level_noop(self):
check_noop(
"""
freezer.move_to("2023-01-01")
""",
)
def test_freezer_fixture_with_marker(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time("2000-01-01")
def test_function(freezer):
freezer.move_to("2023-01-01")
""",
"""
import pytest
@pytest.mark.time_machine("2000-01-01", tick=False)
def test_function(time_machine):
time_machine.move_to("2023-01-01")
""",
)
def test_freezer_fixture_with_marker_tick(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time("2000-01-01", tick=True)
def test_function(freezer):
freezer.move_to("2023-01-01")
""",
"""
import pytest
@pytest.mark.time_machine("2000-01-01", tick=True)
def test_function(time_machine):
time_machine.move_to("2023-01-01")
""",
)
def test_freezer_fixture_with_class_marker(self):
check_transformed(
"""
import pytest
@pytest.mark.freeze_time("2000-01-01")
class TestSomething:
def test_function(self, freezer):
freezer.move_to("2023-01-01")
""",
"""
import pytest
@pytest.mark.time_machine("2000-01-01", tick=False)
class TestSomething:
def test_function(self, time_machine):
time_machine.move_to("2023-01-01")
""",
)
def test_freezer_fixture_with_decorator(self):
check_transformed(
"""
import freezegun
@freezegun.freeze_time("2000-01-01")
def test_function(freezer):
freezer.move_to("2023-01-01")
""",
"""
import time_machine
@time_machine.travel("2000-01-01", tick=False)
def test_function(time_machine):
time_machine.move_to("2023-01-01", tick=False)
""",
)
def test_freezer_fixture_mix(self):
check_transformed(
"""
import freezegun
import pytest
def test_function():
with freezegun.freeze_time("2000-01-01") as t:
t.move_to("2023-01-01")
t.tick()
@pytest.mark.freeze_time("2000-01-01")
def test_function2(freezer):
freezer.move_to("2023-01-01")
freezer.tick()
""",
"""
import time_machine
import pytest
def test_function():
with time_machine.travel("2000-01-01", tick=False) as t:
t.move_to("2023-01-01")
t.shift(1)
@pytest.mark.time_machine("2000-01-01", tick=False)
def test_function2(time_machine):
time_machine.move_to("2023-01-01")
time_machine.shift(1)
""",
)
adamchainz-time-machine-18d4852/tests/test_cli_fuzz.py 0000664 0000000 0000000 00000040041 15250103717 0023030 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import ast
import sys
import pytest
if sys.version_info[:2] == (3, 13) and not sys._is_gil_enabled():
# Hypothesis has no free-threaded wheels for Python 3.13, and cannot be
# built from source there, since PyO3 does not support free-threaded
# Python < 3.14.
pytest.skip("Hypothesis unavailable", allow_module_level=True)
from hypothesis import given, settings
from hypothesis import strategies as st
from time_machine.cli import Report, migrate_contents
# Fuzz the migration CLI with generated source files combining the constructs
# that it targets, varying formatting. The generated code only needs to parse,
# not run, so undefined names are fine.
def indent(block: str) -> str:
return "\n".join(f" {line}" for line in block.splitlines())
freeze_time_callees = st.sampled_from(
[
"freeze_time",
"freezegun.freeze_time",
"(freeze_time)",
"fg.freeze_time",
"ft_alias",
"freezegun.api.freeze_time",
"(freezegun).freeze_time",
"freezegun . freeze_time",
]
)
freeze_time_arglists = st.sampled_from(
[
(),
('"2023-01-01"',),
("dest",),
("dt.datetime(2023, 1, 1)",),
('"2023-01-01 ünïcode"',),
("*args",),
("**kwargs",),
('"2023-01-01"', "tick=True"),
('"2023-01-01"', "tick=False"),
('"2023-01-01"', "tick=tick"),
('"2023-01-01"', "auto_tick_seconds=1"),
('"2023-01-01"', "auto_tick_seconds=0"),
('"2023-01-01"', "as_arg=True"),
('"2023-01-01"', "tz_offset=0"),
('"2023-01-01"', "tz_offset=0.0"),
('"2023-01-01"', "tz_offset=-0"),
('"2023-01-01"', "tz_offset=-4"),
('"2023-01-01"', "tz_offset=0", "tick=True"),
('"2023-01-01"', "real_asyncio=True"),
('"2023-01-01"', "real_asyncio=False"),
('"2023-01-01"', 'ignore=["threading"]'),
('"2023-01-01"', "ignore=[]"),
("tz_offset=0",),
("tick=True",),
("real_asyncio=True",),
("tz_offset=0", "tick=True"),
("tz_offset=0", "real_asyncio=True", 'ignore=["threading"]'),
]
)
method_arglists = st.sampled_from(
[
(),
('"2023-01-01"',),
('"2023-01-01"', '"2024-01-01"'),
("1",),
("delta=1",),
("delta=1", "other=2"),
("tick=True",),
("*args",),
]
)
@st.composite
def calls(
draw: st.DrawFn, callee: str, arglists: st.SearchStrategy[tuple[str, ...]]
) -> str:
args = list(draw(arglists))
style = draw(
st.sampled_from(
[
"plain",
"spaced",
"trailing_comma",
"multiline",
"multiline_no_trailing_comma",
"backslash",
"comment_after_paren",
]
)
)
if not args or style == "plain":
return f"{callee}({', '.join(args)})"
elif style == "spaced":
return f"{callee} ( {' , '.join(args)} )"
elif style == "trailing_comma":
return f"{callee}({', '.join(args)},)"
elif style == "multiline":
comment = " # comment" if draw(st.booleans()) else ""
return f"{callee}(" + "".join(f"\n {arg},{comment}" for arg in args) + "\n)"
elif style == "multiline_no_trailing_comma":
return f"{callee}(\n " + ",\n ".join(args) + "\n)"
elif style == "backslash":
return f"{callee}(\\\n {', '.join(args)}\\\n)"
else:
return f"{callee}( # comment\n {', '.join(args)}\n)"
@st.composite
def freeze_time_calls(draw: st.DrawFn) -> str:
return draw(calls(draw(freeze_time_callees), freeze_time_arglists))
@st.composite
def decorators(draw: st.DrawFn) -> str:
kind = draw(st.sampled_from(["freeze_time", "marker", "not_called", "unrelated"]))
if kind == "freeze_time":
return "@" + draw(freeze_time_calls())
elif kind == "marker":
return "@" + draw(calls("pytest.mark.freeze_time", freeze_time_arglists))
elif kind == "not_called":
return draw(
st.sampled_from(
[
"@freeze_time",
"@pytest.mark.freeze_time",
"@freezegun.freeze_time",
"@ft_alias",
]
)
)
else:
return '@mock.patch("example.thing")'
@st.composite
def pytestmark_statements(draw: st.DrawFn) -> str:
markers = draw(
st.lists(
calls("pytest.mark.freeze_time", freeze_time_arglists)
| st.sampled_from(["pytest.mark.freeze_time", "pytest.mark.django_db"]),
min_size=1,
max_size=2,
)
)
if len(markers) == 1 and draw(st.booleans()):
return f"pytestmark = {markers[0]}"
wrapper = draw(st.sampled_from(["[{}]", "({},)"]))
return "pytestmark = " + wrapper.format(", ".join(markers))
@st.composite
def raw_assignment_statements(draw: st.DrawFn) -> str:
receiver = draw(st.sampled_from(["freezer", "ft", "self.freezer", "freeze_time"]))
lines = [f"{receiver} = " + draw(freeze_time_calls())]
for method in draw(
st.lists(st.sampled_from(["start", "stop", "move_to", "tick"]), max_size=2)
):
form = draw(st.sampled_from(["call", "call", "reference", "chained"]))
if form == "call":
lines.append(draw(calls(f"{receiver}.{method}", method_arglists)))
elif form == "reference":
lines.append(f"cleanup({receiver}.{method})")
else:
lines.append(f"x = {receiver}.{method}().other")
if draw(st.booleans()):
lines.append(f"{receiver} = other()")
return "\n".join(lines)
@st.composite
def method_statements(draw: st.DrawFn) -> str:
receiver = draw(st.sampled_from(["freezer", "t", "ft", "tick", "other"]))
kind = draw(st.sampled_from(["call", "call", "call", "attribute", "reference"]))
if kind == "attribute":
return f"assert {receiver}.time_to_freeze"
elif kind == "reference":
return f"helper({receiver})"
wrapped = draw(st.sampled_from(["{}", "({})", "( {} )", "({}\n)"])).format(receiver)
dot = draw(st.sampled_from([".", " . "]))
method = draw(st.sampled_from(["move_to", "tick", "shift", "start"]))
statement = draw(calls(f"{wrapped}{dot}{method}", method_arglists))
if draw(st.booleans()):
statement = f"x = {statement}"
if draw(st.booleans()):
statement += "; " + draw(calls(f"{receiver}.tick", method_arglists))
if draw(st.booleans()):
statement = 'x = "ünïcode"; ' + statement
return statement
@st.composite
def with_statements(draw: st.DrawFn) -> str:
items = []
for _ in range(draw(st.integers(1, 2))):
item = draw(freeze_time_calls())
as_name = draw(st.sampled_from([None, "t", "ft", "tick", "freezer"]))
if as_name is not None:
item += f" as {as_name}"
items.append(item)
body = draw(
st.lists(
st.just("pass") | method_statements() | nested_with_statements(),
min_size=1,
max_size=2,
)
)
joined = ", ".join(items)
if len(items) > 1 and draw(st.booleans()):
joined = f"({joined})"
statement = f"with {joined}:\n" + "\n".join(indent(s) for s in body)
wrapper = draw(st.sampled_from([None, None, "if", "for", "try"]))
if wrapper == "if":
statement = "if True:\n" + indent(statement)
elif wrapper == "for":
statement = "for x in things:\n" + indent(statement)
elif wrapper == "try":
statement = "try:\n" + indent(statement) + "\nfinally:\n pass"
return statement
@st.composite
def nested_with_statements(draw: st.DrawFn) -> str:
item = draw(freeze_time_calls())
as_name = draw(st.sampled_from([None, "ft", "inner"]))
if as_name is not None:
item += f" as {as_name}"
body = draw(method_statements())
return f"with {item}:\n" + indent(body)
@st.composite
def annotation_statements(draw: st.DrawFn) -> str:
annotation = draw(
st.sampled_from(
[
"FrozenDateTimeFactory",
'"FrozenDateTimeFactory"',
"'FrozenDateTimeFactory'",
'"FrozenDateTimeFactory | None"',
'Optional["FrozenDateTimeFactory"]',
"FDF",
'"FDF"',
]
)
)
return f"fixture: {annotation} = None"
other_statements = st.sampled_from(
[
"FrozenDateTimeFactory = None",
'name = "FrozenDateTimeFactory"',
'print(f"{FrozenDateTimeFactory} here")',
"x = FrozenDateTimeFactory()",
"freeze_time = None",
"x = freeze_time",
'freeze_time("2023-01-01")(function)',
'freeze_time("2023-01-01").start()',
"from freezegun import freeze_time, FakeDate",
"from freezegun.api import FrozenDateTimeFactory",
"import freezegun",
]
)
@st.composite
def function_defs(draw: st.DrawFn) -> str:
lines = draw(st.lists(decorators(), max_size=2))
prefix = "async " if draw(st.booleans()) else ""
params = draw(
st.sampled_from(
[
"",
"freezer",
"self, freezer",
"*, freezer",
"freezer, other",
"freezer=None",
"other",
"freezer: FrozenDateTimeFactory",
'freezer: "FrozenDateTimeFactory"',
'freezer: Optional["FrozenDateTimeFactory"]',
'freezer: "FrozenDateTimeFactory | None"',
"freezer: FDF",
"freezer, time_machine",
"FrozenDateTimeFactory",
"freeze_time",
]
)
)
returns = draw(
st.sampled_from(["", " -> None", " -> FrozenDateTimeFactory", ' -> "FDF"'])
)
lines.append(f"{prefix}def test_function({params}){returns}:")
body = draw(
st.lists(
st.just("pass")
| other_statements
| annotation_statements()
| method_statements()
| raw_assignment_statements()
| with_statements(),
min_size=1,
max_size=3,
)
)
lines.extend(indent(s) for s in body)
return "\n".join(lines)
@st.composite
def class_defs(draw: st.DrawFn) -> str:
lines = draw(st.lists(decorators(), max_size=1))
bases = draw(st.sampled_from(["", "(unittest.TestCase)", "(TestBase)"]))
lines.append(f"class TestSomething{bases}:")
body = draw(
st.lists(
st.just("pass")
| pytestmark_statements()
| function_defs()
| raw_assignment_statements()
| with_statements(),
min_size=1,
max_size=2,
)
)
lines.extend(indent(s) for s in body)
return "\n".join(lines)
imports = st.sampled_from(
[
"import freezegun",
"import freezegun as fg",
"import freezegun.api",
"import freezegun, os",
"from freezegun import freeze_time",
"from freezegun import freeze_time as ft_alias",
"from freezegun import freeze_time, FakeDate",
"from freezegun import freeze_time, FrozenDateTimeFactory",
"from freezegun.api import freeze_time",
"from freezegun.api import FrozenDateTimeFactory",
"from freezegun.api import FrozenDateTimeFactory as FDF",
"from freezegun import FrozenDateTimeFactory # noqa",
"if True: from freezegun import freeze_time, FakeDate",
"if TYPE_CHECKING: from freezegun.api import FrozenDateTimeFactory",
"if TYPE_CHECKING:\n from freezegun.api import FrozenDateTimeFactory",
"import os; from freezegun import freeze_time; import sys",
(
"try:\n"
" from freezegun import freeze_time\n"
"except ImportError:\n"
" freeze_time = None"
),
"import pytest",
"import time_machine",
"from typing import Optional, TYPE_CHECKING",
"from __future__ import annotations",
"from othermod import FrozenDateTimeFactory",
]
)
@st.composite
def modules(draw: st.DrawFn) -> str:
parts = draw(st.lists(imports, unique=True, max_size=4))
parts.extend(
draw(
st.lists(
function_defs()
| class_defs()
| with_statements()
| method_statements()
| raw_assignment_statements()
| pytestmark_statements()
| annotation_statements()
| other_statements,
min_size=1,
max_size=4,
)
)
)
source = "\n\n".join(parts) + "\n"
if draw(st.booleans()):
source = "# -*- coding: utf-8 -*-\n" + source
return source
def freezegun_bound_names(tree: ast.Module) -> set[str]:
"""
The names that the module's freezegun imports bind, that migration is
expected to either rewrite or report.
"""
names = set()
for node in ast.walk(tree):
match node:
case ast.Import():
for alias in node.names:
if alias.name == "freezegun" or alias.name.startswith("freezegun."):
names.add(alias.asname or "freezegun")
case ast.ImportFrom(module="freezegun" | "freezegun.api"):
for alias in node.names:
if alias.name in ("freeze_time", "FrozenDateTimeFactory"):
names.add(alias.asname or alias.name)
return names
def check_report_positions(source: str, reports: list[Report]) -> None:
"""
Each report must point at the reported name in the given source, by
1-based line and 1-based UTF-8 byte column.
"""
lines = source.splitlines()
for report in reports:
name = report.message.removesuffix(" usage not migrated")
assert name != report.message, report.message
expected = name.split(".")[0].encode()
assert 1 <= report.lineno <= len(lines), report
text = lines[report.lineno - 1].encode()[report.col - 1 :]
if text.startswith(expected):
continue
# A string annotation containing the name.
assert text.lstrip(b"rRuU")[:1] in (b'"', b"'"), (report, text)
assert expected in text, (report, text)
@settings(deadline=None, max_examples=500)
@given(source=modules())
def test_migrate_contents_properties(source: str) -> None:
tree = ast.parse(source) # the grammar should only generate valid code
migrated, reports = migrate_contents(source) # must not crash
# the output must still be valid Python
migrated_tree = ast.parse(migrated)
# migration must be idempotent
assert migrate_contents(migrated)[0] == migrated
# nothing to migrate means nothing changed
if "freeze" not in source:
assert migrated == source
assert reports == []
# reports are sorted and unique, and point at the named usage in the
# migrated file, which is what the user sees them alongside.
assert reports == sorted(set(reports))
check_report_positions(migrated, reports)
# migrated calls have a single destination and only the tick keyword
for node in ast.walk(migrated_tree):
match node:
case ast.Call(
func=ast.Attribute(
value=ast.Name(id="time_machine"),
attr="travel",
)
| ast.Attribute(
value=ast.Attribute(
value=ast.Name(id="pytest"),
attr="mark",
),
attr="time_machine",
)
):
assert len(node.args) == 1, ast.unparse(node)
assert [kw.arg for kw in node.keywords] == ["tick"], ast.unparse(node)
# without reports, no uses of the names bound by freezegun imports remain
if not reports:
bound = freezegun_bound_names(tree)
remaining = [
node
for node in ast.walk(migrated_tree)
if isinstance(node, ast.Name) and node.id in bound
]
assert remaining == [], [ast.unparse(node) for node in remaining]
adamchainz-time-machine-18d4852/tests/test_fuzz.py 0000664 0000000 0000000 00000063327 15250103717 0022215 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import datetime as dt
import os
import sys
import time
import uuid
import warnings
from collections.abc import Callable, Generator, Iterator
from contextlib import contextmanager
from unittest import mock
from zoneinfo import ZoneInfo
import pytest
if sys.version_info[:2] == (3, 13) and not sys._is_gil_enabled():
# Hypothesis has no free-threaded wheels for Python 3.13, and cannot be
# built from source there, since PyO3 does not support free-threaded
# Python < 3.14.
pytest.skip("Hypothesis unavailable", allow_module_level=True)
from hypothesis import assume, given, settings
from hypothesis import strategies as st
from hypothesis.stateful import RuleBasedStateMachine, invariant, precondition, rule
import time_machine
from tests.conftest import HAVE_64_BIT_TIME_T
NANOSECONDS_PER_SECOND = time_machine.NANOSECONDS_PER_SECOND
EPOCH_AWARE = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc)
# Bounds that keep generated timestamps positive, whatever timezone offset is
# applied, and well within the range that all supported platforms can convert.
MIN_DATETIME = dt.datetime(1970, 1, 2)
if HAVE_64_BIT_TIME_T:
MAX_DATETIME = dt.datetime(2500, 1, 1)
else: # pragma: no cover
# 32-bit time_t cannot represent timestamps beyond 2038-01-19.
MAX_DATETIME = dt.datetime(2038, 1, 1)
MIN_TIMESTAMP = MIN_DATETIME.replace(tzinfo=dt.timezone.utc).timestamp()
MAX_TIMESTAMP = MAX_DATETIME.replace(tzinfo=dt.timezone.utc).timestamp()
# The "right/*" zones count leap seconds in POSIX timestamps, which makes
# timestamps inconsistent with datetime arithmetic. They're only installed on
# some systems.
zoneinfos = st.timezones().filter(lambda tz: not tz.key.startswith("right/"))
naive_datetimes = st.datetimes(min_value=MIN_DATETIME, max_value=MAX_DATETIME)
aware_datetimes = st.datetimes(
min_value=MIN_DATETIME,
max_value=MAX_DATETIME,
timezones=st.just(dt.timezone.utc) | zoneinfos,
)
timestamps = st.floats(min_value=MIN_TIMESTAMP, max_value=MAX_TIMESTAMP)
def datetime_to_ns(destination: dt.datetime) -> int:
# timedelta arithmetic is exact, unlike float timestamps.
return (destination - EPOCH_AWARE) // dt.timedelta(microseconds=1) * 1_000
def timedelta_to_ns(delta: dt.timedelta) -> int:
return delta // dt.timedelta(microseconds=1) * 1_000
@settings(deadline=None)
@given(timestamp=timestamps)
def test_travel_to_timestamp(timestamp):
expected_ns = round(timestamp * NANOSECONDS_PER_SECOND)
with time_machine.travel(timestamp, tick=False):
assert time.time_ns() == expected_ns
assert time.time() == expected_ns / NANOSECONDS_PER_SECOND
@settings(deadline=None)
@given(destination=aware_datetimes)
def test_travel_to_aware_datetime(destination):
with time_machine.travel(destination, tick=False):
assert time.time_ns() == datetime_to_ns(destination)
now_utc = dt.datetime.now(dt.timezone.utc)
assert now_utc == destination
assert dt.datetime.now(destination.tzinfo) == now_utc
@settings(deadline=None)
@given(destination=naive_datetimes)
def test_travel_to_naive_datetime_treated_as_utc(destination):
# In the default naive_mode (MIXED), naive datetimes are treated as UTC.
with time_machine.travel(destination, tick=False):
assert time.time_ns() == datetime_to_ns(
destination.replace(tzinfo=dt.timezone.utc)
)
assert dt.datetime.now(dt.timezone.utc).replace(tzinfo=None) == destination
@settings(deadline=None)
@given(
destination=st.dates(min_value=MIN_DATETIME.date(), max_value=MAX_DATETIME.date())
)
def test_travel_to_date(destination):
midnight = dt.datetime.combine(destination, dt.time(0, 0), tzinfo=dt.timezone.utc)
with time_machine.travel(destination, tick=False):
assert time.time_ns() == datetime_to_ns(midnight)
assert time.time() == midnight.timestamp()
now = dt.datetime.now(dt.timezone.utc)
assert now == midnight
assert now.date() == destination
@settings(deadline=None)
@given(destination=aware_datetimes)
def test_travel_to_isoformat_string(destination):
with time_machine.travel(destination.isoformat(), tick=False):
assert time.time_ns() == datetime_to_ns(destination)
@settings(deadline=None)
@given(outer=timestamps, inner=timestamps)
def test_travel_nested(outer, inner):
with time_machine.travel(outer, tick=False):
with time_machine.travel(inner, tick=False):
assert time.time_ns() == round(inner * NANOSECONDS_PER_SECOND)
assert time.time_ns() == round(outer * NANOSECONDS_PER_SECOND)
@settings(deadline=None)
@given(timestamp=timestamps)
def test_travel_tick_monotonic(timestamp):
expected_ns = round(timestamp * NANOSECONDS_PER_SECOND)
with time_machine.travel(timestamp):
first = time.time_ns()
second = time.time_ns()
assert expected_ns <= first <= second
assert second - first < 10 * NANOSECONDS_PER_SECOND
@settings(deadline=None)
@given(
delta=st.timedeltas(
min_value=dt.timedelta(days=-18_000), max_value=dt.timedelta(days=18_000)
)
)
def test_shift_timedelta(delta):
start = dt.datetime(2020, 4, 29, tzinfo=dt.timezone.utc)
with time_machine.travel(start, tick=False) as traveller:
traveller.shift(delta)
assert time.time_ns() == datetime_to_ns(start) + timedelta_to_ns(delta)
@settings(deadline=None)
@given(
delta_seconds=st.integers(min_value=-(2**30), max_value=2**30)
| st.floats(min_value=-(2.0**30), max_value=2.0**30)
)
def test_shift_number(delta_seconds):
start = dt.datetime(2020, 4, 29, tzinfo=dt.timezone.utc)
expected_delta_ns = round(delta_seconds * NANOSECONDS_PER_SECOND)
with time_machine.travel(start, tick=False) as traveller:
traveller.shift(delta_seconds)
assert time.time_ns() == datetime_to_ns(start) + expected_delta_ns
@settings(deadline=None)
@given(first=timestamps, second=timestamps)
def test_move_to(first, second):
with time_machine.travel(first, tick=False) as traveller:
assert time.time_ns() == round(first * NANOSECONDS_PER_SECOND)
traveller.move_to(second)
assert time.time_ns() == round(second * NANOSECONDS_PER_SECOND)
@pytest.mark.skipif(
not hasattr(time, "tzset"), reason="Doesn't have tzset, so travel() can't set TZ"
)
@settings(deadline=None)
@given(
# Past 2038, some platforms' time functions stop applying zones' POSIX DST
# rules, diverging from zoneinfo.
destination=st.datetimes(min_value=MIN_DATETIME, max_value=dt.datetime(2038, 1, 1)),
tz=zoneinfos,
)
def test_localtime_and_gmtime_match_datetime(destination, tz):
aware = destination.replace(tzinfo=tz)
with time_machine.travel(aware, tick=False):
seconds = time.time_ns() // NANOSECONDS_PER_SECOND
local = time.localtime()
expected_local = dt.datetime.fromtimestamp(seconds, tz)
assert (
local.tm_year,
local.tm_mon,
local.tm_mday,
local.tm_hour,
local.tm_min,
local.tm_sec,
) == (
expected_local.year,
expected_local.month,
expected_local.day,
expected_local.hour,
expected_local.minute,
expected_local.second,
)
utcoffset = expected_local.utcoffset()
assert utcoffset is not None
assert local.tm_gmtoff == int(utcoffset.total_seconds())
utc = time.gmtime()
expected_utc = dt.datetime.fromtimestamp(seconds, dt.timezone.utc)
assert (
utc.tm_year,
utc.tm_mon,
utc.tm_mday,
utc.tm_hour,
utc.tm_min,
utc.tm_sec,
) == (
expected_utc.year,
expected_utc.month,
expected_utc.day,
expected_utc.hour,
expected_utc.minute,
expected_utc.second,
)
@given(timestamp=timestamps)
def test_extract_timestamp_tzname_number(timestamp):
assert time_machine.extract_timestamp_tzname(timestamp) == (
round(timestamp * NANOSECONDS_PER_SECOND),
None,
)
int_timestamp = int(timestamp)
assert time_machine.extract_timestamp_tzname(int_timestamp) == (
int_timestamp * NANOSECONDS_PER_SECOND,
None,
)
@given(destination=naive_datetimes, tz=zoneinfos)
def test_extract_timestamp_tzname_zoneinfo_datetime(destination, tz):
aware = destination.replace(tzinfo=tz)
timestamp_ns, tzname = time_machine.extract_timestamp_tzname(aware)
assert timestamp_ns == datetime_to_ns(aware)
assert tzname == tz.key
@given(destination=naive_datetimes)
def test_extract_timestamp_tzname_utc_datetime(destination):
aware = destination.replace(tzinfo=dt.timezone.utc)
timestamp_ns, tzname = time_machine.extract_timestamp_tzname(aware)
assert timestamp_ns == datetime_to_ns(aware)
assert tzname == "UTC"
@given(
delta=st.timedeltas(
min_value=dt.timedelta(days=-1_000), max_value=dt.timedelta(days=1_000)
)
)
def test_extract_timestamp_tzname_timedelta(delta):
delta_ns = timedelta_to_ns(delta)
before_ns = time.time_ns()
timestamp_ns, tzname = time_machine.extract_timestamp_tzname(delta)
after_ns = time.time_ns()
assert tzname is None
assert before_ns + delta_ns <= timestamp_ns <= after_ns + delta_ns
# Consistency of all the mocked functions
def utc_datetime_from_ns(timestamp_ns: int) -> dt.datetime:
return EPOCH_AWARE + dt.timedelta(microseconds=timestamp_ns // 1_000)
def struct_time_fields(value: time.struct_time) -> tuple[int, ...]:
return (
value.tm_year,
value.tm_mon,
value.tm_mday,
value.tm_hour,
value.tm_min,
value.tm_sec,
)
def datetime_fields(value: dt.datetime) -> tuple[int, ...]:
return (
value.year,
value.month,
value.day,
value.hour,
value.minute,
value.second,
)
utc_datetimes = st.datetimes(
min_value=MIN_DATETIME, max_value=MAX_DATETIME, timezones=st.just(dt.timezone.utc)
)
@settings(deadline=None)
@given(destination=utc_datetimes)
def test_mocked_functions_agree(destination):
"""
Every mocked function returns its representation of the same instant.
The local timezone is UTC, per conftest.py, and not changed by UTC
destinations.
"""
expected_ns = datetime_to_ns(destination)
expected_utc = destination.astimezone(dt.timezone.utc)
expected_naive = expected_utc.replace(tzinfo=None)
expected_seconds = expected_ns // NANOSECONDS_PER_SECOND
before_clock_monotonic = time.clock_gettime(time.CLOCK_MONOTONIC)
before_monotonic = time.monotonic()
with time_machine.travel(destination, tick=False):
assert time.time_ns() == expected_ns
assert time.time() == expected_ns / NANOSECONDS_PER_SECOND
assert time.clock_gettime_ns(time.CLOCK_REALTIME) == expected_ns
assert time.clock_gettime(time.CLOCK_REALTIME) == time.time()
assert dt.datetime.now(dt.timezone.utc) == expected_utc
assert dt.datetime.now() == expected_naive
assert dt.datetime.today() == expected_naive
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
assert dt.datetime.utcnow() == expected_naive
assert dt.date.today() == expected_naive.date()
assert struct_time_fields(time.gmtime()) == datetime_fields(expected_utc)
assert struct_time_fields(time.localtime()) == datetime_fields(expected_utc)
assert time.gmtime().tm_gmtoff == 0
assert time.strftime("%Y-%m-%d %H:%M:%S") == expected_utc.strftime(
"%Y-%m-%d %H:%M:%S"
)
# Explicit arguments are unaffected.
assert time.gmtime(expected_seconds) == time.gmtime()
assert time.localtime(expected_seconds) == time.localtime()
assert time.strftime("%Y-%m-%d", time.gmtime(0)) == "1970-01-01"
# Other clocks are unaffected, so they keep advancing from their own
# epochs.
assert before_clock_monotonic <= time.clock_gettime(time.CLOCK_MONOTONIC)
assert before_monotonic <= time.monotonic()
@settings(deadline=None)
@given(
destination=aware_datetimes,
offset=st.integers(min_value=-24 * 3600 + 1, max_value=24 * 3600 - 1),
)
def test_datetime_now_fixed_offset(destination, offset):
tz = dt.timezone(dt.timedelta(seconds=offset))
with time_machine.travel(destination, tick=False):
assert dt.datetime.now(tz) == destination.astimezone(tz)
assert dt.datetime.now(tz=tz) == destination.astimezone(tz)
@settings(deadline=None)
@given(destination=utc_datetimes)
def test_datetime_subclasses(destination):
class MyDate(dt.date):
pass
class MyDateTime(dt.datetime):
pass
expected_naive = destination.astimezone(dt.timezone.utc).replace(tzinfo=None)
with time_machine.travel(destination, tick=False):
now = MyDateTime.now()
assert type(now) is MyDateTime
assert now == expected_naive
now_utc = MyDateTime.now(dt.timezone.utc)
assert type(now_utc) is MyDateTime
assert now_utc == destination
today = MyDateTime.today()
assert type(today) is MyDateTime
assert today == expected_naive
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
utcnow = MyDateTime.utcnow()
assert type(utcnow) is MyDateTime
assert utcnow == expected_naive
date_today = MyDate.today()
assert type(date_today) is MyDate
assert date_today == expected_naive.date()
strftime_directives = st.sampled_from(
[
"%Y",
"%m",
"%d",
"%H",
"%M",
"%S",
"%j",
"%a",
"%A",
"%b",
"%B",
"%p",
"%y",
"%Z",
"%z",
"%c",
"%x",
"%X",
"%U",
"%W",
"%w",
"%I",
"%%",
" ",
"-",
"ünïcode",
]
)
@settings(deadline=None)
@given(
# Past 2038, some platforms' time functions stop applying zones' POSIX DST
# rules, diverging from zoneinfo.
destination=st.datetimes(
min_value=MIN_DATETIME,
max_value=dt.datetime(2038, 1, 1),
timezones=st.just(dt.timezone.utc) | zoneinfos,
),
directives=st.lists(strftime_directives, min_size=1, max_size=6),
)
def test_strftime_formats(destination, directives):
fmt = "".join(directives)
with time_machine.travel(destination, tick=False):
assert time.strftime(fmt) == time.strftime(fmt, time.localtime())
# uuid
def time_from_uuid1(value: uuid.UUID) -> int:
"""The timestamp of a uuid1() or uuid6() value, in nanoseconds."""
return (value.time - 0x01B21DD213814000) * 100
def time_from_uuid7(value: uuid.UUID) -> int:
"""The timestamp of a uuid7() value, in nanoseconds."""
return value.time * 1_000_000
uuid_generators: list[
tuple[Callable[[], uuid.UUID], Callable[[uuid.UUID], int], int]
] = [(uuid.uuid1, time_from_uuid1, 100)]
if sys.version_info >= (3, 14):
uuid_generators.append((uuid.uuid6, time_from_uuid1, 100))
uuid_generators.append((uuid.uuid7, time_from_uuid7, 1_000_000))
# uuid7() stores unsigned milliseconds since the Unix epoch, so it cannot
# represent any earlier instant. Start destinations far enough after the epoch
# that the largest negative shift stays representable.
uuid_destinations = st.datetimes(
min_value=dt.datetime(1973, 1, 1),
max_value=MAX_DATETIME,
timezones=st.just(dt.timezone.utc) | zoneinfos,
)
@settings(deadline=None)
@given(
destination=uuid_destinations,
delta=st.timedeltas(
min_value=dt.timedelta(days=-1_000), max_value=dt.timedelta(days=1_000)
),
)
def test_uuid_timestamps(destination, delta):
"""
Time-based UUIDs are generated for the mocked time, including after
shifting in either direction, despite the uuid module's monotonicity
caching.
"""
# Without a shift, the second value is bumped to keep values monotonic.
assume(delta != dt.timedelta(0))
expected_ns = datetime_to_ns(destination)
shifted_ns = expected_ns + timedelta_to_ns(delta)
for generate, time_from, resolution_ns in uuid_generators:
with time_machine.travel(destination, tick=False) as traveller:
assert time_from(generate()) == expected_ns // resolution_ns * resolution_ns
traveller.shift(delta)
assert time_from(generate()) == shifted_ns // resolution_ns * resolution_ns
# Relative destinations
@settings(deadline=None)
@given(
first=timestamps,
delta=st.timedeltas(
min_value=dt.timedelta(days=-1_000), max_value=dt.timedelta(days=1_000)
),
)
def test_relative_destinations_use_mocked_time(first, delta):
first_ns = round(first * NANOSECONDS_PER_SECOND)
delta_ns = timedelta_to_ns(delta)
with time_machine.travel(first, tick=False) as traveller:
with time_machine.travel(delta, tick=False):
assert time.time_ns() == first_ns + delta_ns
with time_machine.travel(None, tick=False):
assert time.time_ns() == first_ns
traveller.move_to(delta)
assert time.time_ns() == first_ns + delta_ns
traveller.move_to(None)
assert time.time_ns() == first_ns + delta_ns
@settings(deadline=None)
@given(timestamp=timestamps)
def test_callable_and_generator_destinations(timestamp):
expected_ns = round(timestamp * NANOSECONDS_PER_SECOND)
def generate() -> Generator[float, None, None]:
yield timestamp
with time_machine.travel(lambda: timestamp, tick=False):
assert time.time_ns() == expected_ns
with time_machine.travel(generate(), tick=False):
assert time.time_ns() == expected_ns
# Strings
@settings(deadline=None)
@given(
destination=aware_datetimes,
style=st.sampled_from(["T", "space", "milliseconds", "seconds", "Z"]),
)
def test_travel_to_string_variants(destination, style):
# Lower-resolution formats truncate the microseconds they cannot represent.
expected = destination
if style == "T":
value = destination.isoformat()
elif style == "space":
value = destination.isoformat(sep=" ")
elif style == "milliseconds":
value = destination.isoformat(timespec="milliseconds")
expected = destination.replace(
microsecond=destination.microsecond // 1000 * 1000
)
elif style == "seconds":
value = destination.isoformat(timespec="seconds")
expected = destination.replace(microsecond=0)
else:
# fromisoformat() only accepts a "Z" suffix on Python 3.11+, so the
# expected value is derived from the datetime rather than the string.
value = destination.astimezone(dt.timezone.utc).isoformat()
value = value.replace("+00:00", "Z")
with time_machine.travel(value, tick=False):
assert time.time_ns() == datetime_to_ns(expected)
# Naive modes and the local timezone
@contextmanager
def local_timezone(key: str) -> Iterator[None]:
try:
with mock.patch.dict(os.environ, {"TZ": key}):
time.tzset()
yield
finally:
# patch.dict has restored the original TZ by now.
time.tzset()
def unambiguous_local(destination: dt.datetime, tz: ZoneInfo) -> bool:
"""
Check that the naive datetime is neither in a gap nor a fold of the given
zone, so its local interpretation is unambiguous.
"""
aware = destination.replace(tzinfo=tz)
if aware.utcoffset() != aware.replace(fold=1).utcoffset():
return False
return (
dt.datetime.fromtimestamp(aware.timestamp(), tz).replace(tzinfo=None)
== destination
)
def test_unambiguous_local():
tz = ZoneInfo("Europe/London")
assert unambiguous_local(dt.datetime(2023, 6, 1, 12, 0), tz)
# The hour repeats when the clocks go back.
assert not unambiguous_local(dt.datetime(2023, 10, 29, 1, 30), tz)
# The hour does not exist when the clocks go forward.
assert not unambiguous_local(dt.datetime(2023, 3, 26, 1, 30), tz)
@pytest.mark.skipif(
not hasattr(time, "tzset"), reason="Doesn't have tzset, so TZ can't be set"
)
@settings(deadline=None)
@given(
destination=st.datetimes(min_value=MIN_DATETIME, max_value=dt.datetime(2038, 1, 1)),
tz=zoneinfos,
mode=st.sampled_from(list(time_machine.NaiveMode)),
)
def test_naive_datetime_modes(destination, tz, mode):
assume(unambiguous_local(destination, tz))
as_utc = destination.replace(tzinfo=dt.timezone.utc)
as_local = destination.replace(tzinfo=tz)
with (
local_timezone(tz.key),
mock.patch.object(time_machine, "naive_mode", mode),
):
if mode == time_machine.NaiveMode.ERROR:
with pytest.raises(RuntimeError):
time_machine.travel(destination)
with pytest.raises(RuntimeError):
time_machine.travel(destination.date())
with pytest.raises(RuntimeError):
time_machine.travel(destination.isoformat())
return
if mode == time_machine.NaiveMode.LOCAL:
expected = as_local
expected_date = dt.datetime.combine(
destination.date(), dt.time(0, 0), tzinfo=tz
)
else:
expected = as_utc
expected_date = dt.datetime.combine(
destination.date(), dt.time(0, 0), tzinfo=dt.timezone.utc
)
if mode == time_machine.NaiveMode.UTC:
expected_string = as_utc
else:
# Naive strings are local in MIXED mode, for backwards compatibility.
expected_string = as_local
with time_machine.travel(destination, tick=False):
assert time.time_ns() == datetime_to_ns(expected)
with time_machine.travel(destination.isoformat(), tick=False):
assert time.time_ns() == datetime_to_ns(expected_string)
assume(
unambiguous_local(dt.datetime.combine(destination.date(), dt.time()), tz)
)
with time_machine.travel(destination.date(), tick=False):
assert time.time_ns() == datetime_to_ns(expected_date)
# Escape hatch
@settings(deadline=None)
@given(destination=aware_datetimes)
def test_escape_hatch_returns_real_time(destination):
with time_machine.travel(destination, tick=False):
real_ns = time_machine.escape_hatch.time.time_ns()
assert real_ns >= LIBRARY_EPOCH_NS
assert time_machine.escape_hatch.time.time() >= LIBRARY_EPOCH_NS / 1e9
assert time_machine.escape_hatch.datetime.datetime.now(
dt.timezone.utc
) >= utc_datetime_from_ns(LIBRARY_EPOCH_NS)
assert time_machine.escape_hatch.datetime.date.today() >= (
utc_datetime_from_ns(LIBRARY_EPOCH_NS).date() - dt.timedelta(days=1)
)
assert time_machine.escape_hatch.time.gmtime().tm_year >= 2020
assert time_machine.escape_hatch.time.localtime().tm_year >= 2020
assert int(time_machine.escape_hatch.time.strftime("%Y")) >= 2020
assert (
time_machine.escape_hatch.time.clock_gettime_ns(time.CLOCK_REALTIME)
>= LIBRARY_EPOCH_NS
)
LIBRARY_EPOCH_NS = datetime_to_ns(dt.datetime(2020, 4, 29, tzinfo=dt.timezone.utc))
# Stateful testing of nested travel, shift() and move_to()
class TravelMachine(RuleBasedStateMachine):
"""
Perform random sequences of travelling, shifting, moving and stopping,
checking that the mocked time always matches a simple model.
"""
def __init__(self) -> None:
super().__init__()
self.travels: list[time_machine.travel] = []
self.travellers: list[time_machine.Traveller] = []
self.expected_ns: list[int] = []
@rule(
timestamp=st.integers(
min_value=int(MIN_TIMESTAMP), max_value=int(MAX_TIMESTAMP)
)
)
def start(self, timestamp: int) -> None:
travel = time_machine.travel(timestamp, tick=False)
self.travels.append(travel)
self.travellers.append(travel.start())
self.expected_ns.append(timestamp * NANOSECONDS_PER_SECOND)
@precondition(lambda self: self.travels)
@rule()
def stop(self) -> None:
self.travels.pop().stop()
self.travellers.pop()
self.expected_ns.pop()
@precondition(lambda self: self.travels)
@rule(
delta=st.integers(min_value=-(10**9), max_value=10**9)
| st.timedeltas(
min_value=dt.timedelta(days=-10_000), max_value=dt.timedelta(days=10_000)
)
)
def shift(self, delta: int | dt.timedelta) -> None:
self.travellers[-1].shift(delta)
if isinstance(delta, int):
self.expected_ns[-1] += delta * NANOSECONDS_PER_SECOND
else:
self.expected_ns[-1] += timedelta_to_ns(delta)
@precondition(lambda self: self.travels)
@rule(destination=aware_datetimes)
def move_to(self, destination: dt.datetime) -> None:
self.travellers[-1].move_to(destination)
self.expected_ns[-1] = datetime_to_ns(destination)
@precondition(lambda self: self.travels)
@rule(delta=st.integers(min_value=-(10**6), max_value=10**6))
def move_to_relative(self, delta: int) -> None:
self.travellers[-1].move_to(dt.timedelta(seconds=delta))
self.expected_ns[-1] += delta * NANOSECONDS_PER_SECOND
@invariant()
def time_matches_model(self) -> None:
assert time_machine.escape_hatch.is_travelling() == bool(self.travels)
if self.travels:
assert time.time_ns() == self.expected_ns[-1]
assert dt.datetime.now(dt.timezone.utc) == utc_datetime_from_ns(
self.expected_ns[-1]
)
else:
assert time.time_ns() >= LIBRARY_EPOCH_NS
def teardown(self) -> None:
while self.travels:
self.travels.pop().stop()
TestTravelMachine = TravelMachine.TestCase
TestTravelMachine.settings = settings(
deadline=None, max_examples=100, stateful_step_count=30
)
adamchainz-time-machine-18d4852/tests/test_time_machine.py 0000664 0000000 0000000 00000207143 15250103717 0023635 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 unittest
import uuid
import warnings
from contextlib import contextmanager
from pathlib import Path
from textwrap import dedent
from unittest import SkipTest, TestCase, mock
from zoneinfo import ZoneInfo
import freezegun
import pytest
from dateutil import tz
import time_machine
from tests.conftest import HAVE_64_BIT_TIME_T
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"
)
py_have_subinterpreters = pytest.mark.skipif(
sys.version_info < (3, 13), reason="subinterpreter API added in Python 3.13"
)
py_utcnow_deprecated = pytest.mark.skipif(
sys.version_info < (3, 12), reason="utcnow() not deprecated before Python 3.12"
)
py_utcnow_not_deprecated = pytest.mark.skipif(
sys.version_info >= (3, 12), reason="utcnow() deprecated on Python 3.12+"
)
have_64_bit_time_t = pytest.mark.skipif(
not HAVE_64_BIT_TIME_T,
reason="Platform has 32-bit time_t, cannot represent distant destinations",
)
file_source = Path(__file__).read_text().splitlines()
def assert_warned_here(record: warnings.WarningMessage) -> None:
"""
Assert the warning was attributed to a line marked with a trailing
“# warns here” comment, rather than to time-machine’s own code.
"""
assert record.filename == __file__
assert file_source[record.lineno - 1].endswith("# warns here")
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()
# 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_now_too_many_args():
with time_machine.travel(EPOCH):
with pytest.raises(TypeError) as excinfo:
dt.datetime.now(dt.timezone.utc, "extra") # type: ignore[call-arg]
assert "now() takes at most 1 argument" in excinfo.value.args[0]
def test_datetime_now_invalid_kwarg():
with time_machine.travel(EPOCH):
with pytest.raises(TypeError) as excinfo:
dt.datetime.now(bad=dt.timezone.utc) # type: ignore[call-arg]
assert "got an unexpected keyword argument 'bad'" in excinfo.value.args[0]
def test_datetime_now_tz_given_twice():
with time_machine.travel(EPOCH):
with pytest.raises(TypeError) as excinfo:
dt.datetime.now(dt.timezone.utc, tz=dt.timezone.utc) # type: ignore[misc]
assert excinfo.value.args == ("now() takes at most 1 argument (2 given)",)
def test_datetime_now_subclass():
class DatetimeSubclass(dt.datetime):
pass
with time_machine.travel(EPOCH):
now = DatetimeSubclass.now()
assert isinstance(now, DatetimeSubclass)
assert now.year == 1970
@have_64_bit_time_t
def test_datetime_now_distant_destination_exact():
# Microsecond precision survives destinations far from the epoch, which a
# floating-point timestamp could not represent.
destination = dt.datetime(2500, 1, 1, microsecond=1, tzinfo=dt.timezone.utc)
with time_machine.travel(destination, tick=False):
assert dt.datetime.now(dt.timezone.utc) == destination
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
@py_utcnow_deprecated
def test_datetime_utcnow_deprecation_warning():
with pytest.warns(DeprecationWarning) as unpatched_records:
dt.datetime.utcnow() # warns here
assert_warned_here(unpatched_records[0])
with time_machine.travel(EPOCH), pytest.warns(DeprecationWarning) as records:
dt.datetime.utcnow() # warns here
assert len(records) == 1
assert str(records[0].message) == str(unpatched_records[0].message)
assert_warned_here(records[0])
@py_utcnow_deprecated
def test_datetime_utcnow_deprecation_error_no_reference_leak():
with time_machine.travel(EPOCH):
traveller = time_machine.traveller_stack[-1]
expected = sys.getrefcount(traveller)
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
for _ in range(10):
with pytest.raises(DeprecationWarning):
dt.datetime.utcnow()
assert sys.getrefcount(traveller) == expected
@py_utcnow_not_deprecated
def test_datetime_utcnow_no_deprecation_warning():
with time_machine.travel(EPOCH), warnings.catch_warnings():
warnings.simplefilter("error")
dt.datetime.utcnow()
def test_datetime_utcnow_no_tick():
with time_machine.travel(EPOCH, tick=False):
now = dt.datetime.utcnow()
assert now.microsecond == 0
@have_64_bit_time_t
def test_datetime_utcnow_distant_destination_exact():
destination = dt.datetime(2500, 1, 1, microsecond=1, tzinfo=dt.timezone.utc)
with time_machine.travel(destination, tick=False):
assert dt.datetime.utcnow() == destination.replace(tzinfo=None)
def test_datetime_utcnow_subclass():
class DatetimeSubclass(dt.datetime):
pass
with time_machine.travel(EPOCH):
now = DatetimeSubclass.utcnow()
assert isinstance(now, DatetimeSubclass)
assert now.year == 1970
assert now.tzinfo is None
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
def test_datetime_today_exact_far_future():
# A float timestamp this large cannot represent every microsecond.
destination = dt.datetime(2243, 1, 1, 0, 0, 0, 1, tzinfo=dt.timezone.utc)
with time_machine.travel(destination, tick=False):
assert dt.datetime.today() == destination.replace(tzinfo=None)
assert dt.date.today() == destination.date()
def test_datetime_today_subclass_exact_far_future():
class DatetimeSubclass(dt.datetime):
pass
destination = dt.datetime(2243, 1, 1, 0, 0, 0, 1, tzinfo=dt.timezone.utc)
with time_machine.travel(destination, tick=False):
today = DatetimeSubclass.today()
assert isinstance(today, DatetimeSubclass)
assert today == destination.replace(tzinfo=None)
def test_date_today_exact_far_future():
# A float timestamp this large rounds up past midnight, which would move
# the date on by a whole day.
destination = dt.datetime(3000, 1, 1, tzinfo=dt.timezone.utc) - dt.timedelta(
microseconds=1
)
with time_machine.travel(destination, tick=False):
assert dt.date.today() == dt.date(2999, 12, 31)
# 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_invalid_clk_id():
with time_machine.travel(EPOCH), pytest.raises(OSError):
time.clock_gettime(99999)
@py_have_clock_gettime
def test_time_clock_gettime_float_clk_id():
# Non-int clk_id values get the original function's behaviour, even if
# they compare equal to CLOCK_REALTIME.
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.clock_gettime(float(time.CLOCK_REALTIME)) # type: ignore[arg-type]
@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_invalid_clk_id():
with time_machine.travel(EPOCH), pytest.raises(OSError):
time.clock_gettime_ns(99999)
@py_have_clock_gettime
def test_time_clock_gettime_ns_float_clk_id():
# Non-int clk_id values get the original function's behaviour, even if
# they compare equal to CLOCK_REALTIME.
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.clock_gettime_ns(float(time.CLOCK_REALTIME)) # type: ignore[arg-type]
@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
@have_64_bit_time_t
def test_time_gmtime_no_args_distant_destination():
# The struct fields stay exact for distant destinations, since gmtime()
# receives whole integer seconds.
destination = dt.datetime(2500, 1, 1, microsecond=3, tzinfo=dt.timezone.utc)
with time_machine.travel(destination, tick=False):
local_time = time.gmtime()
assert (local_time.tm_year, local_time.tm_mon, local_time.tm_mday) == (2500, 1, 1)
assert (local_time.tm_hour, local_time.tm_min, local_time.tm_sec) == (0, 0, 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_gmtime_invalid_arg():
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.gmtime("not-a-number") # type: ignore[arg-type]
def test_time_gmtime_too_many_args():
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.gmtime(EPOCH, EPOCH) # type: ignore[call-arg]
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_localtime_invalid_arg():
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.localtime("not-a-number") # type: ignore[arg-type]
def test_time_localtime_too_many_args():
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.localtime(EPOCH, EPOCH) # type: ignore[call-arg]
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_strftime_no_args():
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.strftime() # type: ignore[call-arg]
def test_time_strftime_too_many_args():
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.strftime("%Y", time.localtime(EPOCH), "extra") # type: ignore[call-arg]
def test_time_strftime_invalid_tuple():
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.strftime("%Y", ("not", "a", "valid", "tuple")) # type: ignore[arg-type]
def test_time_strftime_none():
# Unlike gmtime() and localtime(), strftime() does not accept None.
with time_machine.travel(EPOCH), pytest.raises(TypeError):
time.strftime("%Y", None) # type: ignore[arg-type]
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)
def test_time_time_ns_distant_destination_exact():
destination = dt.datetime(2500, 1, 1, microsecond=1, tzinfo=dt.timezone.utc)
with time_machine.travel(destination, tick=False):
assert time.time_ns() == 16_725_225_600_000_001_000
# 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(4.1, tick=False)
def test_destination_float_rounded():
# Truncating 4.1 * 10**9 would give 4_099_999_999
assert time.time_ns() == 4_100_000_000
@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_none():
now = time.time()
with time_machine.travel(None, tick=False):
assert now <= time.time() <= now + 1.0
def test_destination_none_nested():
with (
time_machine.travel(EPOCH, tick=False),
time_machine.travel(None, tick=False),
):
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
def test_destination_string_no_dateutil_unparsable():
with (
mock.patch.object(time_machine, "HAVE_DATEUTIL", new=False),
pytest.raises(ValueError) as excinfo,
time_machine.travel("the future"),
):
pass # pragma: no cover
assert excinfo.value.args == ("Invalid isoformat string: 'the future'",)
@pytest.mark.parametrize(
("string", "expected_time"),
[
("1970-01-01 00:00:00", EPOCH),
("1970-01-01 00:01:01", EPOCH + 61.0),
("1970-01-01", EPOCH),
],
)
def test_destination_string_no_dateutil(string, expected_time):
with (
mock.patch.object(time_machine, "HAVE_DATEUTIL", new=False),
time_machine.travel(string),
):
assert time.time() == expected_time
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="datetime.fromisoformat extended formats added in 3.11",
)
@pytest.mark.parametrize(
("string", "expected_time"),
[
("19700101T000102", EPOCH + 62.0),
("1970-01-01T00:00:05+01:00", EPOCH - 3600.0 + 5),
],
)
def test_destination_string_no_dateutil_extended_formats(string, expected_time):
with (
mock.patch.object(time_machine, "HAVE_DATEUTIL", new=False),
time_machine.travel(string),
):
assert time.time() == expected_time
def test_destination_string_unparsable_dateutil():
with (
pytest.raises(ValueError) as excinfo,
time_machine.travel("the future"),
):
pass # pragma: no cover
assert excinfo.value.args == ("Unknown string format: %s", "the future")
@pytest.mark.parametrize(
("string", "expected_time"),
[
("1st January 1970 00:03:03", EPOCH + 183.0),
("1st Jan 1970", EPOCH),
],
)
def test_destination_string_dateutil(string, expected_time):
with time_machine.travel(string):
assert time.time() == expected_time
@pytest.mark.parametrize(
["local_tz", "expected_offset"],
[
("UTC", 0),
("Europe/Amsterdam", -3600),
("America/New_York", 5 * 3600),
],
)
@pytest.mark.parametrize("destination", ["1970-01-01 00:00", "1970-01-01"])
def test_destination_string_naive(local_tz, expected_offset, destination):
# MIXED mode (default) - strings are interpreted as local time
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
def test_class_decorator_setUpClass_base_exception():
# pytest's skip outcome, for example, does not derive from Exception.
class Interrupt(BaseException):
pass
@time_machine.travel(EPOCH)
class InterruptTests(TestCase):
@classmethod
def setUpClass(cls) -> None:
raise Interrupt()
def test_something(self) -> None: # pragma: no cover
pass
with pytest.raises(Interrupt):
InterruptTests.setUpClass()
assert not time_machine.escape_hatch.is_travelling()
assert time.time() >= LIBRARY_EPOCH
def test_class_decorator_setUpClass_pytest_skip(testdir):
testdir.makepyfile(
"""
import time
from unittest import TestCase
import pytest
import time_machine
@time_machine.travel(0.0)
class SkippedTests(TestCase):
@classmethod
def setUpClass(cls):
pytest.skip("Not today")
def test_never_runs(self):
pass
def test_after():
assert not time_machine.escape_hatch.is_travelling()
assert time.time() > 10.0
"""
)
result = testdir.runpytest("-v", "-s", "-p", "no:randomly")
result.assert_outcomes(passed=1, skipped=1)
def test_class_decorator_tearDownClass_error():
@time_machine.travel(EPOCH)
class ErrorTests(TestCase):
@classmethod
def tearDownClass(cls) -> None:
raise ValueError("Broken")
def test_something(self) -> None:
assert EPOCH <= time.time() < EPOCH + 10.0
result = unittest.TestResult()
unittest.defaultTestLoader.loadTestsFromTestCase(ErrorTests).run(result)
assert result.testsRun == 1
assert result.failures == []
assert len(result.errors) == 1
assert "ValueError: Broken" in result.errors[0][1]
assert not time_machine.escape_hatch.is_travelling()
assert time.time() >= LIBRARY_EPOCH
# extract_timestamp_tzname() tests
def test_extract_timestamp_tzname_int():
assert time_machine.extract_timestamp_tzname(2) == (
2 * NANOSECONDS_PER_SECOND,
None,
)
def test_extract_timestamp_tzname_float_rounded():
# Truncating 4.1 * 10**9 would give 4_099_999_999
assert time_machine.extract_timestamp_tzname(4.1) == (4_100_000_000, None)
def test_extract_timestamp_tzname_datetime_utc():
destination = dt.datetime(2500, 1, 1, microsecond=1, tzinfo=dt.timezone.utc)
assert time_machine.extract_timestamp_tzname(destination) == (
16_725_225_600_000_001_000,
"UTC",
)
def test_extract_timestamp_tzname_datetime_zoneinfo():
destination = dt.datetime(2020, 4, 29, 12, tzinfo=ZoneInfo("Africa/Addis_Ababa"))
assert time_machine.extract_timestamp_tzname(destination) == (
int(destination.timestamp()) * NANOSECONDS_PER_SECOND,
"Africa/Addis_Ababa",
)
def test_extract_timestamp_tzname_date():
assert time_machine.extract_timestamp_tzname(dt.date(1970, 1, 2)) == (
86_400 * NANOSECONDS_PER_SECOND,
None,
)
def test_extract_timestamp_tzname_timedelta():
delta_ns = 3_600 * NANOSECONDS_PER_SECOND
before_ns = time.time_ns()
timestamp_ns, tzname = time_machine.extract_timestamp_tzname(
dt.timedelta(seconds=3_600)
)
after_ns = time.time_ns()
assert tzname is None
assert before_ns + delta_ns <= timestamp_ns <= after_ns + delta_ns
def test_extract_timestamp_tzname_string():
assert time_machine.extract_timestamp_tzname("1970-01-01 00:02 +0000") == (
120 * NANOSECONDS_PER_SECOND,
None,
)
# 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_timedelta_distant_exact():
# timedelta.total_seconds() cannot represent this delta exactly.
with time_machine.travel(EPOCH, tick=False) as traveller:
traveller.shift(dt.timedelta(days=150_000, microseconds=1))
assert time.time_ns() == 12_960_000_000_000_001_000
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_float_delta_rounded():
# Truncating 4.1 * 10**9 would give 4_099_999_999
with time_machine.travel(EPOCH, tick=False) as traveller:
traveller.shift(4.1)
assert time.time_ns() == 4_100_000_000
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_unsupported_destination_keeps_timezone():
orig_tzname = time.tzname
dest = EPOCH_DATETIME.replace(tzinfo=ZoneInfo("Africa/Addis_Ababa"))
with time_machine.travel(dest, tick=False) as traveller:
assert time.tzname == ("EAT", "EAT")
with pytest.raises(TypeError):
traveller.move_to(object()) # type: ignore[arg-type]
assert time.time() == dest.timestamp()
assert time.tzname == ("EAT", "EAT")
assert time.tzname == orig_tzname
def test_move_to_naive_mode_error_keeps_timezone():
orig_tzname = time.tzname
dest = EPOCH_DATETIME.replace(tzinfo=ZoneInfo("Africa/Addis_Ababa"))
with time_machine.travel(dest, tick=False) as traveller:
assert time.tzname == ("EAT", "EAT")
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.ERROR),
pytest.raises(RuntimeError),
):
traveller.move_to(dt.datetime(1971, 1, 1))
assert time.time() == dest.timestamp()
assert time.tzname == ("EAT", "EAT")
assert time.tzname == orig_tzname
def test_move_to_naive_string_uses_real_local_timezone():
# Naive destinations resolve against the machine’s real timezone (UTC for
# the test suite), not that of the traveller being moved from. move_to()
# therefore has to unmock the timezone before extracting the destination.
dest = EPOCH_DATETIME.replace(tzinfo=ZoneInfo("Africa/Addis_Ababa"))
with time_machine.travel(dest, tick=False) as traveller:
assert time.tzname == ("EAT", "EAT")
traveller.move_to("1971-01-01 00:00:00", tick=False)
assert time.time() == EPOCH_PLUS_ONE_YEAR
def test_move_to_naive_mode_local_uses_real_local_timezone():
with (
change_local_timezone("America/Chicago"),
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.LOCAL),
):
expected = dt.datetime(1971, 1, 1).timestamp()
dest = EPOCH_DATETIME.replace(tzinfo=ZoneInfo("Africa/Addis_Ababa"))
with time_machine.travel(dest, tick=False) as traveller:
assert time.tzname == ("EAT", "EAT")
traveller.move_to(dt.datetime(1971, 1, 1), tick=False)
assert time.time() == expected
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)
time_from_uuid6 = time_from_uuid1
def time_from_uuid7(value: uuid.UUID) -> dt.datetime:
return dt.datetime(1970, 1, 1) + dt.timedelta(milliseconds=value.time)
uuid_generators = [pytest.param(uuid.uuid1, time_from_uuid1, id="uuid1")]
if sys.version_info >= (3, 14):
uuid_generators.append(pytest.param(uuid.uuid6, time_from_uuid6, id="uuid6"))
uuid_generators.append(pytest.param(uuid.uuid7, time_from_uuid7, id="uuid7"))
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
@pytest.mark.parametrize(("generate", "time_from"), uuid_generators)
def test_uuid_travel_backwards(generate, time_from):
future = dt.datetime(2056, 2, 6, 14, 3, 21)
past = dt.datetime(1978, 7, 6, 23, 6, 31)
with time_machine.travel(future, tick=False):
assert time_from(generate()) == future
with time_machine.travel(past, tick=False):
assert time_from(generate()) == past
@pytest.mark.parametrize(("generate", "time_from"), uuid_generators)
def test_uuid_travel_backwards_nested(generate, time_from):
future = dt.datetime(2056, 2, 6, 14, 3, 21)
past = dt.datetime(1978, 7, 6, 23, 6, 31)
with time_machine.travel(EPOCH_PLUS_ONE_YEAR_DATETIME, tick=False):
with time_machine.travel(future, tick=False):
assert time_from(generate()) == future
assert time_from(generate()) == EPOCH_PLUS_ONE_YEAR_DATETIME.replace(
tzinfo=None
)
with time_machine.travel(past, tick=False):
assert time_from(generate()) == past
@pytest.mark.parametrize(("generate", "time_from"), uuid_generators)
def test_uuid_shift_backwards(generate, time_from):
destination = dt.datetime(2056, 2, 6, 14, 3, 21)
with time_machine.travel(destination, tick=False) as traveller:
assert time_from(generate()) == destination
traveller.shift(-dt.timedelta(days=1))
assert time_from(generate()) == destination - dt.timedelta(days=1)
@pytest.mark.parametrize(("generate", "time_from"), uuid_generators)
def test_uuid_move_to_backwards(generate, time_from):
future = dt.datetime(2056, 2, 6, 14, 3, 21)
past = dt.datetime(1978, 7, 6, 23, 6, 31)
with time_machine.travel(future, tick=False) as traveller:
assert time_from(generate()) == future
traveller.move_to(past)
assert time_from(generate()) == past
@pytest.mark.parametrize(("generate", "time_from"), uuid_generators)
def test_uuid_after_travel(generate, time_from):
"""
Travelling to the future should not leave the uuid module stuck there
after time travel has stopped.
"""
with time_machine.travel(dt.datetime(2056, 2, 6, 14, 3, 21), tick=False):
generate()
now = dt.datetime.now(dt.timezone.utc).replace(tzinfo=None)
assert abs(time_from(generate()) - now) < dt.timedelta(seconds=10)
@pytest.mark.parametrize(("generate", "time_from"), uuid_generators)
def test_uuid_monotonic_within_travel(generate, time_from):
"""
Resetting the cached timestamps should not break the ordering guarantee
for values generated at a single point in time.
"""
destination = dt.datetime(2056, 2, 6, 14, 3, 21)
with time_machine.travel(destination, tick=False):
first = generate()
second = generate()
assert time_from(first) == destination
assert first < second
# subinterpreter tests
@py_have_subinterpreters
def test_subinterpreters():
"""
Works in (isolated) subinterpreters, which each import _time_machine
separately, including after destroying the interpreter that first
imported it. Run in a subprocess so that the first import happens within
a subinterpreter.
"""
code = dedent(
"""
import sys
SUB_CODE = '''
import datetime as dt
import time
import time_machine
with time_machine.travel(0.0, tick=False):
assert time.time() == 0.0
assert time.time_ns() == 0
assert time.gmtime().tm_year == 1970
assert dt.datetime.now(dt.timezone.utc) == dt.datetime(
1970, 1, 1, tzinfo=dt.timezone.utc
)
'''
if sys.version_info >= (3, 14):
from concurrent import interpreters
for _ in range(2):
interp = interpreters.create()
try:
interp.exec(SUB_CODE)
finally:
interp.close()
else:
import _interpreters
for _ in range(2):
interp_id = _interpreters.create()
try:
excinfo = _interpreters.exec(interp_id, SUB_CODE)
if excinfo is not None:
raise RuntimeError(excinfo.formatted)
finally:
_interpreters.destroy(interp_id)
print("OK")
"""
)
result = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
assert result.stdout == "OK\n"
@py_have_subinterpreters
def test_main_interpreter_sees_real_time_while_subinterpreter_travels():
"""
Starting travel in a subinterpreter does not affect the main
interpreter, which continues to see the real time while the
subinterpreter is travelling, before the subinterpreter is destroyed.
"""
code = dedent(
f"""
import sys
import time
SUB_CODE = '''
import time
import time_machine
with time_machine.travel(0.0, tick=False):
assert time.time() == 0.0, time.time()
'''
assert time.time() >= {LIBRARY_EPOCH}, time.time()
if sys.version_info >= (3, 14):
from concurrent import interpreters
interp = interpreters.create()
interp.exec(SUB_CODE)
assert time.time() >= {LIBRARY_EPOCH}, time.time()
interp.close()
else:
import _interpreters
interp_id = _interpreters.create()
excinfo = _interpreters.exec(interp_id, SUB_CODE)
if excinfo is not None:
raise RuntimeError(excinfo.formatted)
assert time.time() >= {LIBRARY_EPOCH}, time.time()
_interpreters.destroy(interp_id)
print("OK")
"""
)
result = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
assert result.stdout == "OK\n"
@py_have_subinterpreters
def test_subinterpreters_not_travelling_see_real_time():
"""
Patching applies process-wide, but interpreters that are not travelling
fall back to the original functions and see the real time, whether or not
they have imported time_machine.
"""
code = dedent(
f"""
import sys
import time
import time_machine
SUB_CODE = '''
import datetime as dt
import time
assert time.time() >= {LIBRARY_EPOCH}, time.time()
assert dt.datetime.now().year >= 2020
assert dt.date.today().year >= 2020
import time_machine
assert time.time() >= {LIBRARY_EPOCH}, time.time()
'''
with time_machine.travel(0.0, tick=False):
assert time.time() == 0.0
if sys.version_info >= (3, 14):
from concurrent import interpreters
interp = interpreters.create()
try:
interp.exec(SUB_CODE)
finally:
interp.close()
else:
import _interpreters
interp_id = _interpreters.create()
try:
excinfo = _interpreters.exec(interp_id, SUB_CODE)
if excinfo is not None:
raise RuntimeError(excinfo.formatted)
finally:
_interpreters.destroy(interp_id)
assert time.time() == 0.0
print("OK")
"""
)
result = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
assert result.stdout == "OK\n"
# 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"
@py_have_clock_gettime
def test_c_extension_init_without_clock_realtime():
# time.CLOCK_REALTIME is not always available, e.g. on builds against old
# macOS = official Python.org installer. The C extension checks for it at
# import time; without it, time.clock_gettime() passes through to the real
# clock even while time travelling.
code = dedent(
"""\
import time
clock_realtime = time.CLOCK_REALTIME
del time.CLOCK_REALTIME
import time_machine
with time_machine.travel(0.0, tick=False):
assert time.time() == 0.0
assert time.clock_gettime(clock_realtime) > 0.0
print("ok")
"""
)
result = subprocess.run(
[sys.executable, "-c", code],
check=True,
stdout=subprocess.PIPE,
text=True,
)
assert result.stdout == "ok\n"
@pytest.mark.parametrize(
"bad_value",
[
pytest.param(None, id="import blocked"),
pytest.param((), id="broken module"),
],
)
def test_fallback_time_machine_unavailable(bad_value):
"""
If the time_machine module is unavailable or broken in the calling
interpreter, patched functions fall back to the original functions and
return the real time.
"""
with time_machine.travel(EPOCH):
with mock.patch.dict(sys.modules, {"time_machine": bad_value}):
assert time.time() >= LIBRARY_EPOCH
assert time.time_ns() >= int(LIBRARY_EPOCH * NANOSECONDS_PER_SECOND)
assert time.gmtime().tm_year >= 2020
assert time.localtime().tm_year >= 2020
assert int(time.strftime("%Y")) >= 2020
assert dt.datetime.now().year >= 2020
assert dt.datetime.now(dt.timezone.utc).year >= 2020
assert dt.datetime.utcnow().year >= 2020
assert dt.date.today().year >= 2020
# Still travelling once time_machine is reachable again.
assert time.time() == pytest.approx(EPOCH, abs=10.0)
@py_have_clock_gettime
@pytest.mark.parametrize(
"bad_value",
[
pytest.param(None, id="import blocked"),
pytest.param((), id="broken module"),
],
)
def test_fallback_time_machine_unavailable_clock_gettime(bad_value):
"""
If the time_machine module is unavailable or broken in the calling
interpreter, patched clock_gettime* functions fall back to the original
functions and return the real time.
"""
with time_machine.travel(EPOCH):
with mock.patch.dict(sys.modules, {"time_machine": bad_value}):
assert time.clock_gettime(time.CLOCK_REALTIME) >= LIBRARY_EPOCH
assert time.clock_gettime_ns(time.CLOCK_REALTIME) >= int(
LIBRARY_EPOCH * NANOSECONDS_PER_SECOND
)
# Still travelling once time_machine is reachable again.
assert time.time() == pytest.approx(EPOCH, abs=10.0)
# freeezegun conflict tests
def test_errors_if_freezegun_active_first():
with (
freezegun.freeze_time("2000-01-01"),
pytest.raises(RuntimeError) as excinfo,
time_machine.travel(EPOCH),
):
pass # pragma: no cover
assert excinfo.value.args == (
"time-machine cannot start when freezegun is active.",
)
# naive_mode tests
def test_naive_mode_default_is_mixed():
# MIXED for backwards compatibility
assert time_machine.naive_mode == time_machine.NaiveMode.MIXED
def test_naive_mode_mixed_datetime():
naive_dt = dt.datetime(1970, 1, 1, 0, 2, 0)
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.MIXED),
time_machine.travel(naive_dt),
):
assert time.time() == EPOCH + 120.0
def test_naive_mode_mixed_date():
date = dt.date(1970, 1, 1)
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.MIXED),
time_machine.travel(date),
):
assert time.time() == EPOCH
@pytest.mark.parametrize(
"local_tz", ["UTC", "America/Chicago", "Europe/London", "Asia/Tokyo"]
)
def test_naive_mode_mixed_string(local_tz):
naive_string = "1970-01-01 00:00:00"
with change_local_timezone(local_tz):
expected_timestamp = dt.datetime(1970, 1, 1, 0, 0, 0).timestamp()
with (
change_local_timezone(local_tz),
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.MIXED),
time_machine.travel(naive_string),
):
assert time.time() == expected_timestamp
def test_naive_mode_utc_datetime():
naive_dt = dt.datetime(1970, 1, 1, 0, 2, 0)
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.UTC),
time_machine.travel(naive_dt),
):
assert time.time() == EPOCH + 120.0
def test_naive_mode_utc_date():
date = dt.date(1970, 1, 1)
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.UTC),
time_machine.travel(date),
):
assert time.time() == EPOCH
@pytest.mark.parametrize(
"local_tz", ["UTC", "America/Chicago", "Europe/London", "Asia/Tokyo"]
)
def test_naive_mode_local_datetime(local_tz):
naive_dt = dt.datetime(1970, 1, 1, 0, 0, 0)
with change_local_timezone(local_tz):
expected_timestamp = naive_dt.timestamp()
with (
change_local_timezone(local_tz),
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.LOCAL),
time_machine.travel(naive_dt),
):
assert time.time() == expected_timestamp
@pytest.mark.parametrize(
"local_tz", ["UTC", "America/Chicago", "Europe/London", "Asia/Tokyo"]
)
def test_naive_mode_local_date(local_tz):
date = dt.date(1970, 1, 1)
with change_local_timezone(local_tz):
expected_timestamp = dt.datetime.combine(date, dt.time()).timestamp()
with (
change_local_timezone(local_tz),
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.LOCAL),
time_machine.travel(date),
):
assert time.time() == expected_timestamp
def test_naive_mode_error_datetime():
naive_dt = dt.datetime(1970, 1, 1, 0, 2, 0)
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.ERROR),
pytest.raises(
RuntimeError,
match="Naive datetime provided while time_machine.naive_mode is set to ERROR",
),
):
time_machine.travel(naive_dt).start()
def test_naive_mode_error_date():
date = dt.date(1970, 1, 1)
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.ERROR),
pytest.raises(
RuntimeError,
match="date object provided while time_machine.naive_mode is set to ERROR",
),
):
time_machine.travel(date).start()
def test_naive_mode_error_aware_datetime_works():
aware_dt = dt.datetime(1970, 1, 1, 0, 2, 0, tzinfo=dt.timezone.utc)
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.ERROR),
time_machine.travel(aware_dt),
):
assert time.time() == EPOCH + 120.0
def test_naive_mode_utc_string():
naive_string = "1970-01-01 00:02:00"
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.UTC),
time_machine.travel(naive_string),
):
assert time.time() == EPOCH + 120.0
@pytest.mark.parametrize(
"local_tz", ["UTC", "America/Chicago", "Europe/London", "Asia/Tokyo"]
)
def test_naive_mode_local_string(local_tz):
naive_string = "1970-01-01 00:00:00"
# Calculate what timestamp we expect by using Python's normal behavior
with change_local_timezone(local_tz):
expected_timestamp = dt.datetime(1970, 1, 1, 0, 0, 0).timestamp()
with (
change_local_timezone(local_tz),
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.LOCAL),
time_machine.travel(naive_string),
):
assert time.time() == expected_timestamp
def test_naive_mode_error_string():
naive_string = "1970-01-01 00:02:00"
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.ERROR),
pytest.raises(
RuntimeError,
match="Naive datetime string provided while time_machine.naive_mode is set to ERROR",
),
):
time_machine.travel(naive_string).start()
def test_naive_mode_error_string_aware_works():
aware_string = "1970-01-01 00:02:00+00:00"
with (
mock.patch.object(time_machine, "naive_mode", time_machine.NaiveMode.ERROR),
time_machine.travel(aware_string),
):
assert time.time() == EPOCH + 120.0
# pytest plugin tests
def test_fixture_class_available_without_pytest():
# TimeMachineFixture should be importable even when pytest is not
# installed.
code = dedent(
"""\
import sys
sys.modules["pytest"] = None # block import
import time_machine
assert not time_machine.HAVE_PYTEST
assert time_machine.TimeMachineFixture
print("ok")
"""
)
result = subprocess.run(
[sys.executable, "-c", code],
check=True,
stdout=subprocess.PIPE,
text=True,
)
assert result.stdout == "ok\n"
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_fixture_start_error():
fixture = time_machine.TimeMachineFixture()
with (
mock.patch.object(
time_machine.travel, "start", side_effect=ValueError("Broken")
),
pytest.raises(ValueError),
):
fixture.move_to(EPOCH)
assert fixture.traveller is None
fixture.stop() # nothing to stop
assert not time_machine.escape_hatch.is_travelling()
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_function_arg(testdir):
testdir.makepyfile(
"""
import pytest
import time
@pytest.mark.time_machine(0, tick=False)
def test():
for _ in range(10):
assert time.time() == 0.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_date_today(self):
real_today = dt.date.today()
with time_machine.travel(EPOCH):
eh_today = time_machine.escape_hatch.datetime.date.today()
assert eh_today >= real_today
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.datetime.date.today()
assert excinfo.value.args == ("Not currently time-travelling.",)
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
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.datetime.datetime.now()
assert excinfo.value.args == ("Not currently time-travelling.",)
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_today(self):
real_today = dt.datetime.today()
with time_machine.travel(EPOCH):
eh_today = time_machine.escape_hatch.datetime.datetime.today()
assert eh_today >= real_today
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.datetime.datetime.today()
assert excinfo.value.args == ("Not currently time-travelling.",)
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
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.datetime.datetime.utcnow()
assert excinfo.value.args == ("Not currently time-travelling.",)
@py_utcnow_deprecated
def test_datetime_utcnow_deprecation_warning(self):
with pytest.warns(DeprecationWarning) as unpatched_records:
dt.datetime.utcnow() # warns here
assert_warned_here(unpatched_records[0])
with time_machine.travel(EPOCH), pytest.warns(DeprecationWarning) as records:
time_machine.escape_hatch.datetime.datetime.utcnow() # warns here
assert len(records) == 1
assert str(records[0].message) == str(unpatched_records[0].message)
assert_warned_here(records[0])
@py_utcnow_not_deprecated
def test_datetime_utcnow_no_deprecation_warning(self):
with time_machine.travel(EPOCH), warnings.catch_warnings():
warnings.simplefilter("error")
time_machine.escape_hatch.datetime.datetime.utcnow()
@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
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.time.clock_gettime(time.CLOCK_REALTIME)
assert excinfo.value.args == ("Not currently time-travelling.",)
@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
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.time.clock_gettime_ns(time.CLOCK_REALTIME)
assert excinfo.value.args == ("Not currently time-travelling.",)
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
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.time.gmtime()
assert excinfo.value.args == ("Not currently time-travelling.",)
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
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.time.localtime()
assert excinfo.value.args == ("Not currently time-travelling.",)
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
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.time.strftime("%Y-%m-%d")
assert excinfo.value.args == ("Not currently time-travelling.",)
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
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.time.time()
assert excinfo.value.args == ("Not currently time-travelling.",)
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
with pytest.raises(ValueError) as excinfo:
time_machine.escape_hatch.time.time_ns()
assert excinfo.value.args == ("Not currently time-travelling.",)
adamchainz-time-machine-18d4852/tox.ini 0000664 0000000 0000000 00000001103 15250103717 0017737 0 ustar 00root root 0000000 0000000 [tox]
requires =
tox>=4.2
env_list =
py{315, 314, 313, 312, 311, 310, 315t, 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 \
-W 'ignore:Module globals; __loader__ != __spec__.loader' \
-m coverage run \
-m pytest {posargs:tests}
dependency_groups =
test
[testenv:py313t]
dependency_groups =
test-base
adamchainz-time-machine-18d4852/uv.lock 0000664 0000000 0000000 00000612777 15250103717 0017761 0 ustar 00root root 0000000 0000000 version = 1
revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version >= '3.12' and python_full_version < '3.14'",
"python_full_version == '3.11.*'",
"python_full_version < '3.11'",
]
[[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 = "1.0.0"
source = { registry = "https://pypi.org/simple" }
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.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
]
[[package]]
name = "bashlex"
version = "0.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/60/aae0bb54f9af5e0128ba90eb83d8d0d506ee8f0475c4fdda3deeda20b1d2/bashlex-0.18.tar.gz", hash = "sha256:5bb03a01c6d5676338c36fd1028009c8ad07e7d61d8a1ce3f513b7fff52796ee", size = 68742, upload-time = "2023-01-18T15:21:26.402Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/be/6985abb1011fda8a523cfe21ed9629e397d6e06fb5bae99750402b25c95b/bashlex-0.18-py2.py3-none-any.whl", hash = "sha256:91d73a23a3e51711919c1c899083890cdecffc91d8c088942725ac13e9dcfffa", size = 69539, upload-time = "2023-01-18T15:21:24.167Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
]
[[package]]
name = "bracex"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ac/01/5f394b8bcd6e5b92f73130990960423bbb19711f906bd9fe9ea5557c667c/bracex-3.0.1.tar.gz", hash = "sha256:4e38e32392e4a4780fe15d644bfc7c8514057cfc3861e060b11814ce829c25e4", size = 44019, upload-time = "2026-07-20T13:43:00.335Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/8f/6f7273a7adb8d73fc8d21ede4376a3e475e52f98435c6007f69100dec8ca/bracex-3.0.1-py3-none-any.whl", hash = "sha256:6523ad83aeb5098a4ee597cff0f964442ff74e460bd3fafaffab6a013ff2288c", size = 11940, upload-time = "2026-07-20T13:42:59.268Z" },
]
[[package]]
name = "build"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "os_name == 'nt'" },
{ name = "packaging" },
{ name = "pyproject-hooks" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" },
]
[[package]]
name = "certifi"
version = "2026.7.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" },
{ url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" },
{ url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" },
{ url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" },
{ url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" },
{ url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" },
{ url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" },
{ url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" },
{ url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" },
{ url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" },
{ url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" },
{ url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" },
{ url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" },
{ url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" },
{ url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" },
{ url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" },
{ url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" },
{ url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" },
{ url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" },
{ url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" },
{ url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" },
{ url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" },
{ url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" },
{ url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" },
{ url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" },
{ url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" },
{ url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" },
{ url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" },
{ url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" },
{ url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" },
{ url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" },
{ url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" },
{ url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" },
{ url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" },
{ url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" },
{ url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" },
{ url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" },
{ url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" },
{ url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" },
{ url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" },
{ url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" },
{ url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" },
{ url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" },
{ url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" },
{ url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" },
{ url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" },
{ url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" },
{ url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" },
{ url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" },
{ url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" },
{ url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" },
{ url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" },
{ url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" },
{ url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" },
{ url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" },
{ url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" },
{ url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" },
{ url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" },
{ url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" },
{ url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" },
{ url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" },
{ url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" },
{ url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" },
{ url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" },
{ url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" },
{ url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" },
{ url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" },
{ url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" },
{ url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" },
{ url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" },
{ url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" },
{ url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" },
{ url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" },
{ url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" },
{ url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" },
{ url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" },
{ url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" },
{ url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" },
{ url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" },
{ url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" },
{ url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" },
{ url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" },
{ url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" },
{ url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" },
{ url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" },
{ url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" },
{ url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" },
{ url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" },
{ url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" },
{ url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" },
{ url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" },
{ url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" },
{ url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" },
{ url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" },
{ url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" },
{ url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" },
{ url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" },
{ url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" },
{ url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" },
{ url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" },
{ url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" },
{ url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" },
{ url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" },
{ url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" },
{ url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" },
{ url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" },
{ url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" },
{ url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" },
{ url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" },
{ url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" },
{ url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" },
{ url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" },
{ url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" },
{ url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" },
{ url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" },
{ url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" },
{ url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" },
{ url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" },
{ url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" },
{ url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" },
{ url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" },
{ url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" },
{ url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" },
{ url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" },
{ url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" },
{ url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" },
{ url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" },
{ url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" },
{ url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" },
{ url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" },
{ url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" },
{ url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" },
{ url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" },
{ url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" },
{ url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" },
{ url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" },
{ url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" },
{ url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" },
{ url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" },
{ url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" },
{ url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" },
]
[[package]]
name = "cibuildwheel"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bashlex" },
{ name = "bracex" },
{ name = "build" },
{ name = "certifi" },
{ name = "dependency-groups" },
{ name = "filelock" },
{ name = "humanize" },
{ name = "packaging" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/b5/68696cc0023dcde3dc5f9f94f7aa3d319fe8f47ad80ba12224f90c837fcb/cibuildwheel-4.2.0.tar.gz", hash = "sha256:a018495ace49e84422ef89c01891439ee0fd00cebd3758b9a879415ff4892d97", size = 455959, upload-time = "2026-08-05T02:25:04.579Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/ef/217ba042d73ac2d7500794993069cde78551f538a75c67626929a8587f80/cibuildwheel-4.2.0-py3-none-any.whl", hash = "sha256:4c7d6fc52cf5422c944aed86bc531f15e2279b91ff2c5e9e6fed9d6e3eefc635", size = 167818, upload-time = "2026-08-05T02:25:02.833Z" },
]
[[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.15.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/70/b052a519a584663a7bd052841a2debe11c8309ec49a7786340003f9c0a02/coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264", size = 222245, upload-time = "2026-08-06T13:46:55.253Z" },
{ url = "https://files.pythonhosted.org/packages/67/39/892fa511aba3d1c3c8f49509a0ff5c71eab9f9f88d08e1a38da395821660/coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d", size = 222762, upload-time = "2026-08-06T13:46:57.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/95/b2c724ce1e64bc23cb5b1d7eeffa9548dc3d811f7a6297b2d01607f4e062/coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150", size = 249498, upload-time = "2026-08-06T13:46:59.012Z" },
{ url = "https://files.pythonhosted.org/packages/0b/4f/b1973f67a1382af65b572a31ed692f8e490a6ad707191eab59148376832a/coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15", size = 251328, upload-time = "2026-08-06T13:47:00.764Z" },
{ url = "https://files.pythonhosted.org/packages/a2/09/03efa6722a132abcac91b32a60b64b240dd707c189c64eee697e48992c96/coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0", size = 253194, upload-time = "2026-08-06T13:47:01.976Z" },
{ url = "https://files.pythonhosted.org/packages/45/63/8299201d9c80fb65551ce99c966cab83d706ec4066ac999bef08201346de/coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d", size = 255106, upload-time = "2026-08-06T13:47:03.281Z" },
{ url = "https://files.pythonhosted.org/packages/ee/16/26fd8a691eb8d9a230128685f6d23309d7402cb030aa553001788c8c50fc/coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4", size = 250177, upload-time = "2026-08-06T13:47:04.713Z" },
{ url = "https://files.pythonhosted.org/packages/ad/ef/3c7556f33783a0a566e01443ca62bd8eb2cdfe22d271efdc02e08beb5654/coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54", size = 251234, upload-time = "2026-08-06T13:47:06.104Z" },
{ url = "https://files.pythonhosted.org/packages/29/49/640a34043edac950738f36a3567832db5731d4cb2ed84b59cdb89c6bccbf/coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20", size = 249237, upload-time = "2026-08-06T13:47:07.467Z" },
{ url = "https://files.pythonhosted.org/packages/48/f5/e80f212669dd1be954ff844f883ef11a437ef4fd0089c6e0effc7b66b15d/coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325", size = 253050, upload-time = "2026-08-06T13:47:08.748Z" },
{ url = "https://files.pythonhosted.org/packages/c7/e9/e5da0fe39f7fde1bca9edc09c60921bb5fdba4cec7db5bbad41ddfd8c230/coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88", size = 249508, upload-time = "2026-08-06T13:47:10.072Z" },
{ url = "https://files.pythonhosted.org/packages/7d/38/41bf25774a0c8bba6b467f917cb1c9a0a2605e02dc93aad489fc7050ed59/coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04", size = 250110, upload-time = "2026-08-06T13:47:11.35Z" },
{ url = "https://files.pythonhosted.org/packages/89/6e/26f2e54b79acc29d179ee4272922625aedb69198c4eb61f7ff4f098f3c78/coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338", size = 224294, upload-time = "2026-08-06T13:47:12.753Z" },
{ url = "https://files.pythonhosted.org/packages/7b/06/9a318fc3ae040d4d6cb2d86101c6aa963fab20899a5c58666adf52cde0ca/coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360", size = 224919, upload-time = "2026-08-06T13:47:14.17Z" },
{ url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" },
{ url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" },
{ url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" },
{ url = "https://files.pythonhosted.org/packages/fb/65/ec03b743a2a229c72cc1eff3e57be9d3564e9c6b4d5aba2d70744a3fc0d8/coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6", size = 255199, upload-time = "2026-08-06T13:47:19.765Z" },
{ url = "https://files.pythonhosted.org/packages/41/4b/5163729e4b6582d61975cfd3ccab45b4ec53e21cf156d9941cb025188468/coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d", size = 257308, upload-time = "2026-08-06T13:47:21.206Z" },
{ url = "https://files.pythonhosted.org/packages/86/08/2167a0f08fb87d702fa423a48578a32865464b7c9e1db3911ad7812ab414/coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce", size = 259268, upload-time = "2026-08-06T13:47:22.503Z" },
{ url = "https://files.pythonhosted.org/packages/1e/e5/68eebae3053dbd48508edea559c21b23fbdf3460784f91370c83a86a6acd/coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7", size = 253392, upload-time = "2026-08-06T13:47:23.88Z" },
{ url = "https://files.pythonhosted.org/packages/1a/46/fd4ced40a2b691c774e515c9b69500bfa64c7960b67fcee4b2f6fad97fc3/coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b", size = 255001, upload-time = "2026-08-06T13:47:25.469Z" },
{ url = "https://files.pythonhosted.org/packages/53/25/ae2e5fa710bb6957a9aadeb9e3598d3b3e4af6587ce857ad42e8639a3f30/coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc", size = 253061, upload-time = "2026-08-06T13:47:26.845Z" },
{ url = "https://files.pythonhosted.org/packages/d7/31/67ddc0365db2c6e93ac8580bc4bbc50f65273262f973f63ebcdbc15c0495/coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571", size = 256831, upload-time = "2026-08-06T13:47:28.217Z" },
{ url = "https://files.pythonhosted.org/packages/f6/78/82b8fd18f57fb13f12d98fe874995bb2c4f9f17be8aff762c426323fdb96/coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719", size = 252781, upload-time = "2026-08-06T13:47:29.712Z" },
{ url = "https://files.pythonhosted.org/packages/0a/eb/6c74ef4dd12b252e573c49bdef9e2ac265bf3dbb79b8d7feb3266e084e9e/coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7", size = 253692, upload-time = "2026-08-06T13:47:31.192Z" },
{ url = "https://files.pythonhosted.org/packages/5a/66/eb9aed1c3fd2d36ee00eb173f434b14fa607fc056739c9a89ff4244010ea/coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e", size = 224461, upload-time = "2026-08-06T13:47:32.572Z" },
{ url = "https://files.pythonhosted.org/packages/e2/6d/81fa4161dfb3ed9d74e40d58647eff83a56b7612e78352581280fce2f477/coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc", size = 224937, upload-time = "2026-08-06T13:47:34.205Z" },
{ url = "https://files.pythonhosted.org/packages/5b/c1/d8dacf683c6cad3cf85ce68fd3774a6774ec402128822fdfaed920f11e6a/coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890", size = 224479, upload-time = "2026-08-06T13:47:36.118Z" },
{ url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" },
{ url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" },
{ url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" },
{ url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" },
{ url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" },
{ url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" },
{ url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" },
{ url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" },
{ url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" },
{ url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" },
{ url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" },
{ url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" },
{ url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" },
{ url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" },
{ url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" },
{ url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" },
{ url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" },
{ url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" },
{ url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" },
{ url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" },
{ url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" },
{ url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" },
{ url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" },
{ url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" },
{ url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" },
{ url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" },
{ url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" },
{ url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" },
{ url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" },
{ url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" },
{ url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" },
{ url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" },
{ url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" },
{ url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" },
{ url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" },
{ url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" },
{ url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" },
{ url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" },
{ url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" },
{ url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" },
{ url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" },
{ url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" },
{ url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" },
{ url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" },
{ url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" },
{ url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" },
{ url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" },
{ url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" },
{ url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" },
{ url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" },
{ url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" },
{ url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" },
{ url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" },
{ url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" },
{ url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" },
{ url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" },
{ url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" },
{ url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" },
{ url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" },
{ url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" },
{ url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" },
{ url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" },
{ url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" },
{ url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" },
{ url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" },
{ url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" },
{ url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" },
{ url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" },
{ url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" },
{ url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" },
{ url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" },
{ url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" },
{ url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" },
{ url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" },
{ url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" },
{ url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" },
{ url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" },
{ url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" },
{ url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" },
{ url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" },
{ url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" },
{ url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" },
{ url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "dependency-groups"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b5/16/65e61d8e837e0d70a99a0d4dd76e2af53357c3a03ed1db8ad9b5786d7f25/dependency_groups-1.3.2.tar.gz", hash = "sha256:c81831f43828dbc3987ee247eb198241a0152b8e7ceab10977cc3808eb388ac7", size = 13309, upload-time = "2026-08-22T02:43:30.137Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/e4/da5f2f176050efcfb9222a76a3e81c41f69f775be8e563b76ca4292a134f/dependency_groups-1.3.2-py3-none-any.whl", hash = "sha256:8c337f7b92445823b0d0ea507f611390c58dcd2112be00aebcd738a2ca7a1c64", size = 9802, upload-time = "2026-08-22T02:43:28.853Z" },
]
[[package]]
name = "docutils"
version = "0.21.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
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 = "docutils"
version = "0.22.4"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version >= '3.12' and python_full_version < '3.14'",
"python_full_version == '3.11.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "filelock"
version = "3.32.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/30/03b03951873a1a0ffc7e8ca0e10c15597b59e8d0e39260704cd2ea087bc4/filelock-3.32.4.tar.gz", hash = "sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30", size = 222126, upload-time = "2026-08-23T17:37:55.363Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/a4/9b63d595d748e3aff8812b65eacc1a2c4bd90b7c2012e08e72373b4835eb/filelock-3.32.4-py3-none-any.whl", hash = "sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd", size = 99864, upload-time = "2026-08-23T17:37:53.913Z" },
]
[[package]]
name = "freezegun"
version = "1.5.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" },
]
[[package]]
name = "furo"
version = "2025.12.19"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "accessible-pygments" },
{ name = "beautifulsoup4" },
{ name = "pygments" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "sphinx-basic-ng" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" },
]
[[package]]
name = "humanize"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0a/ea/13a1ef3c12d12662905801495283530251918b70d62d368f1d2e0272c70d/humanize-4.16.0.tar.gz", hash = "sha256:7dc2244a2f84a4bfb1d36c37bac80cd78e35cdc5c119206d87b018e1445f3a3f", size = 89515, upload-time = "2026-06-30T16:17:29.859Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/aa/0b7365d30fed43e7a3449aba1fe20a0a7174d9cf13e282af4e69ac825441/humanize-4.16.0-py3-none-any.whl", hash = "sha256:353eb2f34c09d098b2880eee8bef21832eae6d174f48c5762fff7e5fcb74d01d", size = 137209, upload-time = "2026-06-30T16:17:28.36Z" },
]
[[package]]
name = "hypothesis"
version = "6.165.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "sortedcontainers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/e2/0fad246d2b6330e1f78479bfc566b5c22be82aee8a865cde9a08f648487d/hypothesis-6.165.10.tar.gz", hash = "sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32", size = 503703, upload-time = "2026-08-16T22:56:15.404Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/c1/9a9538e6d185baf5cc7f15bc3b76e08efbb3de4b3c782f234356449c0dd7/hypothesis-6.165.10-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f", size = 783243, upload-time = "2026-08-16T22:55:44.058Z" },
{ url = "https://files.pythonhosted.org/packages/a1/30/b70d9d79e871a75cbdeccd9067f20ecdb9eb2a1dfa03c630be3ad13b8b30/hypothesis-6.165.10-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd", size = 778815, upload-time = "2026-08-16T22:55:46.948Z" },
{ url = "https://files.pythonhosted.org/packages/db/52/6f0a9b7aab24b0635e2238f3fbddea5b54b17879ac813df42a3cc3384c5c/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5", size = 1108009, upload-time = "2026-08-16T22:54:53.082Z" },
{ url = "https://files.pythonhosted.org/packages/f6/06/8d0d4e11ff02350d09ec9f9e90af354158e59e16a8907ba5199a4ff2d7e8/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1", size = 1136596, upload-time = "2026-08-16T22:54:54.443Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/01a1e440f2e38dc1ccf5d597af5b8a0bee5f21b674c99c123b5554de9690/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52", size = 1135234, upload-time = "2026-08-16T22:55:08.911Z" },
{ url = "https://files.pythonhosted.org/packages/7d/18/8a26c24d3d9db20265f39df341ab265858c094e209571e3179cf237935f4/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f", size = 1157528, upload-time = "2026-08-16T22:56:02.159Z" },
{ url = "https://files.pythonhosted.org/packages/ea/8e/ce3c829b1937402d7944420ca26a05a0c8563e894dcff03d34ffa279d306/hypothesis-6.165.10-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb", size = 1112870, upload-time = "2026-08-16T22:54:55.919Z" },
{ url = "https://files.pythonhosted.org/packages/f2/1b/4c4926d6c9a2b5d7cc090cc1e91219d6796102aa2a2c4b8f961c939e60b5/hypothesis-6.165.10-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb", size = 1149683, upload-time = "2026-08-16T22:55:30.567Z" },
{ url = "https://files.pythonhosted.org/packages/cb/f9/df24eb28412f82465e2b7707f0ff1ec274d580bce389d4d9156617dc7bba/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e", size = 1283402, upload-time = "2026-08-16T22:54:18.054Z" },
{ url = "https://files.pythonhosted.org/packages/4d/07/c2b2a761300cf60b90ccebba4328175331e67d34f4fbd39429a7ddcdce49/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef", size = 1409948, upload-time = "2026-08-16T22:54:22.343Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ec/1c2bf1acdd0e273d81f833f85caf0ae5423db68a783554992fca36e6c541/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143", size = 1265023, upload-time = "2026-08-16T22:54:41.402Z" },
{ url = "https://files.pythonhosted.org/packages/3d/a8/7f984908b7391160c7801b84e51ca8e4ba88c89e8d8811aa1aa7c03de73c/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c", size = 1282698, upload-time = "2026-08-16T22:56:06.998Z" },
{ url = "https://files.pythonhosted.org/packages/48/78/3a5d91c2d0250521736c42dfa2402b75049bc5fe2fb716c10bc84bb91ed1/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7", size = 1324816, upload-time = "2026-08-16T22:54:46.675Z" },
{ url = "https://files.pythonhosted.org/packages/6f/99/27450763853a034bca1574d3e0a315164b33ff49c3862df6872dda45e25e/hypothesis-6.165.10-cp310-abi3-win32.whl", hash = "sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746", size = 669039, upload-time = "2026-08-16T22:55:11.962Z" },
{ url = "https://files.pythonhosted.org/packages/2c/fc/ff2988b72b5705ad9ca500444bf3f43e3c2f41edfa034bbfeb23b215791a/hypothesis-6.165.10-cp310-abi3-win_amd64.whl", hash = "sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2", size = 675213, upload-time = "2026-08-16T22:55:01.697Z" },
{ url = "https://files.pythonhosted.org/packages/c5/8b/821810d36f78d9d9421cd2c5d9d36983b45bb3575c3086276cc5c76f9f73/hypothesis-6.165.10-cp310-abi3-win_arm64.whl", hash = "sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1", size = 673537, upload-time = "2026-08-16T22:54:47.898Z" },
{ url = "https://files.pythonhosted.org/packages/26/61/5e89268ce03317fb9f82449a1b3efd9e599dee090288fd0cf7586c532fb1/hypothesis-6.165.10-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:73e6df02a6a62f8045b511c272f894d08e56d174504c793c9effcbc6778051a8", size = 783959, upload-time = "2026-08-16T22:55:29.078Z" },
{ url = "https://files.pythonhosted.org/packages/e1/e9/f4e0832e81bb53b70cf1712e28c867db64245b32595b594217452e7dbd8d/hypothesis-6.165.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8b20f44773a9ab84400465e318712d8c2ca16418d35b9f80aa27fdf2d690ad10", size = 779684, upload-time = "2026-08-16T22:54:57.698Z" },
{ url = "https://files.pythonhosted.org/packages/77/de/ea072d3359d5678771bed407f80439e8ac7ca905d1031b0372f61bf5746e/hypothesis-6.165.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb8c7d05ea27a093a92b250904095d71d924b6b44e5795a415c1b20c265f0c65", size = 1108540, upload-time = "2026-08-16T22:55:03.282Z" },
{ url = "https://files.pythonhosted.org/packages/28/56/e7c395cdaa3d6c28b944c1c3c516dee50d2b7b3aeafa31874b57009ca51f/hypothesis-6.165.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4dafd6d6ababfa3b14dd6e5f0378cb7c7d291895a31a40abcbb7cc74f396131", size = 1158089, upload-time = "2026-08-16T22:54:36.205Z" },
{ url = "https://files.pythonhosted.org/packages/b1/49/1c6d2c465b9c5fc3213f1be89be95ba53819ca0130248c484129ccfefb71/hypothesis-6.165.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa74636a49fc8077413ce8db3e85f1c4aff880788bb55bda56253118e036fe5b", size = 1284125, upload-time = "2026-08-16T22:54:37.727Z" },
{ url = "https://files.pythonhosted.org/packages/7d/bc/7caf5ac3d0173bd57bd2a5ab854ca49a3664a4309257be1452f81025cc24/hypothesis-6.165.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2b112768cfb67f2b683e53e58c1a33d27811aacf60c942b8eb74635e469a73f6", size = 1325082, upload-time = "2026-08-16T22:54:38.952Z" },
{ url = "https://files.pythonhosted.org/packages/70/99/9d844330f570d6a4f127a683eab1e78c8263e6e72b16189f3534fa6bf6de/hypothesis-6.165.10-cp310-cp310-win_amd64.whl", hash = "sha256:56cb8c9055e50545fe6e3e5a560ec25a724673b2e4051f3c24d44e3ebc35dd72", size = 675082, upload-time = "2026-08-16T22:54:29.872Z" },
{ url = "https://files.pythonhosted.org/packages/ed/c2/b9546ace11f241c9c02d389f258cb80c14447a8c885771c9f1f0bc1d85ca/hypothesis-6.165.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:592107a0faf6c9c3a63a8dbf13dfb1cbda1cf599b0bc11c953221b00204b9ce1", size = 783716, upload-time = "2026-08-16T22:55:36.624Z" },
{ url = "https://files.pythonhosted.org/packages/37/10/27c2fdd574fd798caf5e91eb51f7834b098f5d840ce733efb3fba79ef86e/hypothesis-6.165.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9180c362bde06fd05380298ded4e234fbc0d6ede0a864835bfd91c1e24283d5", size = 779507, upload-time = "2026-08-16T22:55:07.633Z" },
{ url = "https://files.pythonhosted.org/packages/5e/b6/70bc23695f3783c4b0486b6cad47b08a20f791db4a3c1b25250add9659fa/hypothesis-6.165.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d623801ae3dcd97b77b983400ef3d48bf976648e4efff19929175322eaae074d", size = 1108406, upload-time = "2026-08-16T22:55:39.653Z" },
{ url = "https://files.pythonhosted.org/packages/71/4c/32e200bd7a352af4b7f4e3729aaa4cd002cb5fe8c4c6aef5599d0019f152/hypothesis-6.165.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f6236cfb90b7817bb1a6a087589ca4aa46d73170f0dd62963952ed5dadc589", size = 1157850, upload-time = "2026-08-16T22:55:24.394Z" },
{ url = "https://files.pythonhosted.org/packages/03/a5/8efc2a9a484822efc0d0da466f50094e0f2c068187faaf33831fc905873e/hypothesis-6.165.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad0764730e8e3421601c2cc7e1f054a9206c60ea0917165d8d9193dc453f34f1", size = 1283704, upload-time = "2026-08-16T22:54:27.279Z" },
{ url = "https://files.pythonhosted.org/packages/46/2a/90cc8d7463929c04786f29600de45f3227c12fa9bed1d5b7ce319b05e1c9/hypothesis-6.165.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:10d9a650a4666b0914831f769703d36140ed8039fd19bf9b71f615b8541eccf2", size = 1325077, upload-time = "2026-08-16T22:55:16.561Z" },
{ url = "https://files.pythonhosted.org/packages/82/ac/bc16faba4b42883e3d290bfaceff51e258b63fbbdf789bf9fe88df1ce537/hypothesis-6.165.10-cp311-cp311-win_amd64.whl", hash = "sha256:5671d2b2bf83bd4b6f02e55b32d432506eff5358c82f39b460a849ce19a2666e", size = 674920, upload-time = "2026-08-16T22:55:42.613Z" },
{ url = "https://files.pythonhosted.org/packages/e9/45/cde4f78afe2b9e29caecf38319eedc1deb76aebcacbdd128e03cbb2511c3/hypothesis-6.165.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:637445c1593a2a9d1024fda50082f07bb56baedda78d90a25f64b8111727ef94", size = 784835, upload-time = "2026-08-16T22:54:45.429Z" },
{ url = "https://files.pythonhosted.org/packages/7f/81/847f30b81cbfd07607296b3ce43067cf4f80799bd9244167f587de9c8081/hypothesis-6.165.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:713f4ce4e82c26b53031f139de959bc9e8b54d3995aa824b89bbdf8229df2a45", size = 776419, upload-time = "2026-08-16T22:55:33.633Z" },
{ url = "https://files.pythonhosted.org/packages/04/66/4c71c5be7a49d84b8c3a9278c1807c4c81181ab5474beb27df9d4c40dc0e/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f", size = 1106830, upload-time = "2026-08-16T22:55:10.389Z" },
{ url = "https://files.pythonhosted.org/packages/e3/c4/e2cbd2810e79f7a452a8ea9f6c6438ee718ce938d8cc12252cf0b36a81d3/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28", size = 1156952, upload-time = "2026-08-16T22:55:53.35Z" },
{ url = "https://files.pythonhosted.org/packages/a8/8b/794ced36864825492ac3712d5acab5a257b4601e6a9dc2ccdd3937198f87/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81", size = 1280780, upload-time = "2026-08-16T22:54:34.983Z" },
{ url = "https://files.pythonhosted.org/packages/5d/2d/550525442cdbcc2daf1f9bdd8ba35bcbde63db7c7a22f2ef137fbb49df2f/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f", size = 1324130, upload-time = "2026-08-16T22:55:48.659Z" },
{ url = "https://files.pythonhosted.org/packages/74/59/6caf69dd5fe03499ada94c9cec016bffcc164511c6b93fe680f01209b9ff/hypothesis-6.165.10-cp312-cp312-win_amd64.whl", hash = "sha256:3376f2594763aef14faa519b0fb27cae7ce9eeaab4c69efa07777499110306c9", size = 672337, upload-time = "2026-08-16T22:54:49.11Z" },
{ url = "https://files.pythonhosted.org/packages/b1/fb/c82c5bd92864ffcf319772fedc8c9bf2dbe4ca14baa0fee6e49e67b5ba1c/hypothesis-6.165.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad", size = 784726, upload-time = "2026-08-16T22:54:32.371Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b9/3d7acd08506da85557e65147b7f3fca8c47684e33be90bee0acb523920db/hypothesis-6.165.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4", size = 776375, upload-time = "2026-08-16T22:55:13.303Z" },
{ url = "https://files.pythonhosted.org/packages/38/6b/922e8b3f9a706dd89d440b9545d2c6231c65e74da1c1fee3ff36c251b9c4/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d", size = 1106763, upload-time = "2026-08-16T22:55:06.129Z" },
{ url = "https://files.pythonhosted.org/packages/01/39/f5b9a5d390d4edd1ad472334493ac442963ebeb4daaa74ff4bdac6ef292f/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620", size = 1156778, upload-time = "2026-08-16T22:54:33.824Z" },
{ url = "https://files.pythonhosted.org/packages/b5/5f/5fbe1be4326337fd6acefe2d18ed44007ee1dc1f98fe5b3c0eb22942364d/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb", size = 1280756, upload-time = "2026-08-16T22:55:54.834Z" },
{ url = "https://files.pythonhosted.org/packages/25/c0/cf6f9e1ef632a1a75694eed0db3a02e6fc75c367a363e94acee52f043c64/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96", size = 1323889, upload-time = "2026-08-16T22:55:56.567Z" },
{ url = "https://files.pythonhosted.org/packages/cc/cc/662b94880f260b0a88de1fdcf60fc9984f6e2a796da549542adc10a7bc83/hypothesis-6.165.10-cp313-cp313-win_amd64.whl", hash = "sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747", size = 672346, upload-time = "2026-08-16T22:56:03.792Z" },
{ url = "https://files.pythonhosted.org/packages/3f/77/55e020c9c576532ff7d20bf8b1dfa052ecbd5ada1949b02f76c44c966f7e/hypothesis-6.165.10-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209", size = 784833, upload-time = "2026-08-16T22:55:21.255Z" },
{ url = "https://files.pythonhosted.org/packages/4f/f2/01da2adf829cf549eaddcabb8e8072077fb3d26da4275f4c1e89b2c0af74/hypothesis-6.165.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611", size = 776545, upload-time = "2026-08-16T22:56:10.159Z" },
{ url = "https://files.pythonhosted.org/packages/cf/8e/58d4f842895220b793c53fc94a6489705b3665bb4d0ae4d338ce03fdf9fb/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e", size = 1107271, upload-time = "2026-08-16T22:54:50.266Z" },
{ url = "https://files.pythonhosted.org/packages/8f/b8/206468912d2153306bb8a41afdfc59e45b7a73a0495bbe4b9cb4f0e79c1d/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191", size = 1156915, upload-time = "2026-08-16T22:54:25.89Z" },
{ url = "https://files.pythonhosted.org/packages/fb/d3/bf5a22929b70a4cfd3edf69c5642b029b27ddb5cfda48fa295d384b01abb/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424", size = 1281205, upload-time = "2026-08-16T22:54:44.083Z" },
{ url = "https://files.pythonhosted.org/packages/07/a2/d7b2ba444d36fc84d4779f4431e74dd9b023dc63bcf282199f6e48ad39f4/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889", size = 1324243, upload-time = "2026-08-16T22:55:41.123Z" },
{ url = "https://files.pythonhosted.org/packages/d1/95/afe6b531fd01928c6f63d394ee413fa2338d088b2b44efcc23596b54477e/hypothesis-6.165.10-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063", size = 616382, upload-time = "2026-08-16T22:55:18.449Z" },
{ url = "https://files.pythonhosted.org/packages/48/86/9b4fb75f520a028edec50ffc904a94d724180395d71feb6d7a0ce7bb6f00/hypothesis-6.165.10-cp314-cp314-win_amd64.whl", hash = "sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd", size = 672145, upload-time = "2026-08-16T22:54:24.831Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ba/f7bbaae0c789bab7ddb764d2056ee1a463cc95a8acbccc90d4184e48b242/hypothesis-6.165.10-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff", size = 783287, upload-time = "2026-08-16T22:54:23.751Z" },
{ url = "https://files.pythonhosted.org/packages/3a/83/01ef80772b4abd335c49405576dc503cede94fb5da30ba2643a119013aea/hypothesis-6.165.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4", size = 774991, upload-time = "2026-08-16T22:55:25.987Z" },
{ url = "https://files.pythonhosted.org/packages/a3/0b/f47506241f9d5a5a2efe4c65b6bf4830e9d9576e5d3779007a260699e608/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413", size = 1105499, upload-time = "2026-08-16T22:54:51.864Z" },
{ url = "https://files.pythonhosted.org/packages/84/fe/abb3909b7089835112fbe75bf00d817d733b3a8032759783db0a24ff1e56/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3", size = 1155685, upload-time = "2026-08-16T22:54:30.94Z" },
{ url = "https://files.pythonhosted.org/packages/73/2f/1964738921640184067121ae77414522fc3f0463fc26c6e25a4f3b8e42ca/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647", size = 1279177, upload-time = "2026-08-16T22:54:40.179Z" },
{ url = "https://files.pythonhosted.org/packages/34/c5/312af8ae038d3af9cf3f7f1021c1abfe31c0d9035e4cf63519e0a7dc983e/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3", size = 1322921, upload-time = "2026-08-16T22:54:42.7Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e7/b0a2fde7570c090a1b914026266a421c751ef10138fffe37fe0ef9e675c0/hypothesis-6.165.10-cp314-cp314t-win_amd64.whl", hash = "sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8", size = 672147, upload-time = "2026-08-16T22:55:27.527Z" },
{ url = "https://files.pythonhosted.org/packages/47/fd/985aa564d6ffd06483d45a62b40d319df0a703cd8bc1d041de17d102fbaa/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5", size = 782882, upload-time = "2026-08-16T22:55:37.93Z" },
{ url = "https://files.pythonhosted.org/packages/f8/2c/6cc11151e450f72353a490940cd0db704680d07b78dc75dcc9f480e0d0e1/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a", size = 774584, upload-time = "2026-08-16T22:55:51.822Z" },
{ url = "https://files.pythonhosted.org/packages/10/39/ef26fa79c1738dfe9cdb1a3584fb6717d26429ca6c9d011cc4fdf08130c2/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0", size = 1104876, upload-time = "2026-08-16T22:54:58.937Z" },
{ url = "https://files.pythonhosted.org/packages/4e/f4/3fcc84e7637f42bf00d987093b9418083ac8db81b87392608a60f4b7c5fd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558", size = 1133353, upload-time = "2026-08-16T22:54:28.635Z" },
{ url = "https://files.pythonhosted.org/packages/35/59/21c5c14179c38f8d0de3560e7f1825c083311b3013b63f817d7dc78dfcbd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8", size = 1132300, upload-time = "2026-08-16T22:56:08.539Z" },
{ url = "https://files.pythonhosted.org/packages/14/af/fbb56059961e416b2de7b9dc5352db2e8572bd5ea46892957e4c1e5548ab/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245", size = 1155175, upload-time = "2026-08-16T22:55:19.824Z" },
{ url = "https://files.pythonhosted.org/packages/0f/53/77fb0c2dad445858555429c4e06cf94a59ae8d2407dd6426b5af97c84828/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f", size = 1109881, upload-time = "2026-08-16T22:55:32.029Z" },
{ url = "https://files.pythonhosted.org/packages/a8/7b/d187f673ff30e6ada640953636f978ffe64a6332f756b64163c2277f8d0c/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2", size = 1144963, upload-time = "2026-08-16T22:56:13.428Z" },
{ url = "https://files.pythonhosted.org/packages/e0/60/31d504e364134d60af23e5f6365db0da3cf4a51b3ed3d4836e5a2cff12cf/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48", size = 1278684, upload-time = "2026-08-16T22:55:22.971Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e6/89d26834a08c02f8da149e541dd40d7a96f68d9722f43146e69a77436ed7/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c", size = 1407202, upload-time = "2026-08-16T22:55:14.949Z" },
{ url = "https://files.pythonhosted.org/packages/dc/61/20d1e72246867ea195440092e8bb422c7ddc2f271b87b5b65679d5532719/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3", size = 1261395, upload-time = "2026-08-16T22:56:05.448Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9b/ebab6c3c2b90a16abb4119198178652d12aff83cc8ec2cfde5276c69fb1e/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe", size = 1279213, upload-time = "2026-08-16T22:55:35.066Z" },
{ url = "https://files.pythonhosted.org/packages/23/78/69b219b524231d36eb20c792e1f01e7cb037e02bd0af1c29f77ed9a969c0/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d", size = 1322367, upload-time = "2026-08-16T22:54:21.279Z" },
{ url = "https://files.pythonhosted.org/packages/55/63/ad5cc153dcc72ae5e7905fb9b3585f3e48ce892a2d6366f90163e867a69d/hypothesis-6.165.10-cp315-abi3.abi3t-win32.whl", hash = "sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc", size = 666038, upload-time = "2026-08-16T22:56:11.797Z" },
{ url = "https://files.pythonhosted.org/packages/80/32/b62307b73fbc99f0a4381d6f9456df76fbcbb7a27ef7256e26f0376f48ea/hypothesis-6.165.10-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d", size = 671941, upload-time = "2026-08-16T22:55:00.235Z" },
{ url = "https://files.pythonhosted.org/packages/c2/dd/e0f98add0548ef73ea7afac45da1fb8efc854d7f9931db568754d0f963f3/hypothesis-6.165.10-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015", size = 669931, upload-time = "2026-08-16T22:55:50.205Z" },
{ url = "https://files.pythonhosted.org/packages/0b/6a/880d6eeed5c451fb40a66733dadec4a5d498628a4a7f6a8a5f633f4c6dcb/hypothesis-6.165.10-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:34ee6402df6f31274d89119f1561b5f7489c97866afc5b7a3ed3a13d7e762802", size = 784644, upload-time = "2026-08-16T22:54:20.127Z" },
{ url = "https://files.pythonhosted.org/packages/27/e0/9e942bd3c3cf5ea0d5c0fd0905893bbfb6cefb7284c70fcc8033f8fdec38/hypothesis-6.165.10-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:277f41801e88dad2eba082f91a75632b7584ff64044ba2cf9dadf511b0d19cd0", size = 780515, upload-time = "2026-08-16T22:55:04.676Z" },
{ url = "https://files.pythonhosted.org/packages/19/32/f11a618415dc5fa9cdde41fea56c489f0814759527ae1ecd11a75a4558b9/hypothesis-6.165.10-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72df95fb1db41755b155c5f02106e0036a339250555c8d351d488704fd112cf9", size = 1109374, upload-time = "2026-08-16T22:56:00.241Z" },
{ url = "https://files.pythonhosted.org/packages/5e/6f/db49b719842297c2b71e0d81e5b8967d31215fb7389421abcb465ce7ed3f/hypothesis-6.165.10-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e20a02775eb3cf0ffb4f0219b6d7c1f240336663d4e5d7028675ec247c790c4", size = 1159092, upload-time = "2026-08-16T22:55:58.57Z" },
{ url = "https://files.pythonhosted.org/packages/b2/2a/bf0bae84ba1cb3923d295973f1fe38ee867eaf90119e0d559116083be300/hypothesis-6.165.10-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1ec53f08732e3cfd0342cbbd75dbd1b193c8f19390660466e536a748bb81f757", size = 676045, upload-time = "2026-08-16T22:55:45.514Z" },
]
[[package]]
name = "idna"
version = "3.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
]
[[package]]
name = "imagesize"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[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.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "platformdirs"
version = "4.11.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" },
]
[[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.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "pyproject-hooks"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
]
[[package]]
name = "pytest"
version = "9.1.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/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "pytest-randomly"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/27/b3/36192dacc0f470ac2cc516f73e01739c9a48a8224f76beada4f85e1c8a89/pytest_randomly-4.1.0.tar.gz", hash = "sha256:47f1d9746c3bc3efabd53ae1ebfb8bb385cf3d4df4b505b6d58d9c97a3dfe70f", size = 14302, upload-time = "2026-04-20T13:01:51.831Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/db/2df9a1fca597a273f957a559c20c2d95d629928384507b2afa43ba6909d1/pytest_randomly-4.1.0-py3-none-any.whl", hash = "sha256:f55e89e53367b090c0c053697d7f9d77595543d0e0516c93978b50c0f6b252f9", size = 8353, upload-time = "2026-04-20T13:01:50.382Z" },
]
[[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.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
name = "roman-numerals"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" },
]
[[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.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]]
name = "soupsieve"
version = "2.9.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" },
]
[[package]]
name = "sphinx"
version = "8.1.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "alabaster" },
{ name = "babel" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } },
{ name = "imagesize" },
{ name = "jinja2" },
{ name = "packaging" },
{ name = "pygments" },
{ name = "requests" },
{ name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml" },
{ name = "tomli" },
]
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 = "9.0.4"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.11.*'",
]
dependencies = [
{ name = "alabaster" },
{ name = "babel" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } },
{ name = "imagesize" },
{ name = "jinja2" },
{ name = "packaging" },
{ name = "pygments" },
{ name = "requests" },
{ name = "roman-numerals" },
{ name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" },
]
[[package]]
name = "sphinx"
version = "9.1.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version >= '3.12' and python_full_version < '3.14'",
]
dependencies = [
{ name = "alabaster" },
{ name = "babel" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } },
{ name = "imagesize" },
{ name = "jinja2" },
{ name = "packaging" },
{ name = "pygments" },
{ name = "requests" },
{ name = "roman-numerals" },
{ name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" },
]
[[package]]
name = "sphinx-basic-ng"
version = "1.0.0b2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
]
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-copybutton"
version = "0.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
]
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 = "3.5.1"
source = { editable = "." }
[package.optional-dependencies]
cli = [
{ name = "tokenize-rt" },
]
dateutil = [
{ name = "python-dateutil" },
]
[package.dev-dependencies]
build = [
{ name = "cibuildwheel", marker = "python_full_version >= '3.14'" },
]
docs = [
{ name = "furo" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "sphinx-copybutton" },
]
test = [
{ name = "coverage", extra = ["toml"] },
{ name = "freezegun" },
{ name = "hypothesis" },
{ name = "pytest" },
{ name = "pytest-randomly" },
{ name = "python-dateutil" },
]
test-base = [
{ name = "coverage", extra = ["toml"] },
{ name = "freezegun" },
{ name = "pytest" },
{ name = "pytest-randomly" },
{ name = "python-dateutil" },
]
[package.metadata]
requires-dist = [
{ name = "python-dateutil", marker = "extra == 'dateutil'", specifier = ">=2.8.2" },
{ name = "tokenize-rt", marker = "extra == 'cli'" },
]
provides-extras = ["cli", "dateutil"]
[package.metadata.requires-dev]
build = [{ name = "cibuildwheel", marker = "python_full_version >= '3.14'", specifier = ">=3.3.1" }]
docs = [
{ name = "furo", specifier = ">=2024.8.6" },
{ name = "sphinx", specifier = ">=7.4.7" },
{ name = "sphinx-copybutton", specifier = ">=0.5.2" },
]
test = [
{ name = "coverage", extras = ["toml"] },
{ name = "freezegun" },
{ name = "hypothesis" },
{ name = "pytest", specifier = ">=9" },
{ name = "pytest-randomly" },
{ name = "python-dateutil" },
]
test-base = [
{ name = "coverage", extras = ["toml"] },
{ name = "freezegun" },
{ name = "pytest", specifier = ">=9" },
{ 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.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]