Travel through time in your tests.
A command-line tool for automatically migrating test code from freezegun to time-machine. The tool parses Python source files and rewrites imports, decorators, and context manager usage to use time-machine's API.
The primary entry point for the migration tool with support for batch file processing.
def main(argv: Sequence[str] | None = None) -> int:
"""
Main entry point for the migration tool.
Parameters:
- argv: Command line arguments, uses sys.argv if None
Returns:
Exit code (0 for success, non-zero for errors)
"""Usage from command line:
# Install with CLI dependencies
pip install time-machine[cli]
# Migrate single file
python -m time_machine migrate test_example.py
# Migrate multiple files
python -m time_machine migrate test_*.py
# Migrate from stdin
cat test_file.py | python -m time_machine migrate -Functions for processing individual files and batches of files during migration.
def migrate_files(files: list[str]) -> int:
"""
Migrate multiple files from freezegun to time-machine.
Parameters:
- files: List of file paths to migrate
Returns:
Exit code (0 if no files changed, 1 if any files changed)
"""
def migrate_file(filename: str) -> int:
"""
Migrate a single file from freezegun to time-machine.
Parameters:
- filename: Path to file to migrate, or "-" for stdin
Returns:
1 if file was modified, 0 if no changes needed
"""
def migrate_contents(contents_text: str) -> str:
"""
Migrate file contents from freezegun to time-machine.
Parameters:
- contents_text: Source code as string
Returns:
Modified source code with time-machine imports and calls
"""The migration tool performs the following automatic transformations:
# Before migration
import freezegun
from freezegun import freeze_time
# After migration
import time_machine
from time_machine import travel# Before migration
@freezegun.freeze_time("2023-01-01")
def test_something():
pass
@freeze_time("2023-01-01")
def test_something():
pass
# After migration
@time_machine.travel("2023-01-01", tick=False)
def test_something():
pass
@time_machine.travel("2023-01-01", tick=False)
def test_something():
pass# Before migration
with freezegun.freeze_time("2023-01-01"):
pass
with freeze_time("2023-01-01"):
pass
# After migration
with time_machine.travel("2023-01-01", tick=False):
pass
with time_machine.travel("2023-01-01", tick=False):
pass# Before migration
@freeze_time("2023-01-01")
class TestSomething(unittest.TestCase):
pass
# After migration
@time_machine.travel("2023-01-01", tick=False)
class TestSomething(unittest.TestCase):
passBefore migration (test_old.py):
import freezegun
from datetime import datetime
@freezegun.freeze_time("2023-01-01")
def test_new_year():
assert datetime.now().year == 2023
def test_context_manager():
with freezegun.freeze_time("2023-06-15"):
assert datetime.now().month == 6After migration:
import time_machine
from datetime import datetime
@time_machine.travel("2023-01-01", tick=False)
def test_new_year():
assert datetime.now().year == 2023
def test_context_manager():
with time_machine.travel("2023-06-15", tick=False):
assert datetime.now().month == 6Before migration:
from freezegun import freeze_time
import unittest
@freeze_time("2023-01-01")
class TestFeatures(unittest.TestCase):
def test_feature_one(self):
self.assertEqual(datetime.now().year, 2023)
def test_feature_two(self):
self.assertEqual(datetime.now().month, 1)After migration:
import time_machine
import unittest
@time_machine.travel("2023-01-01", tick=False)
class TestFeatures(unittest.TestCase):
def test_feature_one(self):
self.assertEqual(datetime.now().year, 2023)
def test_feature_two(self):
self.assertEqual(datetime.now().month, 1)# Check what would change without modifying files
python -m time_machine migrate test_file.py > preview.py
diff test_file.py preview.py
# Migrate file in place
python -m time_machine migrate test_file.py
# Migrate multiple files with glob pattern
python -m time_machine migrate tests/test_*.py
# Process stdin (useful in pipelines)
find . -name "test_*.py" -exec cat {} \; | python -m time_machine migrate -import subprocess
import glob
def migrate_project():
"""Migrate entire project from freezegun to time-machine."""
test_files = glob.glob("tests/**/*.py", recursive=True)
for file_path in test_files:
result = subprocess.run([
"python", "-m", "time_machine", "migrate", file_path
], capture_output=True, text=True)
if result.returncode == 1:
print(f"Migrated: {file_path}")
elif result.returncode != 0:
print(f"Error migrating {file_path}: {result.stderr}")
migrate_project()The migration tool handles various edge cases and provides informative error messages:
# Non-UTF-8 files
# Output: "file.py is non-utf-8 (not supported)"
# Syntax errors in source
# Tool silently skips files with syntax errors
# Already migrated files
# Tool detects existing time-machine imports and skips unnecessary changesThe migration tool has the following limitations:
freeze_time() usage patternsSome patterns require manual migration:
# Complex decorator patterns (manual migration needed)
@freeze_time("2023-01-01", tick=True) # Has keyword args
def test_something():
pass
# Should become:
@time_machine.travel("2023-01-01", tick=True) # Keep original tick value
def test_something():
pass
# Dynamic usage (manual migration needed)
freeze_func = freeze_time
with freeze_func("2023-01-01"): # Dynamic reference
pass
# Should become:
travel_func = time_machine.travel
with travel_func("2023-01-01", tick=False):
passThe CLI tool requires additional dependencies:
# Install with CLI support
pip install time-machine[cli]
# Or install dependencies manually
pip install tokenize-rtfrom collections.abc import SequenceInstall with Tessl CLI
npx tessl i tessl/pypi-time-machine