codeshift 0.3.3__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.
- codeshift/cli/commands/apply.py +24 -2
- codeshift/cli/package_manager.py +102 -0
- codeshift/knowledge/generator.py +11 -1
- codeshift/knowledge_base/libraries/aiohttp.yaml +186 -0
- codeshift/knowledge_base/libraries/attrs.yaml +181 -0
- codeshift/knowledge_base/libraries/celery.yaml +244 -0
- codeshift/knowledge_base/libraries/click.yaml +195 -0
- codeshift/knowledge_base/libraries/django.yaml +355 -0
- codeshift/knowledge_base/libraries/flask.yaml +270 -0
- codeshift/knowledge_base/libraries/httpx.yaml +183 -0
- codeshift/knowledge_base/libraries/marshmallow.yaml +238 -0
- codeshift/knowledge_base/libraries/numpy.yaml +429 -0
- codeshift/knowledge_base/libraries/pytest.yaml +192 -0
- codeshift/knowledge_base/libraries/sqlalchemy.yaml +2 -1
- codeshift/migrator/engine.py +60 -0
- codeshift/migrator/transforms/__init__.py +2 -0
- codeshift/migrator/transforms/aiohttp_transformer.py +608 -0
- codeshift/migrator/transforms/attrs_transformer.py +570 -0
- codeshift/migrator/transforms/celery_transformer.py +546 -0
- codeshift/migrator/transforms/click_transformer.py +526 -0
- codeshift/migrator/transforms/django_transformer.py +852 -0
- codeshift/migrator/transforms/fastapi_transformer.py +12 -7
- codeshift/migrator/transforms/flask_transformer.py +505 -0
- codeshift/migrator/transforms/httpx_transformer.py +419 -0
- codeshift/migrator/transforms/marshmallow_transformer.py +515 -0
- codeshift/migrator/transforms/numpy_transformer.py +413 -0
- codeshift/migrator/transforms/pydantic_v1_to_v2.py +53 -8
- codeshift/migrator/transforms/pytest_transformer.py +351 -0
- codeshift/migrator/transforms/requests_transformer.py +74 -1
- codeshift/migrator/transforms/sqlalchemy_transformer.py +692 -39
- {codeshift-0.3.3.dist-info → codeshift-0.3.4.dist-info}/METADATA +46 -4
- {codeshift-0.3.3.dist-info → codeshift-0.3.4.dist-info}/RECORD +36 -15
- {codeshift-0.3.3.dist-info → codeshift-0.3.4.dist-info}/WHEEL +0 -0
- {codeshift-0.3.3.dist-info → codeshift-0.3.4.dist-info}/entry_points.txt +0 -0
- {codeshift-0.3.3.dist-info → codeshift-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {codeshift-0.3.3.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, []
|