codeshift 0.3.2__py3-none-any.whl → 0.3.4__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.
Files changed (38) hide show
  1. codeshift/cli/commands/apply.py +24 -2
  2. codeshift/cli/commands/upgrade_all.py +4 -1
  3. codeshift/cli/package_manager.py +102 -0
  4. codeshift/knowledge/generator.py +11 -1
  5. codeshift/knowledge_base/libraries/aiohttp.yaml +186 -0
  6. codeshift/knowledge_base/libraries/attrs.yaml +181 -0
  7. codeshift/knowledge_base/libraries/celery.yaml +244 -0
  8. codeshift/knowledge_base/libraries/click.yaml +195 -0
  9. codeshift/knowledge_base/libraries/django.yaml +355 -0
  10. codeshift/knowledge_base/libraries/flask.yaml +270 -0
  11. codeshift/knowledge_base/libraries/httpx.yaml +183 -0
  12. codeshift/knowledge_base/libraries/marshmallow.yaml +238 -0
  13. codeshift/knowledge_base/libraries/numpy.yaml +429 -0
  14. codeshift/knowledge_base/libraries/pytest.yaml +192 -0
  15. codeshift/knowledge_base/libraries/sqlalchemy.yaml +2 -1
  16. codeshift/migrator/engine.py +60 -0
  17. codeshift/migrator/transforms/__init__.py +2 -0
  18. codeshift/migrator/transforms/aiohttp_transformer.py +608 -0
  19. codeshift/migrator/transforms/attrs_transformer.py +570 -0
  20. codeshift/migrator/transforms/celery_transformer.py +546 -0
  21. codeshift/migrator/transforms/click_transformer.py +526 -0
  22. codeshift/migrator/transforms/django_transformer.py +852 -0
  23. codeshift/migrator/transforms/fastapi_transformer.py +12 -7
  24. codeshift/migrator/transforms/flask_transformer.py +505 -0
  25. codeshift/migrator/transforms/httpx_transformer.py +419 -0
  26. codeshift/migrator/transforms/marshmallow_transformer.py +515 -0
  27. codeshift/migrator/transforms/numpy_transformer.py +413 -0
  28. codeshift/migrator/transforms/pydantic_v1_to_v2.py +53 -8
  29. codeshift/migrator/transforms/pytest_transformer.py +351 -0
  30. codeshift/migrator/transforms/requests_transformer.py +74 -1
  31. codeshift/migrator/transforms/sqlalchemy_transformer.py +692 -39
  32. codeshift/scanner/dependency_parser.py +1 -1
  33. {codeshift-0.3.2.dist-info → codeshift-0.3.4.dist-info}/METADATA +46 -4
  34. {codeshift-0.3.2.dist-info → codeshift-0.3.4.dist-info}/RECORD +38 -17
  35. {codeshift-0.3.2.dist-info → codeshift-0.3.4.dist-info}/WHEEL +0 -0
  36. {codeshift-0.3.2.dist-info → codeshift-0.3.4.dist-info}/entry_points.txt +0 -0
  37. {codeshift-0.3.2.dist-info → codeshift-0.3.4.dist-info}/licenses/LICENSE +0 -0
  38. {codeshift-0.3.2.dist-info → codeshift-0.3.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,419 @@
1
+ """HTTPX library transformation using LibCST.
2
+
3
+ Handles migrations from httpx 0.x to 0.28+ including:
4
+ - Timeout parameter renames (connect_timeout -> connect, etc.)
5
+ - proxies parameter removal (use proxy or mounts)
6
+ - app parameter removal (use WSGITransport/ASGITransport)
7
+ """
8
+
9
+ import libcst as cst
10
+
11
+ from codeshift.migrator.ast_transforms import BaseTransformer
12
+
13
+
14
+ class HTTPXTransformer(BaseTransformer):
15
+ """Transform HTTPX library code for version upgrades."""
16
+
17
+ # Timeout parameter mappings (old -> new)
18
+ TIMEOUT_PARAM_MAPPINGS = {
19
+ "connect_timeout": "connect",
20
+ "read_timeout": "read",
21
+ "write_timeout": "write",
22
+ "pool_timeout": "pool",
23
+ }
24
+
25
+ def __init__(self) -> None:
26
+ super().__init__()
27
+ self._needs_wsgi_transport_import = False
28
+ self._needs_asgi_transport_import = False
29
+ self._needs_http_transport_import = False
30
+ self._has_httpx_import = False
31
+
32
+ def leave_Call(self, original_node: cst.Call, updated_node: cst.Call) -> cst.BaseExpression:
33
+ """Transform HTTPX function calls."""
34
+ # Handle httpx.Timeout() calls
35
+ if self._is_timeout_call(updated_node):
36
+ return self._transform_timeout_call(updated_node)
37
+
38
+ # Handle httpx.Client() and httpx.AsyncClient() calls
39
+ if self._is_client_call(updated_node):
40
+ return self._transform_client_call(updated_node)
41
+
42
+ return updated_node
43
+
44
+ def _is_timeout_call(self, node: cst.Call) -> bool:
45
+ """Check if this is a Timeout() call."""
46
+ # Match httpx.Timeout(...) or Timeout(...)
47
+ if isinstance(node.func, cst.Attribute):
48
+ if (
49
+ isinstance(node.func.value, cst.Name)
50
+ and node.func.value.value == "httpx"
51
+ and node.func.attr.value == "Timeout"
52
+ ):
53
+ return True
54
+ elif isinstance(node.func, cst.Name):
55
+ if node.func.value == "Timeout":
56
+ return True
57
+ return False
58
+
59
+ def _is_client_call(self, node: cst.Call) -> bool:
60
+ """Check if this is a Client() or AsyncClient() call."""
61
+ client_names = {"Client", "AsyncClient"}
62
+
63
+ if isinstance(node.func, cst.Attribute):
64
+ if (
65
+ isinstance(node.func.value, cst.Name)
66
+ and node.func.value.value == "httpx"
67
+ and node.func.attr.value in client_names
68
+ ):
69
+ return True
70
+ elif isinstance(node.func, cst.Name):
71
+ if node.func.value in client_names:
72
+ return True
73
+ return False
74
+
75
+ def _get_client_type(self, node: cst.Call) -> str | None:
76
+ """Get the client type (Client or AsyncClient)."""
77
+ if isinstance(node.func, cst.Attribute):
78
+ return str(node.func.attr.value)
79
+ elif isinstance(node.func, cst.Name):
80
+ return str(node.func.value)
81
+ return None
82
+
83
+ def _transform_timeout_call(self, node: cst.Call) -> cst.Call:
84
+ """Transform Timeout() call to use new parameter names."""
85
+ new_args: list[cst.Arg] = []
86
+ changed = False
87
+
88
+ for arg in node.args:
89
+ if isinstance(arg.keyword, cst.Name):
90
+ keyword_name = arg.keyword.value
91
+
92
+ # Handle 'timeout' keyword -> positional argument
93
+ if keyword_name == "timeout":
94
+ # Convert timeout=X to just X as first positional argument
95
+ new_arg = cst.Arg(value=arg.value)
96
+ new_args.insert(0, new_arg)
97
+ changed = True
98
+ self.record_change(
99
+ description="Convert Timeout(timeout=...) to Timeout(...)",
100
+ line_number=1,
101
+ original="Timeout(timeout=...)",
102
+ replacement="Timeout(...)",
103
+ transform_name="timeout_keyword_to_positional",
104
+ )
105
+ continue
106
+
107
+ # Handle *_timeout -> shorter names
108
+ if keyword_name in self.TIMEOUT_PARAM_MAPPINGS:
109
+ new_keyword = self.TIMEOUT_PARAM_MAPPINGS[keyword_name]
110
+ new_arg = arg.with_changes(keyword=cst.Name(new_keyword))
111
+ new_args.append(new_arg)
112
+ changed = True
113
+ self.record_change(
114
+ description=f"Rename Timeout({keyword_name}=...) to Timeout({new_keyword}=...)",
115
+ line_number=1,
116
+ original=f"Timeout({keyword_name}=...)",
117
+ replacement=f"Timeout({new_keyword}=...)",
118
+ transform_name=f"timeout_{keyword_name}_to_{new_keyword}",
119
+ )
120
+ continue
121
+
122
+ new_args.append(arg)
123
+
124
+ if changed:
125
+ return node.with_changes(args=new_args)
126
+ return node
127
+
128
+ def _transform_client_call(self, node: cst.Call) -> cst.Call:
129
+ """Transform Client() or AsyncClient() calls."""
130
+ client_type = self._get_client_type(node)
131
+ new_args = []
132
+ changed = False
133
+
134
+ for arg in node.args:
135
+ if isinstance(arg.keyword, cst.Name):
136
+ keyword_name = arg.keyword.value
137
+
138
+ # Handle proxies parameter -> proxy (for simple single-value cases)
139
+ if keyword_name == "proxies":
140
+ transformed_arg = self._transform_proxies_arg(arg, client_type)
141
+ if transformed_arg is not None:
142
+ new_args.append(transformed_arg)
143
+ changed = True
144
+ continue
145
+ # If transform returned None, we'll skip this arg (it was recorded as needing manual fix)
146
+
147
+ # Handle app parameter -> transport
148
+ if keyword_name == "app":
149
+ transformed_arg = self._transform_app_arg(arg, client_type)
150
+ new_args.append(transformed_arg)
151
+ changed = True
152
+ continue
153
+
154
+ new_args.append(arg)
155
+
156
+ if changed:
157
+ return node.with_changes(args=new_args)
158
+ return node
159
+
160
+ def _transform_proxies_arg(self, arg: cst.Arg, client_type: str | None) -> cst.Arg | None:
161
+ """Transform proxies=... argument.
162
+
163
+ For simple string values, convert to proxy=...
164
+ For dict values, this is more complex and may need mounts.
165
+ """
166
+ # Check if it's a simple string value
167
+ if isinstance(arg.value, cst.SimpleString):
168
+ # Simple case: proxies="http://..." -> proxy="http://..."
169
+ new_arg = arg.with_changes(keyword=cst.Name("proxy"))
170
+ self.record_change(
171
+ description=f"Convert {client_type or 'Client'}(proxies=...) to {client_type or 'Client'}(proxy=...)",
172
+ line_number=1,
173
+ original=f"{client_type or 'Client'}(proxies=...)",
174
+ replacement=f"{client_type or 'Client'}(proxy=...)",
175
+ transform_name="client_proxies_to_proxy",
176
+ )
177
+ return new_arg
178
+
179
+ # Check if it's a variable reference (Name)
180
+ if isinstance(arg.value, cst.Name):
181
+ # Variable reference - convert to proxy but note it may need review
182
+ new_arg = arg.with_changes(keyword=cst.Name("proxy"))
183
+ self.record_change(
184
+ description=f"Convert {client_type or 'Client'}(proxies=...) to {client_type or 'Client'}(proxy=...)",
185
+ line_number=1,
186
+ original=f"{client_type or 'Client'}(proxies=...)",
187
+ replacement=f"{client_type or 'Client'}(proxy=...)",
188
+ transform_name="client_proxies_to_proxy",
189
+ confidence=0.8,
190
+ notes="If proxies was a dict, may need to use mounts with HTTPTransport instead",
191
+ )
192
+ return new_arg
193
+
194
+ # For dict values, record that manual migration is needed
195
+ if isinstance(arg.value, cst.Dict):
196
+ self.record_change(
197
+ description=f"Manual migration needed: {client_type or 'Client'}(proxies={{...}}) requires mounts with HTTPTransport",
198
+ line_number=1,
199
+ original=f"{client_type or 'Client'}(proxies={{...}})",
200
+ replacement=f"{client_type or 'Client'}(mounts={{...}})",
201
+ transform_name="client_proxies_dict_to_mounts",
202
+ confidence=0.5,
203
+ notes="Dict-based proxies must be converted to mounts with HTTPTransport. Example: mounts={'http://': httpx.HTTPTransport(proxy='...')}",
204
+ )
205
+ # Return the original arg - we can't auto-transform dicts safely
206
+ return arg
207
+
208
+ # For other cases, convert to proxy and note uncertainty
209
+ new_arg = arg.with_changes(keyword=cst.Name("proxy"))
210
+ self.record_change(
211
+ description=f"Convert {client_type or 'Client'}(proxies=...) to {client_type or 'Client'}(proxy=...)",
212
+ line_number=1,
213
+ original=f"{client_type or 'Client'}(proxies=...)",
214
+ replacement=f"{client_type or 'Client'}(proxy=...)",
215
+ transform_name="client_proxies_to_proxy",
216
+ confidence=0.7,
217
+ notes="Review this change - if original value was a dict, use mounts instead",
218
+ )
219
+ return new_arg
220
+
221
+ def _transform_app_arg(self, arg: cst.Arg, client_type: str | None) -> cst.Arg:
222
+ """Transform app=... argument to transport=WSGITransport/ASGITransport(app=...)."""
223
+ is_async = client_type == "AsyncClient"
224
+ transport_name = "ASGITransport" if is_async else "WSGITransport"
225
+
226
+ if is_async:
227
+ self._needs_asgi_transport_import = True
228
+ else:
229
+ self._needs_wsgi_transport_import = True
230
+
231
+ # Create the transport call: httpx.WSGITransport(app=...) or httpx.ASGITransport(app=...)
232
+ transport_call = cst.Call(
233
+ func=cst.Attribute(
234
+ value=cst.Name("httpx"),
235
+ attr=cst.Name(transport_name),
236
+ ),
237
+ args=[
238
+ cst.Arg(
239
+ keyword=cst.Name("app"),
240
+ value=arg.value,
241
+ equal=cst.AssignEqual(
242
+ whitespace_before=cst.SimpleWhitespace(""),
243
+ whitespace_after=cst.SimpleWhitespace(""),
244
+ ),
245
+ )
246
+ ],
247
+ )
248
+
249
+ # Create transport=... argument
250
+ new_arg = cst.Arg(
251
+ keyword=cst.Name("transport"),
252
+ value=transport_call,
253
+ equal=cst.AssignEqual(
254
+ whitespace_before=cst.SimpleWhitespace(""),
255
+ whitespace_after=cst.SimpleWhitespace(""),
256
+ ),
257
+ )
258
+
259
+ self.record_change(
260
+ description=f"Convert {client_type or 'Client'}(app=...) to {client_type or 'Client'}(transport=httpx.{transport_name}(app=...))",
261
+ line_number=1,
262
+ original=f"{client_type or 'Client'}(app=...)",
263
+ replacement=f"{client_type or 'Client'}(transport=httpx.{transport_name}(app=...))",
264
+ transform_name=f"{'async_' if is_async else ''}client_app_to_{'asgi' if is_async else 'wsgi'}_transport",
265
+ )
266
+
267
+ return new_arg
268
+
269
+ def visit_ImportFrom(self, node: cst.ImportFrom) -> bool:
270
+ """Track httpx imports."""
271
+ if node.module is not None:
272
+ module_name = self._get_module_name(node.module)
273
+ if module_name == "httpx":
274
+ self._has_httpx_import = True
275
+ return True
276
+
277
+ def visit_Import(self, node: cst.Import) -> bool:
278
+ """Track httpx imports."""
279
+ if isinstance(node.names, cst.ImportStar):
280
+ return True
281
+ for name in node.names:
282
+ if isinstance(name, cst.ImportAlias):
283
+ if isinstance(name.name, cst.Name) and name.name.value == "httpx":
284
+ self._has_httpx_import = True
285
+ return True
286
+
287
+ def _get_module_name(self, module: cst.BaseExpression) -> str:
288
+ """Get the full module name from a Name or Attribute node."""
289
+ if isinstance(module, cst.Name):
290
+ return str(module.value)
291
+ elif isinstance(module, cst.Attribute):
292
+ return f"{self._get_module_name(module.value)}.{module.attr.value}"
293
+ return ""
294
+
295
+
296
+ class HTTPXImportTransformer(BaseTransformer):
297
+ """Handle import updates for HTTPX transformations."""
298
+
299
+ def __init__(
300
+ self,
301
+ needs_wsgi_transport: bool = False,
302
+ needs_asgi_transport: bool = False,
303
+ needs_http_transport: bool = False,
304
+ ) -> None:
305
+ super().__init__()
306
+ self.needs_wsgi_transport = needs_wsgi_transport
307
+ self.needs_asgi_transport = needs_asgi_transport
308
+ self.needs_http_transport = needs_http_transport
309
+ self._has_wsgi_transport = False
310
+ self._has_asgi_transport = False
311
+ self._has_http_transport = False
312
+
313
+ def visit_ImportFrom(self, node: cst.ImportFrom) -> bool:
314
+ """Check existing httpx imports."""
315
+ if node.module is None:
316
+ return True
317
+
318
+ module_name = self._get_module_name(node.module)
319
+ if module_name == "httpx":
320
+ if not isinstance(node.names, cst.ImportStar):
321
+ for name in node.names:
322
+ if isinstance(name, cst.ImportAlias):
323
+ imported = self._get_name_value(name.name)
324
+ if imported == "WSGITransport":
325
+ self._has_wsgi_transport = True
326
+ elif imported == "ASGITransport":
327
+ self._has_asgi_transport = True
328
+ elif imported == "HTTPTransport":
329
+ self._has_http_transport = True
330
+ return True
331
+
332
+ def leave_ImportFrom(
333
+ self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom
334
+ ) -> cst.ImportFrom:
335
+ """Add missing transport imports to httpx import statement."""
336
+ if updated_node.module is None:
337
+ return updated_node
338
+
339
+ module_name = self._get_module_name(updated_node.module)
340
+ if module_name != "httpx":
341
+ return updated_node
342
+
343
+ if isinstance(updated_node.names, cst.ImportStar):
344
+ return updated_node
345
+
346
+ new_names = list(updated_node.names)
347
+ changed = False
348
+
349
+ if self.needs_wsgi_transport and not self._has_wsgi_transport:
350
+ new_names.append(cst.ImportAlias(name=cst.Name("WSGITransport")))
351
+ self._has_wsgi_transport = True
352
+ changed = True
353
+
354
+ if self.needs_asgi_transport and not self._has_asgi_transport:
355
+ new_names.append(cst.ImportAlias(name=cst.Name("ASGITransport")))
356
+ self._has_asgi_transport = True
357
+ changed = True
358
+
359
+ if self.needs_http_transport and not self._has_http_transport:
360
+ new_names.append(cst.ImportAlias(name=cst.Name("HTTPTransport")))
361
+ self._has_http_transport = True
362
+ changed = True
363
+
364
+ if changed:
365
+ return updated_node.with_changes(names=new_names)
366
+ return updated_node
367
+
368
+ def _get_module_name(self, module: cst.BaseExpression) -> str:
369
+ """Get the full module name from a Name or Attribute node."""
370
+ if isinstance(module, cst.Name):
371
+ return str(module.value)
372
+ elif isinstance(module, cst.Attribute):
373
+ return f"{self._get_module_name(module.value)}.{module.attr.value}"
374
+ return ""
375
+
376
+ def _get_name_value(self, node: cst.BaseExpression) -> str | None:
377
+ """Extract the string value from a Name node."""
378
+ if isinstance(node, cst.Name):
379
+ return str(node.value)
380
+ return None
381
+
382
+
383
+ def transform_httpx(source_code: str) -> tuple[str, list]:
384
+ """Transform HTTPX library code.
385
+
386
+ Args:
387
+ source_code: The source code to transform
388
+
389
+ Returns:
390
+ Tuple of (transformed_code, list of changes)
391
+ """
392
+ try:
393
+ tree = cst.parse_module(source_code)
394
+ except cst.ParserSyntaxError:
395
+ return source_code, []
396
+
397
+ # First pass: main transformations
398
+ transformer = HTTPXTransformer()
399
+ transformer.set_source(source_code)
400
+
401
+ try:
402
+ transformed_tree = tree.visit(transformer)
403
+
404
+ # Second pass: add missing imports if needed
405
+ if (
406
+ transformer._needs_wsgi_transport_import
407
+ or transformer._needs_asgi_transport_import
408
+ or transformer._needs_http_transport_import
409
+ ):
410
+ import_transformer = HTTPXImportTransformer(
411
+ needs_wsgi_transport=transformer._needs_wsgi_transport_import,
412
+ needs_asgi_transport=transformer._needs_asgi_transport_import,
413
+ needs_http_transport=transformer._needs_http_transport_import,
414
+ )
415
+ transformed_tree = transformed_tree.visit(import_transformer)
416
+
417
+ return transformed_tree.code, transformer.changes
418
+ except Exception:
419
+ return source_code, []