CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-time-machine

Travel through time in your tests.

Overview
Eval results
Files

cli-migration.mddocs/

CLI Migration Tool

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.

Capabilities

Main CLI Interface

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 -

File Processing Functions

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
    """

Migration Transformations

The migration tool performs the following automatic transformations:

Import Statement Changes

# Before migration
import freezegun
from freezegun import freeze_time

# After migration  
import time_machine
from time_machine import travel

Decorator Usage Changes

# 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

Context Manager Changes

# 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

Class Decorator Changes

# 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):
    pass

Migration Examples

Simple Test File Migration

Before 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 == 6

After 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 == 6

Class-based Test Migration

Before 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)

Usage Patterns

Interactive Migration

# 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 -

Batch Migration Script

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()

Error Handling

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 changes

Limitations

The migration tool has the following limitations:

  1. Simple patterns only: Only migrates basic freeze_time() usage patterns
  2. No keyword arguments: Does not migrate calls with keyword arguments beyond the destination
  3. Static analysis: Cannot handle dynamic import patterns or complex decorators
  4. Manual review needed: Complex usage patterns may require manual adjustment

Manual Migration Examples

Some 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):
    pass

Dependencies

The CLI tool requires additional dependencies:

# Install with CLI support
pip install time-machine[cli]

# Or install dependencies manually
pip install tokenize-rt

Type Definitions

from collections.abc import Sequence

Install with Tessl CLI

npx tessl i tessl/pypi-time-machine

docs

cli-migration.md

core-time-travel.md

escape-hatch.md

index.md

pytest-integration.md

tile.json