0
# CLI Migration Tool
1
2
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.
3
4
## Capabilities
5
6
### Main CLI Interface
7
8
The primary entry point for the migration tool with support for batch file processing.
9
10
```python { .api }
11
def main(argv: Sequence[str] | None = None) -> int:
12
"""
13
Main entry point for the migration tool.
14
15
Parameters:
16
- argv: Command line arguments, uses sys.argv if None
17
18
Returns:
19
Exit code (0 for success, non-zero for errors)
20
"""
21
```
22
23
Usage from command line:
24
25
```bash
26
# Install with CLI dependencies
27
pip install time-machine[cli]
28
29
# Migrate single file
30
python -m time_machine migrate test_example.py
31
32
# Migrate multiple files
33
python -m time_machine migrate test_*.py
34
35
# Migrate from stdin
36
cat test_file.py | python -m time_machine migrate -
37
```
38
39
### File Processing Functions
40
41
Functions for processing individual files and batches of files during migration.
42
43
```python { .api }
44
def migrate_files(files: list[str]) -> int:
45
"""
46
Migrate multiple files from freezegun to time-machine.
47
48
Parameters:
49
- files: List of file paths to migrate
50
51
Returns:
52
Exit code (0 if no files changed, 1 if any files changed)
53
"""
54
55
def migrate_file(filename: str) -> int:
56
"""
57
Migrate a single file from freezegun to time-machine.
58
59
Parameters:
60
- filename: Path to file to migrate, or "-" for stdin
61
62
Returns:
63
1 if file was modified, 0 if no changes needed
64
"""
65
66
def migrate_contents(contents_text: str) -> str:
67
"""
68
Migrate file contents from freezegun to time-machine.
69
70
Parameters:
71
- contents_text: Source code as string
72
73
Returns:
74
Modified source code with time-machine imports and calls
75
"""
76
```
77
78
### Migration Transformations
79
80
The migration tool performs the following automatic transformations:
81
82
#### Import Statement Changes
83
84
```python
85
# Before migration
86
import freezegun
87
from freezegun import freeze_time
88
89
# After migration
90
import time_machine
91
from time_machine import travel
92
```
93
94
#### Decorator Usage Changes
95
96
```python
97
# Before migration
98
@freezegun.freeze_time("2023-01-01")
99
def test_something():
100
pass
101
102
@freeze_time("2023-01-01")
103
def test_something():
104
pass
105
106
# After migration
107
@time_machine.travel("2023-01-01", tick=False)
108
def test_something():
109
pass
110
111
@time_machine.travel("2023-01-01", tick=False)
112
def test_something():
113
pass
114
```
115
116
#### Context Manager Changes
117
118
```python
119
# Before migration
120
with freezegun.freeze_time("2023-01-01"):
121
pass
122
123
with freeze_time("2023-01-01"):
124
pass
125
126
# After migration
127
with time_machine.travel("2023-01-01", tick=False):
128
pass
129
130
with time_machine.travel("2023-01-01", tick=False):
131
pass
132
```
133
134
#### Class Decorator Changes
135
136
```python
137
# Before migration
138
@freeze_time("2023-01-01")
139
class TestSomething(unittest.TestCase):
140
pass
141
142
# After migration
143
@time_machine.travel("2023-01-01", tick=False)
144
class TestSomething(unittest.TestCase):
145
pass
146
```
147
148
### Migration Examples
149
150
#### Simple Test File Migration
151
152
Before migration (`test_old.py`):
153
```python
154
import freezegun
155
from datetime import datetime
156
157
@freezegun.freeze_time("2023-01-01")
158
def test_new_year():
159
assert datetime.now().year == 2023
160
161
def test_context_manager():
162
with freezegun.freeze_time("2023-06-15"):
163
assert datetime.now().month == 6
164
```
165
166
After migration:
167
```python
168
import time_machine
169
from datetime import datetime
170
171
@time_machine.travel("2023-01-01", tick=False)
172
def test_new_year():
173
assert datetime.now().year == 2023
174
175
def test_context_manager():
176
with time_machine.travel("2023-06-15", tick=False):
177
assert datetime.now().month == 6
178
```
179
180
#### Class-based Test Migration
181
182
Before migration:
183
```python
184
from freezegun import freeze_time
185
import unittest
186
187
@freeze_time("2023-01-01")
188
class TestFeatures(unittest.TestCase):
189
def test_feature_one(self):
190
self.assertEqual(datetime.now().year, 2023)
191
192
def test_feature_two(self):
193
self.assertEqual(datetime.now().month, 1)
194
```
195
196
After migration:
197
```python
198
import time_machine
199
import unittest
200
201
@time_machine.travel("2023-01-01", tick=False)
202
class TestFeatures(unittest.TestCase):
203
def test_feature_one(self):
204
self.assertEqual(datetime.now().year, 2023)
205
206
def test_feature_two(self):
207
self.assertEqual(datetime.now().month, 1)
208
```
209
210
### Usage Patterns
211
212
#### Interactive Migration
213
214
```bash
215
# Check what would change without modifying files
216
python -m time_machine migrate test_file.py > preview.py
217
diff test_file.py preview.py
218
219
# Migrate file in place
220
python -m time_machine migrate test_file.py
221
222
# Migrate multiple files with glob pattern
223
python -m time_machine migrate tests/test_*.py
224
225
# Process stdin (useful in pipelines)
226
find . -name "test_*.py" -exec cat {} \; | python -m time_machine migrate -
227
```
228
229
#### Batch Migration Script
230
231
```python
232
import subprocess
233
import glob
234
235
def migrate_project():
236
"""Migrate entire project from freezegun to time-machine."""
237
test_files = glob.glob("tests/**/*.py", recursive=True)
238
239
for file_path in test_files:
240
result = subprocess.run([
241
"python", "-m", "time_machine", "migrate", file_path
242
], capture_output=True, text=True)
243
244
if result.returncode == 1:
245
print(f"Migrated: {file_path}")
246
elif result.returncode != 0:
247
print(f"Error migrating {file_path}: {result.stderr}")
248
249
migrate_project()
250
```
251
252
### Error Handling
253
254
The migration tool handles various edge cases and provides informative error messages:
255
256
```python
257
# Non-UTF-8 files
258
# Output: "file.py is non-utf-8 (not supported)"
259
260
# Syntax errors in source
261
# Tool silently skips files with syntax errors
262
263
# Already migrated files
264
# Tool detects existing time-machine imports and skips unnecessary changes
265
```
266
267
### Limitations
268
269
The migration tool has the following limitations:
270
271
1. **Simple patterns only**: Only migrates basic `freeze_time()` usage patterns
272
2. **No keyword arguments**: Does not migrate calls with keyword arguments beyond the destination
273
3. **Static analysis**: Cannot handle dynamic import patterns or complex decorators
274
4. **Manual review needed**: Complex usage patterns may require manual adjustment
275
276
#### Manual Migration Examples
277
278
Some patterns require manual migration:
279
280
```python
281
# Complex decorator patterns (manual migration needed)
282
@freeze_time("2023-01-01", tick=True) # Has keyword args
283
def test_something():
284
pass
285
286
# Should become:
287
@time_machine.travel("2023-01-01", tick=True) # Keep original tick value
288
def test_something():
289
pass
290
291
# Dynamic usage (manual migration needed)
292
freeze_func = freeze_time
293
with freeze_func("2023-01-01"): # Dynamic reference
294
pass
295
296
# Should become:
297
travel_func = time_machine.travel
298
with travel_func("2023-01-01", tick=False):
299
pass
300
```
301
302
### Dependencies
303
304
The CLI tool requires additional dependencies:
305
306
```python
307
# Install with CLI support
308
pip install time-machine[cli]
309
310
# Or install dependencies manually
311
pip install tokenize-rt
312
```
313
314
## Type Definitions
315
316
```python { .api }
317
from collections.abc import Sequence
318
```