codeshift 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeshift/__init__.py +8 -0
- codeshift/analyzer/__init__.py +5 -0
- codeshift/analyzer/risk_assessor.py +388 -0
- codeshift/api/__init__.py +1 -0
- codeshift/api/auth.py +182 -0
- codeshift/api/config.py +73 -0
- codeshift/api/database.py +215 -0
- codeshift/api/main.py +103 -0
- codeshift/api/models/__init__.py +55 -0
- codeshift/api/models/auth.py +108 -0
- codeshift/api/models/billing.py +92 -0
- codeshift/api/models/migrate.py +42 -0
- codeshift/api/models/usage.py +116 -0
- codeshift/api/routers/__init__.py +5 -0
- codeshift/api/routers/auth.py +440 -0
- codeshift/api/routers/billing.py +395 -0
- codeshift/api/routers/migrate.py +304 -0
- codeshift/api/routers/usage.py +291 -0
- codeshift/api/routers/webhooks.py +289 -0
- codeshift/cli/__init__.py +5 -0
- codeshift/cli/commands/__init__.py +7 -0
- codeshift/cli/commands/apply.py +352 -0
- codeshift/cli/commands/auth.py +842 -0
- codeshift/cli/commands/diff.py +221 -0
- codeshift/cli/commands/scan.py +368 -0
- codeshift/cli/commands/upgrade.py +436 -0
- codeshift/cli/commands/upgrade_all.py +518 -0
- codeshift/cli/main.py +221 -0
- codeshift/cli/quota.py +210 -0
- codeshift/knowledge/__init__.py +50 -0
- codeshift/knowledge/cache.py +167 -0
- codeshift/knowledge/generator.py +231 -0
- codeshift/knowledge/models.py +151 -0
- codeshift/knowledge/parser.py +270 -0
- codeshift/knowledge/sources.py +388 -0
- codeshift/knowledge_base/__init__.py +17 -0
- codeshift/knowledge_base/loader.py +102 -0
- codeshift/knowledge_base/models.py +110 -0
- codeshift/migrator/__init__.py +23 -0
- codeshift/migrator/ast_transforms.py +256 -0
- codeshift/migrator/engine.py +395 -0
- codeshift/migrator/llm_migrator.py +320 -0
- codeshift/migrator/transforms/__init__.py +19 -0
- codeshift/migrator/transforms/fastapi_transformer.py +174 -0
- codeshift/migrator/transforms/pandas_transformer.py +236 -0
- codeshift/migrator/transforms/pydantic_v1_to_v2.py +637 -0
- codeshift/migrator/transforms/requests_transformer.py +218 -0
- codeshift/migrator/transforms/sqlalchemy_transformer.py +175 -0
- codeshift/scanner/__init__.py +6 -0
- codeshift/scanner/code_scanner.py +352 -0
- codeshift/scanner/dependency_parser.py +473 -0
- codeshift/utils/__init__.py +5 -0
- codeshift/utils/api_client.py +266 -0
- codeshift/utils/cache.py +318 -0
- codeshift/utils/config.py +71 -0
- codeshift/utils/llm_client.py +221 -0
- codeshift/validator/__init__.py +6 -0
- codeshift/validator/syntax_checker.py +183 -0
- codeshift/validator/test_runner.py +224 -0
- codeshift-0.2.0.dist-info/METADATA +326 -0
- codeshift-0.2.0.dist-info/RECORD +65 -0
- codeshift-0.2.0.dist-info/WHEEL +5 -0
- codeshift-0.2.0.dist-info/entry_points.txt +2 -0
- codeshift-0.2.0.dist-info/licenses/LICENSE +21 -0
- codeshift-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""LLM-based migration for complex cases.
|
|
2
|
+
|
|
3
|
+
LLM migrations (Tier 2/3) are routed through the PyResolve API,
|
|
4
|
+
which handles authentication, quota checking, and billing.
|
|
5
|
+
Users must have a Pro or Unlimited subscription to use LLM features.
|
|
6
|
+
|
|
7
|
+
Tier 1 (deterministic AST transforms) remains free and local.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from codeshift.migrator.ast_transforms import (
|
|
15
|
+
TransformChange,
|
|
16
|
+
TransformResult,
|
|
17
|
+
TransformStatus,
|
|
18
|
+
)
|
|
19
|
+
from codeshift.utils.api_client import PyResolveAPIClient, get_api_client
|
|
20
|
+
from codeshift.utils.cache import LLMCache, get_llm_cache
|
|
21
|
+
from codeshift.validator.syntax_checker import quick_syntax_check
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class LLMMigrationResult:
|
|
26
|
+
"""Result of an LLM-based migration."""
|
|
27
|
+
|
|
28
|
+
original_code: str
|
|
29
|
+
migrated_code: str
|
|
30
|
+
success: bool
|
|
31
|
+
used_cache: bool = False
|
|
32
|
+
error: str | None = None
|
|
33
|
+
validation_passed: bool = True
|
|
34
|
+
usage: dict | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LLMMigrator:
|
|
38
|
+
"""Handles complex migrations using LLM via the PyResolve API.
|
|
39
|
+
|
|
40
|
+
LLM migrations require authentication and a Pro or Unlimited subscription.
|
|
41
|
+
All LLM calls are routed through the PyResolve API, which handles:
|
|
42
|
+
- Authentication and authorization
|
|
43
|
+
- Quota checking and billing
|
|
44
|
+
- Server-side Anthropic API calls
|
|
45
|
+
|
|
46
|
+
This ensures users cannot bypass the subscription model by using
|
|
47
|
+
their own API keys.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
client: PyResolveAPIClient | None = None,
|
|
53
|
+
cache: LLMCache | None = None,
|
|
54
|
+
use_cache: bool = True,
|
|
55
|
+
validate_output: bool = True,
|
|
56
|
+
):
|
|
57
|
+
"""Initialize the LLM migrator.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
client: API client to use. Defaults to singleton.
|
|
61
|
+
cache: Cache to use. Defaults to singleton.
|
|
62
|
+
use_cache: Whether to use caching
|
|
63
|
+
validate_output: Whether to validate migrated code syntax
|
|
64
|
+
"""
|
|
65
|
+
self.client = client or get_api_client()
|
|
66
|
+
self.cache = cache or get_llm_cache() if use_cache else None
|
|
67
|
+
self.use_cache = use_cache
|
|
68
|
+
self.validate_output = validate_output
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_available(self) -> bool:
|
|
72
|
+
"""Check if LLM migration is available (user is authenticated)."""
|
|
73
|
+
return self.client.is_available
|
|
74
|
+
|
|
75
|
+
def migrate(
|
|
76
|
+
self,
|
|
77
|
+
code: str,
|
|
78
|
+
library: str,
|
|
79
|
+
from_version: str,
|
|
80
|
+
to_version: str,
|
|
81
|
+
context: str | None = None,
|
|
82
|
+
) -> LLMMigrationResult:
|
|
83
|
+
"""Migrate code using the LLM via PyResolve API.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
code: Source code to migrate
|
|
87
|
+
library: Library being upgraded
|
|
88
|
+
from_version: Current version
|
|
89
|
+
to_version: Target version
|
|
90
|
+
context: Optional context about the code
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
LLMMigrationResult with the migrated code
|
|
94
|
+
"""
|
|
95
|
+
if not self.is_available:
|
|
96
|
+
return LLMMigrationResult(
|
|
97
|
+
original_code=code,
|
|
98
|
+
migrated_code=code,
|
|
99
|
+
success=False,
|
|
100
|
+
error=(
|
|
101
|
+
"LLM migrations require authentication. "
|
|
102
|
+
"Run 'codeshift login' to authenticate, then upgrade to Pro tier."
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Check cache first
|
|
107
|
+
if self.use_cache and self.cache:
|
|
108
|
+
cached = self.cache.get_migration(code, library, from_version, to_version)
|
|
109
|
+
if cached:
|
|
110
|
+
return LLMMigrationResult(
|
|
111
|
+
original_code=code,
|
|
112
|
+
migrated_code=cached,
|
|
113
|
+
success=True,
|
|
114
|
+
used_cache=True,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Call PyResolve API (which calls Anthropic server-side)
|
|
118
|
+
response = self.client.migrate_code(
|
|
119
|
+
code=code,
|
|
120
|
+
library=library,
|
|
121
|
+
from_version=from_version,
|
|
122
|
+
to_version=to_version,
|
|
123
|
+
context=context,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not response.success:
|
|
127
|
+
return LLMMigrationResult(
|
|
128
|
+
original_code=code,
|
|
129
|
+
migrated_code=code,
|
|
130
|
+
success=False,
|
|
131
|
+
error=response.error,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
migrated_code = response.content
|
|
135
|
+
|
|
136
|
+
# Validate syntax
|
|
137
|
+
validation_passed = True
|
|
138
|
+
if self.validate_output:
|
|
139
|
+
if not quick_syntax_check(migrated_code):
|
|
140
|
+
validation_passed = False
|
|
141
|
+
# Try to fix common issues
|
|
142
|
+
migrated_code = self._attempt_fix(migrated_code)
|
|
143
|
+
if not quick_syntax_check(migrated_code):
|
|
144
|
+
return LLMMigrationResult(
|
|
145
|
+
original_code=code,
|
|
146
|
+
migrated_code=code,
|
|
147
|
+
success=False,
|
|
148
|
+
error="LLM output has syntax errors",
|
|
149
|
+
validation_passed=False,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Cache the result
|
|
153
|
+
if self.use_cache and self.cache:
|
|
154
|
+
self.cache.set_migration(code, library, from_version, to_version, migrated_code)
|
|
155
|
+
|
|
156
|
+
return LLMMigrationResult(
|
|
157
|
+
original_code=code,
|
|
158
|
+
migrated_code=migrated_code,
|
|
159
|
+
success=True,
|
|
160
|
+
validation_passed=validation_passed,
|
|
161
|
+
usage=response.usage,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _attempt_fix(self, code: str) -> str:
|
|
165
|
+
"""Attempt to fix common syntax issues in LLM output.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
code: Code with potential issues
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Fixed code (or original if unfixable)
|
|
172
|
+
"""
|
|
173
|
+
# Common fixes
|
|
174
|
+
fixes = [
|
|
175
|
+
# Remove trailing incomplete lines
|
|
176
|
+
(r"\n\s*$", "\n"),
|
|
177
|
+
# Fix unclosed strings (simple cases)
|
|
178
|
+
(r'(["\'])([^"\'\n]*?)$', r"\1\2\1"),
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
fixed = code
|
|
182
|
+
for pattern, replacement in fixes:
|
|
183
|
+
fixed = re.sub(pattern, replacement, fixed)
|
|
184
|
+
|
|
185
|
+
return fixed
|
|
186
|
+
|
|
187
|
+
def explain_migration(
|
|
188
|
+
self,
|
|
189
|
+
original: str,
|
|
190
|
+
transformed: str,
|
|
191
|
+
library: str,
|
|
192
|
+
) -> str | None:
|
|
193
|
+
"""Get an explanation of a migration change.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
original: Original code
|
|
197
|
+
transformed: Transformed code
|
|
198
|
+
library: Library being upgraded
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Explanation string or None if unavailable
|
|
202
|
+
"""
|
|
203
|
+
if not self.is_available:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
response = self.client.explain_change(original, transformed, library)
|
|
207
|
+
if response.success:
|
|
208
|
+
return response.content.strip()
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def migrate_with_llm_fallback(
|
|
213
|
+
code: str,
|
|
214
|
+
library: str,
|
|
215
|
+
from_version: str,
|
|
216
|
+
to_version: str,
|
|
217
|
+
deterministic_result: TransformResult | None = None,
|
|
218
|
+
) -> TransformResult:
|
|
219
|
+
"""Migrate code with LLM as a fallback for failures.
|
|
220
|
+
|
|
221
|
+
IMPORTANT: LLM migrations require Pro tier or higher subscription.
|
|
222
|
+
If the user is not authenticated or doesn't have the required tier,
|
|
223
|
+
only deterministic transforms will be applied.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
code: Source code to migrate
|
|
227
|
+
library: Library being upgraded
|
|
228
|
+
from_version: Current version
|
|
229
|
+
to_version: Target version
|
|
230
|
+
deterministic_result: Optional result from deterministic transform
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
TransformResult combining deterministic and LLM migrations
|
|
234
|
+
"""
|
|
235
|
+
# If deterministic transform succeeded fully, use it
|
|
236
|
+
if deterministic_result and deterministic_result.status == TransformStatus.SUCCESS:
|
|
237
|
+
return deterministic_result
|
|
238
|
+
|
|
239
|
+
# Try LLM migration
|
|
240
|
+
migrator = LLMMigrator()
|
|
241
|
+
|
|
242
|
+
if not migrator.is_available:
|
|
243
|
+
# Return deterministic result if available, or no changes
|
|
244
|
+
if deterministic_result:
|
|
245
|
+
# Add a note about LLM not being available
|
|
246
|
+
errors = list(deterministic_result.errors)
|
|
247
|
+
errors.append(
|
|
248
|
+
"LLM fallback not available. Run 'codeshift login' and upgrade to Pro "
|
|
249
|
+
"for LLM-powered migrations."
|
|
250
|
+
)
|
|
251
|
+
return TransformResult(
|
|
252
|
+
file_path=deterministic_result.file_path,
|
|
253
|
+
status=deterministic_result.status,
|
|
254
|
+
original_code=deterministic_result.original_code,
|
|
255
|
+
transformed_code=deterministic_result.transformed_code,
|
|
256
|
+
changes=deterministic_result.changes,
|
|
257
|
+
errors=errors,
|
|
258
|
+
)
|
|
259
|
+
return TransformResult(
|
|
260
|
+
file_path=Path("<unknown>"),
|
|
261
|
+
status=TransformStatus.NO_CHANGES,
|
|
262
|
+
original_code=code,
|
|
263
|
+
transformed_code=code,
|
|
264
|
+
errors=[
|
|
265
|
+
"LLM not available. Run 'codeshift login' and upgrade to Pro "
|
|
266
|
+
"for LLM-powered migrations."
|
|
267
|
+
],
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Use deterministic result as base if available
|
|
271
|
+
base_code = deterministic_result.transformed_code if deterministic_result else code
|
|
272
|
+
context = None
|
|
273
|
+
|
|
274
|
+
if deterministic_result and deterministic_result.errors:
|
|
275
|
+
context = f"Deterministic transform had issues: {', '.join(deterministic_result.errors)}"
|
|
276
|
+
|
|
277
|
+
result = migrator.migrate(
|
|
278
|
+
code=base_code,
|
|
279
|
+
library=library,
|
|
280
|
+
from_version=from_version,
|
|
281
|
+
to_version=to_version,
|
|
282
|
+
context=context,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Combine results
|
|
286
|
+
changes = []
|
|
287
|
+
if deterministic_result:
|
|
288
|
+
changes.extend(deterministic_result.changes)
|
|
289
|
+
|
|
290
|
+
if result.success and result.migrated_code != base_code:
|
|
291
|
+
changes.append(
|
|
292
|
+
TransformChange(
|
|
293
|
+
description="LLM-assisted migration",
|
|
294
|
+
line_number=1,
|
|
295
|
+
original="(various)",
|
|
296
|
+
replacement="(LLM migrated)",
|
|
297
|
+
transform_name="llm_migration",
|
|
298
|
+
confidence=0.8, # Lower confidence for LLM
|
|
299
|
+
notes="This change was made by the LLM and should be reviewed carefully",
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
status = TransformStatus.SUCCESS if result.success else TransformStatus.PARTIAL
|
|
304
|
+
if not changes:
|
|
305
|
+
status = TransformStatus.NO_CHANGES
|
|
306
|
+
|
|
307
|
+
errors = []
|
|
308
|
+
if deterministic_result:
|
|
309
|
+
errors.extend(deterministic_result.errors)
|
|
310
|
+
if result.error:
|
|
311
|
+
errors.append(result.error)
|
|
312
|
+
|
|
313
|
+
return TransformResult(
|
|
314
|
+
file_path=deterministic_result.file_path if deterministic_result else Path("<unknown>"),
|
|
315
|
+
status=status,
|
|
316
|
+
original_code=code,
|
|
317
|
+
transformed_code=result.migrated_code if result.success else base_code,
|
|
318
|
+
changes=changes,
|
|
319
|
+
errors=errors,
|
|
320
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Library-specific transformation modules."""
|
|
2
|
+
|
|
3
|
+
from codeshift.migrator.transforms.fastapi_transformer import FastAPITransformer
|
|
4
|
+
from codeshift.migrator.transforms.pandas_transformer import (
|
|
5
|
+
PandasAppendTransformer,
|
|
6
|
+
PandasTransformer,
|
|
7
|
+
)
|
|
8
|
+
from codeshift.migrator.transforms.pydantic_v1_to_v2 import PydanticV1ToV2Transformer
|
|
9
|
+
from codeshift.migrator.transforms.requests_transformer import RequestsTransformer
|
|
10
|
+
from codeshift.migrator.transforms.sqlalchemy_transformer import SQLAlchemyTransformer
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"PydanticV1ToV2Transformer",
|
|
14
|
+
"FastAPITransformer",
|
|
15
|
+
"SQLAlchemyTransformer",
|
|
16
|
+
"PandasTransformer",
|
|
17
|
+
"PandasAppendTransformer",
|
|
18
|
+
"RequestsTransformer",
|
|
19
|
+
]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""FastAPI transformation using LibCST."""
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
|
|
5
|
+
from codeshift.migrator.ast_transforms import BaseTransformer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FastAPITransformer(BaseTransformer):
|
|
9
|
+
"""Transform FastAPI code for version upgrades (0.99 to 0.100+)."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
super().__init__()
|
|
13
|
+
self._in_fastapi_context = False
|
|
14
|
+
|
|
15
|
+
def leave_ImportFrom(
|
|
16
|
+
self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom
|
|
17
|
+
) -> cst.ImportFrom:
|
|
18
|
+
"""Transform starlette imports to fastapi imports."""
|
|
19
|
+
if original_node.module is None:
|
|
20
|
+
return updated_node
|
|
21
|
+
|
|
22
|
+
module_name = self._get_module_name(original_node.module)
|
|
23
|
+
|
|
24
|
+
# Transform starlette.responses imports
|
|
25
|
+
if module_name == "starlette.responses":
|
|
26
|
+
self.record_change(
|
|
27
|
+
description="Import from fastapi.responses instead of starlette.responses",
|
|
28
|
+
line_number=1,
|
|
29
|
+
original=f"from {module_name}",
|
|
30
|
+
replacement="from fastapi.responses",
|
|
31
|
+
transform_name="starlette_to_fastapi_responses",
|
|
32
|
+
)
|
|
33
|
+
return updated_node.with_changes(
|
|
34
|
+
module=cst.Attribute(
|
|
35
|
+
value=cst.Name("fastapi"),
|
|
36
|
+
attr=cst.Name("responses"),
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Transform starlette.requests imports
|
|
41
|
+
if module_name == "starlette.requests":
|
|
42
|
+
self.record_change(
|
|
43
|
+
description="Import Request from fastapi instead of starlette.requests",
|
|
44
|
+
line_number=1,
|
|
45
|
+
original=f"from {module_name}",
|
|
46
|
+
replacement="from fastapi",
|
|
47
|
+
transform_name="starlette_to_fastapi_request",
|
|
48
|
+
)
|
|
49
|
+
return updated_node.with_changes(module=cst.Name("fastapi"))
|
|
50
|
+
|
|
51
|
+
# Transform starlette.websockets imports
|
|
52
|
+
if module_name == "starlette.websockets":
|
|
53
|
+
self.record_change(
|
|
54
|
+
description="Import WebSocket from fastapi instead of starlette.websockets",
|
|
55
|
+
line_number=1,
|
|
56
|
+
original=f"from {module_name}",
|
|
57
|
+
replacement="from fastapi",
|
|
58
|
+
transform_name="starlette_to_fastapi_websocket",
|
|
59
|
+
)
|
|
60
|
+
return updated_node.with_changes(module=cst.Name("fastapi"))
|
|
61
|
+
|
|
62
|
+
# Transform starlette.status imports
|
|
63
|
+
if module_name == "starlette.status":
|
|
64
|
+
self.record_change(
|
|
65
|
+
description="Import status from fastapi instead of starlette",
|
|
66
|
+
line_number=1,
|
|
67
|
+
original=f"from {module_name}",
|
|
68
|
+
replacement="from fastapi import status",
|
|
69
|
+
transform_name="starlette_to_fastapi_status",
|
|
70
|
+
)
|
|
71
|
+
return updated_node.with_changes(module=cst.Name("fastapi"))
|
|
72
|
+
|
|
73
|
+
return updated_node
|
|
74
|
+
|
|
75
|
+
def leave_Call(self, original_node: cst.Call, updated_node: cst.Call) -> cst.Call:
|
|
76
|
+
"""Transform FastAPI function calls."""
|
|
77
|
+
# Handle Field, Query, Path, Body regex -> pattern
|
|
78
|
+
if isinstance(updated_node.func, cst.Name):
|
|
79
|
+
func_name = updated_node.func.value
|
|
80
|
+
if func_name in ("Field", "Query", "Path", "Body"):
|
|
81
|
+
new_args = []
|
|
82
|
+
changed = False
|
|
83
|
+
for arg in updated_node.args:
|
|
84
|
+
if isinstance(arg.keyword, cst.Name) and arg.keyword.value == "regex":
|
|
85
|
+
new_args.append(arg.with_changes(keyword=cst.Name("pattern")))
|
|
86
|
+
changed = True
|
|
87
|
+
self.record_change(
|
|
88
|
+
description=f"Rename {func_name}(regex=...) to {func_name}(pattern=...)",
|
|
89
|
+
line_number=1,
|
|
90
|
+
original=f"{func_name}(regex=...)",
|
|
91
|
+
replacement=f"{func_name}(pattern=...)",
|
|
92
|
+
transform_name=f"{func_name.lower()}_regex_to_pattern",
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
new_args.append(arg)
|
|
96
|
+
|
|
97
|
+
if changed:
|
|
98
|
+
return updated_node.with_changes(args=new_args)
|
|
99
|
+
|
|
100
|
+
# Handle Depends(use_cache=...) -> Depends(use_cached=...)
|
|
101
|
+
if isinstance(updated_node.func, cst.Name) and updated_node.func.value == "Depends":
|
|
102
|
+
new_args = []
|
|
103
|
+
changed = False
|
|
104
|
+
for arg in updated_node.args:
|
|
105
|
+
if isinstance(arg.keyword, cst.Name) and arg.keyword.value == "use_cache":
|
|
106
|
+
new_args.append(arg.with_changes(keyword=cst.Name("use_cached")))
|
|
107
|
+
changed = True
|
|
108
|
+
self.record_change(
|
|
109
|
+
description="Rename Depends(use_cache=...) to Depends(use_cached=...)",
|
|
110
|
+
line_number=1,
|
|
111
|
+
original="Depends(use_cache=...)",
|
|
112
|
+
replacement="Depends(use_cached=...)",
|
|
113
|
+
transform_name="depends_use_cache_rename",
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
new_args.append(arg)
|
|
117
|
+
|
|
118
|
+
if changed:
|
|
119
|
+
return updated_node.with_changes(args=new_args)
|
|
120
|
+
|
|
121
|
+
# Handle FastAPI(openapi_prefix=...) -> FastAPI(root_path=...)
|
|
122
|
+
if isinstance(updated_node.func, cst.Name) and updated_node.func.value == "FastAPI":
|
|
123
|
+
new_args = []
|
|
124
|
+
changed = False
|
|
125
|
+
for arg in updated_node.args:
|
|
126
|
+
if isinstance(arg.keyword, cst.Name) and arg.keyword.value == "openapi_prefix":
|
|
127
|
+
new_args.append(arg.with_changes(keyword=cst.Name("root_path")))
|
|
128
|
+
changed = True
|
|
129
|
+
self.record_change(
|
|
130
|
+
description="Rename openapi_prefix to root_path",
|
|
131
|
+
line_number=1,
|
|
132
|
+
original="FastAPI(openapi_prefix=...)",
|
|
133
|
+
replacement="FastAPI(root_path=...)",
|
|
134
|
+
transform_name="openapi_prefix_to_root_path",
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
new_args.append(arg)
|
|
138
|
+
|
|
139
|
+
if changed:
|
|
140
|
+
return updated_node.with_changes(args=new_args)
|
|
141
|
+
|
|
142
|
+
return updated_node
|
|
143
|
+
|
|
144
|
+
def _get_module_name(self, module: cst.BaseExpression) -> str:
|
|
145
|
+
"""Get the full module name from a Name or Attribute node."""
|
|
146
|
+
if isinstance(module, cst.Name):
|
|
147
|
+
return str(module.value)
|
|
148
|
+
elif isinstance(module, cst.Attribute):
|
|
149
|
+
return f"{self._get_module_name(module.value)}.{module.attr.value}"
|
|
150
|
+
return ""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def transform_fastapi(source_code: str) -> tuple[str, list]:
|
|
154
|
+
"""Transform FastAPI code.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
source_code: The source code to transform
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Tuple of (transformed_code, list of changes)
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
tree = cst.parse_module(source_code)
|
|
164
|
+
except cst.ParserSyntaxError:
|
|
165
|
+
return source_code, []
|
|
166
|
+
|
|
167
|
+
transformer = FastAPITransformer()
|
|
168
|
+
transformer.set_source(source_code)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
transformed_tree = tree.visit(transformer)
|
|
172
|
+
return transformed_tree.code, transformer.changes
|
|
173
|
+
except Exception:
|
|
174
|
+
return source_code, []
|