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,352 @@
|
|
|
1
|
+
"""Code scanner for finding library imports and usage."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import libcst as cst
|
|
7
|
+
from libcst.metadata import MetadataWrapper, PositionProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ImportInfo:
|
|
12
|
+
"""Information about an import statement."""
|
|
13
|
+
|
|
14
|
+
module: str # e.g., "pydantic"
|
|
15
|
+
names: list[str] # e.g., ["BaseModel", "Field"]
|
|
16
|
+
alias: str | None = None # e.g., "pd" for "import pandas as pd"
|
|
17
|
+
file_path: Path = field(default_factory=Path)
|
|
18
|
+
line_number: int = 0
|
|
19
|
+
is_from_import: bool = False # True for "from x import y"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def full_import(self) -> str:
|
|
23
|
+
"""Get the full import string."""
|
|
24
|
+
if self.is_from_import:
|
|
25
|
+
names_str = ", ".join(self.names)
|
|
26
|
+
return f"from {self.module} import {names_str}"
|
|
27
|
+
if self.alias:
|
|
28
|
+
return f"import {self.module} as {self.alias}"
|
|
29
|
+
return f"import {self.module}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class UsageInfo:
|
|
34
|
+
"""Information about a symbol usage."""
|
|
35
|
+
|
|
36
|
+
symbol: str # e.g., "BaseModel", "user.dict()"
|
|
37
|
+
usage_type: str # "class", "function", "method_call", "attribute", "decorator"
|
|
38
|
+
file_path: Path = field(default_factory=Path)
|
|
39
|
+
line_number: int = 0
|
|
40
|
+
column: int = 0
|
|
41
|
+
context: str = "" # Surrounding code for context
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def location(self) -> str:
|
|
45
|
+
"""Get a human-readable location string."""
|
|
46
|
+
return f"{self.file_path}:{self.line_number}:{self.column}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ImportVisitor(cst.CSTVisitor):
|
|
50
|
+
"""Visitor to collect import statements for a specific library."""
|
|
51
|
+
|
|
52
|
+
METADATA_DEPENDENCIES = (PositionProvider,)
|
|
53
|
+
|
|
54
|
+
def __init__(self, target_library: str):
|
|
55
|
+
self.target_library = target_library
|
|
56
|
+
self.imports: list[ImportInfo] = []
|
|
57
|
+
self._imported_names: set[str] = set()
|
|
58
|
+
|
|
59
|
+
def visit_Import(self, node: cst.Import) -> None:
|
|
60
|
+
"""Visit import statements like 'import pydantic'."""
|
|
61
|
+
for name in node.names if isinstance(node.names, tuple) else []:
|
|
62
|
+
if isinstance(name, cst.ImportAlias):
|
|
63
|
+
module_name = self._get_name_value(name.name)
|
|
64
|
+
if module_name and module_name.startswith(self.target_library):
|
|
65
|
+
alias = None
|
|
66
|
+
if name.asname and isinstance(name.asname, cst.AsName):
|
|
67
|
+
alias = self._get_name_value(name.asname.name)
|
|
68
|
+
|
|
69
|
+
pos = self.get_metadata(PositionProvider, node)
|
|
70
|
+
self.imports.append(
|
|
71
|
+
ImportInfo(
|
|
72
|
+
module=module_name,
|
|
73
|
+
names=[module_name],
|
|
74
|
+
alias=alias,
|
|
75
|
+
line_number=pos.start.line if pos else 0,
|
|
76
|
+
is_from_import=False,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
self._imported_names.add(alias or module_name)
|
|
80
|
+
|
|
81
|
+
def visit_ImportFrom(self, node: cst.ImportFrom) -> None:
|
|
82
|
+
"""Visit from-import statements like 'from pydantic import BaseModel'."""
|
|
83
|
+
if node.module is None:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
module_name = self._get_name_value(node.module)
|
|
87
|
+
if not module_name or not module_name.startswith(self.target_library):
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
names = []
|
|
91
|
+
if isinstance(node.names, cst.ImportStar):
|
|
92
|
+
names = ["*"]
|
|
93
|
+
self._imported_names.add("*")
|
|
94
|
+
elif isinstance(node.names, tuple):
|
|
95
|
+
for name in node.names:
|
|
96
|
+
if isinstance(name, cst.ImportAlias):
|
|
97
|
+
imported_name = self._get_name_value(name.name)
|
|
98
|
+
if imported_name:
|
|
99
|
+
names.append(imported_name)
|
|
100
|
+
if name.asname and isinstance(name.asname, cst.AsName):
|
|
101
|
+
alias = self._get_name_value(name.asname.name)
|
|
102
|
+
self._imported_names.add(alias or imported_name)
|
|
103
|
+
else:
|
|
104
|
+
self._imported_names.add(imported_name)
|
|
105
|
+
|
|
106
|
+
if names:
|
|
107
|
+
pos = self.get_metadata(PositionProvider, node)
|
|
108
|
+
self.imports.append(
|
|
109
|
+
ImportInfo(
|
|
110
|
+
module=module_name,
|
|
111
|
+
names=names,
|
|
112
|
+
line_number=pos.start.line if pos else 0,
|
|
113
|
+
is_from_import=True,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def _get_name_value(self, node: cst.BaseExpression) -> str | None:
|
|
118
|
+
"""Extract the string value from a Name or Attribute node."""
|
|
119
|
+
if isinstance(node, cst.Name):
|
|
120
|
+
return str(node.value)
|
|
121
|
+
if isinstance(node, cst.Attribute):
|
|
122
|
+
base = self._get_name_value(node.value)
|
|
123
|
+
if base:
|
|
124
|
+
return f"{base}.{node.attr.value}"
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def imported_names(self) -> set[str]:
|
|
129
|
+
"""Get all imported names from the target library."""
|
|
130
|
+
return self._imported_names
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class UsageVisitor(cst.CSTVisitor):
|
|
134
|
+
"""Visitor to collect usage of imported symbols."""
|
|
135
|
+
|
|
136
|
+
METADATA_DEPENDENCIES = (PositionProvider,)
|
|
137
|
+
|
|
138
|
+
def __init__(self, imported_names: set[str], target_library: str):
|
|
139
|
+
self.imported_names = imported_names
|
|
140
|
+
self.target_library = target_library
|
|
141
|
+
self.usages: list[UsageInfo] = []
|
|
142
|
+
|
|
143
|
+
def visit_ClassDef(self, node: cst.ClassDef) -> None:
|
|
144
|
+
"""Visit class definitions to find BaseModel subclasses."""
|
|
145
|
+
for base in node.bases:
|
|
146
|
+
base_name = self._get_name_value(base.value)
|
|
147
|
+
if base_name and base_name in self.imported_names:
|
|
148
|
+
pos = self.get_metadata(PositionProvider, node)
|
|
149
|
+
self.usages.append(
|
|
150
|
+
UsageInfo(
|
|
151
|
+
symbol=base_name,
|
|
152
|
+
usage_type="class_inheritance",
|
|
153
|
+
line_number=pos.start.line if pos else 0,
|
|
154
|
+
context=f"class {node.name.value}({base_name})",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def visit_Decorator(self, node: cst.Decorator) -> None:
|
|
159
|
+
"""Visit decorators like @validator."""
|
|
160
|
+
decorator_name = self._get_name_value(node.decorator)
|
|
161
|
+
if not decorator_name:
|
|
162
|
+
# Handle decorator calls like @validator("field")
|
|
163
|
+
if isinstance(node.decorator, cst.Call):
|
|
164
|
+
decorator_name = self._get_name_value(node.decorator.func)
|
|
165
|
+
|
|
166
|
+
if decorator_name and decorator_name in self.imported_names:
|
|
167
|
+
pos = self.get_metadata(PositionProvider, node)
|
|
168
|
+
self.usages.append(
|
|
169
|
+
UsageInfo(
|
|
170
|
+
symbol=decorator_name,
|
|
171
|
+
usage_type="decorator",
|
|
172
|
+
line_number=pos.start.line if pos else 0,
|
|
173
|
+
context=f"@{decorator_name}",
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def visit_Call(self, node: cst.Call) -> None:
|
|
178
|
+
"""Visit function/method calls."""
|
|
179
|
+
# Handle method calls like .dict(), .json(), etc.
|
|
180
|
+
if isinstance(node.func, cst.Attribute):
|
|
181
|
+
method_name = node.func.attr.value
|
|
182
|
+
if method_name in {
|
|
183
|
+
"dict",
|
|
184
|
+
"json",
|
|
185
|
+
"schema",
|
|
186
|
+
"schema_json",
|
|
187
|
+
"parse_obj",
|
|
188
|
+
"parse_raw",
|
|
189
|
+
"parse_file",
|
|
190
|
+
"copy",
|
|
191
|
+
"update_forward_refs",
|
|
192
|
+
}:
|
|
193
|
+
pos = self.get_metadata(PositionProvider, node)
|
|
194
|
+
self.usages.append(
|
|
195
|
+
UsageInfo(
|
|
196
|
+
symbol=f".{method_name}()",
|
|
197
|
+
usage_type="method_call",
|
|
198
|
+
line_number=pos.start.line if pos else 0,
|
|
199
|
+
context=f".{method_name}()",
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Handle direct function calls like Field()
|
|
204
|
+
func_name = self._get_name_value(node.func)
|
|
205
|
+
if func_name and func_name in self.imported_names:
|
|
206
|
+
pos = self.get_metadata(PositionProvider, node)
|
|
207
|
+
self.usages.append(
|
|
208
|
+
UsageInfo(
|
|
209
|
+
symbol=func_name,
|
|
210
|
+
usage_type="function_call",
|
|
211
|
+
line_number=pos.start.line if pos else 0,
|
|
212
|
+
context=f"{func_name}()",
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def visit_Attribute(self, node: cst.Attribute) -> None:
|
|
217
|
+
"""Visit attribute access like __fields__."""
|
|
218
|
+
attr_name = node.attr.value
|
|
219
|
+
if attr_name in {"__fields__", "__validators__", "model_fields"}:
|
|
220
|
+
pos = self.get_metadata(PositionProvider, node)
|
|
221
|
+
self.usages.append(
|
|
222
|
+
UsageInfo(
|
|
223
|
+
symbol=attr_name,
|
|
224
|
+
usage_type="attribute",
|
|
225
|
+
line_number=pos.start.line if pos else 0,
|
|
226
|
+
context=attr_name,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _get_name_value(self, node: cst.BaseExpression) -> str | None:
|
|
231
|
+
"""Extract the string value from a Name or Attribute node."""
|
|
232
|
+
if isinstance(node, cst.Name):
|
|
233
|
+
return str(node.value)
|
|
234
|
+
if isinstance(node, cst.Attribute):
|
|
235
|
+
base = self._get_name_value(node.value)
|
|
236
|
+
if base:
|
|
237
|
+
return f"{base}.{node.attr.value}"
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass
|
|
242
|
+
class ScanResult:
|
|
243
|
+
"""Result of scanning a codebase."""
|
|
244
|
+
|
|
245
|
+
imports: list[ImportInfo] = field(default_factory=list)
|
|
246
|
+
usages: list[UsageInfo] = field(default_factory=list)
|
|
247
|
+
files_scanned: int = 0
|
|
248
|
+
files_with_imports: int = 0
|
|
249
|
+
errors: list[tuple[Path, str]] = field(default_factory=list)
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def has_library_usage(self) -> bool:
|
|
253
|
+
"""Check if the library is used in the codebase."""
|
|
254
|
+
return len(self.imports) > 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class CodeScanner:
|
|
258
|
+
"""Scanner for finding library usage in Python code."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, target_library: str, exclude_patterns: list[str] | None = None):
|
|
261
|
+
"""Initialize the scanner.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
target_library: Name of the library to scan for (e.g., "pydantic")
|
|
265
|
+
exclude_patterns: Glob patterns for paths to exclude
|
|
266
|
+
"""
|
|
267
|
+
self.target_library = target_library
|
|
268
|
+
self.exclude_patterns = exclude_patterns or []
|
|
269
|
+
|
|
270
|
+
def scan_file(self, file_path: Path) -> tuple[list[ImportInfo], list[UsageInfo]]:
|
|
271
|
+
"""Scan a single file for library usage.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
file_path: Path to the Python file
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Tuple of (imports, usages) found in the file
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
SyntaxError: If the file has invalid Python syntax
|
|
281
|
+
"""
|
|
282
|
+
source_code = file_path.read_text()
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
tree = cst.parse_module(source_code)
|
|
286
|
+
except cst.ParserSyntaxError as e:
|
|
287
|
+
raise SyntaxError(f"Invalid Python syntax in {file_path}: {e}") from e
|
|
288
|
+
|
|
289
|
+
wrapper = MetadataWrapper(tree)
|
|
290
|
+
|
|
291
|
+
# First pass: find imports
|
|
292
|
+
import_visitor = ImportVisitor(self.target_library)
|
|
293
|
+
wrapper.visit(import_visitor)
|
|
294
|
+
|
|
295
|
+
# Update file paths in imports
|
|
296
|
+
for imp in import_visitor.imports:
|
|
297
|
+
imp.file_path = file_path
|
|
298
|
+
|
|
299
|
+
# Second pass: find usages (only if we have imports)
|
|
300
|
+
usages = []
|
|
301
|
+
if import_visitor.imported_names:
|
|
302
|
+
usage_visitor = UsageVisitor(import_visitor.imported_names, self.target_library)
|
|
303
|
+
wrapper.visit(usage_visitor)
|
|
304
|
+
for usage in usage_visitor.usages:
|
|
305
|
+
usage.file_path = file_path
|
|
306
|
+
usages = usage_visitor.usages
|
|
307
|
+
|
|
308
|
+
return import_visitor.imports, usages
|
|
309
|
+
|
|
310
|
+
def scan_directory(self, directory: Path) -> ScanResult:
|
|
311
|
+
"""Scan a directory for library usage.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
directory: Path to the directory to scan
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
ScanResult containing all imports and usages found
|
|
318
|
+
"""
|
|
319
|
+
result = ScanResult()
|
|
320
|
+
|
|
321
|
+
# Find all Python files
|
|
322
|
+
python_files = list(directory.rglob("*.py"))
|
|
323
|
+
|
|
324
|
+
for file_path in python_files:
|
|
325
|
+
# Check exclude patterns
|
|
326
|
+
relative_path = str(file_path.relative_to(directory))
|
|
327
|
+
if self._should_exclude(relative_path):
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
result.files_scanned += 1
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
imports, usages = self.scan_file(file_path)
|
|
334
|
+
if imports:
|
|
335
|
+
result.files_with_imports += 1
|
|
336
|
+
result.imports.extend(imports)
|
|
337
|
+
result.usages.extend(usages)
|
|
338
|
+
except SyntaxError as e:
|
|
339
|
+
result.errors.append((file_path, str(e)))
|
|
340
|
+
except Exception as e:
|
|
341
|
+
result.errors.append((file_path, f"Error scanning file: {e}"))
|
|
342
|
+
|
|
343
|
+
return result
|
|
344
|
+
|
|
345
|
+
def _should_exclude(self, path: str) -> bool:
|
|
346
|
+
"""Check if a path should be excluded based on patterns."""
|
|
347
|
+
import fnmatch
|
|
348
|
+
|
|
349
|
+
for pattern in self.exclude_patterns:
|
|
350
|
+
if fnmatch.fnmatch(path, pattern):
|
|
351
|
+
return True
|
|
352
|
+
return False
|