codeshift 0.3.3__py3-none-any.whl → 0.3.5__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.5.dist-info}/METADATA +46 -4
- {codeshift-0.3.3.dist-info → codeshift-0.3.5.dist-info}/RECORD +36 -15
- {codeshift-0.3.3.dist-info → codeshift-0.3.5.dist-info}/WHEEL +0 -0
- {codeshift-0.3.3.dist-info → codeshift-0.3.5.dist-info}/entry_points.txt +0 -0
- {codeshift-0.3.3.dist-info → codeshift-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {codeshift-0.3.3.dist-info → codeshift-0.3.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""attrs 21.x to 23.x+ transformation using LibCST.
|
|
2
|
+
|
|
3
|
+
Transforms legacy attr namespace to modern attrs namespace:
|
|
4
|
+
- @attr.s -> @attrs.define
|
|
5
|
+
- @attr.attrs -> @attrs.define
|
|
6
|
+
- @attr.s(frozen=True) -> @attrs.frozen
|
|
7
|
+
- attr.ib() -> attrs.field()
|
|
8
|
+
- attr.attrib() -> attrs.field()
|
|
9
|
+
- attr.Factory -> attrs.Factory
|
|
10
|
+
- attr.asdict/astuple/fields/has/evolve -> attrs.*
|
|
11
|
+
- attr.validators.* -> attrs.validators.*
|
|
12
|
+
- attr.converters.* -> attrs.converters.*
|
|
13
|
+
- cmp parameter -> eq and order parameters
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import libcst as cst
|
|
17
|
+
|
|
18
|
+
from codeshift.migrator.ast_transforms import BaseTransformer
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AttrsTransformer(BaseTransformer):
|
|
22
|
+
"""Transform attrs 21.x code to 23.x+."""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self._needs_attrs_import = False
|
|
27
|
+
self._needs_define_import = False
|
|
28
|
+
self._needs_frozen_import = False
|
|
29
|
+
self._needs_field_import = False
|
|
30
|
+
self._needs_factory_import = False
|
|
31
|
+
self._has_attr_import = False
|
|
32
|
+
self._has_attrs_import = False
|
|
33
|
+
|
|
34
|
+
def visit_Import(self, node: cst.Import) -> bool:
|
|
35
|
+
"""Track import attr statements."""
|
|
36
|
+
if isinstance(node.names, cst.ImportStar):
|
|
37
|
+
return True
|
|
38
|
+
for name in node.names:
|
|
39
|
+
if isinstance(name, cst.ImportAlias):
|
|
40
|
+
if isinstance(name.name, cst.Name):
|
|
41
|
+
if name.name.value == "attr":
|
|
42
|
+
self._has_attr_import = True
|
|
43
|
+
elif name.name.value == "attrs":
|
|
44
|
+
self._has_attrs_import = True
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
def visit_ImportFrom(self, node: cst.ImportFrom) -> bool:
|
|
48
|
+
"""Track from attr/attrs import statements."""
|
|
49
|
+
if node.module is None:
|
|
50
|
+
return True
|
|
51
|
+
module_name = self._get_module_name(node.module)
|
|
52
|
+
if module_name == "attr" or module_name.startswith("attr."):
|
|
53
|
+
self._has_attr_import = True
|
|
54
|
+
elif module_name == "attrs" or module_name.startswith("attrs."):
|
|
55
|
+
self._has_attrs_import = True
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def leave_Import(self, original_node: cst.Import, updated_node: cst.Import) -> cst.Import:
|
|
59
|
+
"""Transform import attr to import attrs."""
|
|
60
|
+
if isinstance(updated_node.names, cst.ImportStar):
|
|
61
|
+
return updated_node
|
|
62
|
+
|
|
63
|
+
new_names = []
|
|
64
|
+
changed = False
|
|
65
|
+
|
|
66
|
+
for name in updated_node.names:
|
|
67
|
+
if isinstance(name, cst.ImportAlias):
|
|
68
|
+
if isinstance(name.name, cst.Name) and name.name.value == "attr":
|
|
69
|
+
# Transform import attr to import attrs
|
|
70
|
+
new_names.append(name.with_changes(name=cst.Name("attrs")))
|
|
71
|
+
changed = True
|
|
72
|
+
self.record_change(
|
|
73
|
+
description="Change 'import attr' to 'import attrs'",
|
|
74
|
+
line_number=1,
|
|
75
|
+
original="import attr",
|
|
76
|
+
replacement="import attrs",
|
|
77
|
+
transform_name="import_attr_to_attrs",
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
new_names.append(name)
|
|
81
|
+
else:
|
|
82
|
+
new_names.append(name)
|
|
83
|
+
|
|
84
|
+
if changed:
|
|
85
|
+
return updated_node.with_changes(names=new_names)
|
|
86
|
+
return updated_node
|
|
87
|
+
|
|
88
|
+
def leave_ImportFrom(
|
|
89
|
+
self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom
|
|
90
|
+
) -> cst.ImportFrom:
|
|
91
|
+
"""Transform from attr import ... to from attrs import ..."""
|
|
92
|
+
if updated_node.module is None:
|
|
93
|
+
return updated_node
|
|
94
|
+
|
|
95
|
+
module_name = self._get_module_name(updated_node.module)
|
|
96
|
+
|
|
97
|
+
# Transform from attr import ...
|
|
98
|
+
if module_name == "attr":
|
|
99
|
+
self.record_change(
|
|
100
|
+
description="Change 'from attr import' to 'from attrs import'",
|
|
101
|
+
line_number=1,
|
|
102
|
+
original="from attr import ...",
|
|
103
|
+
replacement="from attrs import ...",
|
|
104
|
+
transform_name="from_attr_to_attrs",
|
|
105
|
+
)
|
|
106
|
+
# Also transform imported names
|
|
107
|
+
names = updated_node.names
|
|
108
|
+
if not isinstance(names, cst.ImportStar):
|
|
109
|
+
names = tuple(names)
|
|
110
|
+
new_names = self._transform_import_names(names)
|
|
111
|
+
return updated_node.with_changes(module=cst.Name("attrs"), names=new_names)
|
|
112
|
+
|
|
113
|
+
# Transform from attr.validators import ...
|
|
114
|
+
if module_name == "attr.validators":
|
|
115
|
+
self.record_change(
|
|
116
|
+
description="Change 'from attr.validators' to 'from attrs.validators'",
|
|
117
|
+
line_number=1,
|
|
118
|
+
original="from attr.validators import ...",
|
|
119
|
+
replacement="from attrs.validators import ...",
|
|
120
|
+
transform_name="attr_validators_to_attrs_validators",
|
|
121
|
+
)
|
|
122
|
+
return updated_node.with_changes(
|
|
123
|
+
module=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("validators"))
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Transform from attr.converters import ...
|
|
127
|
+
if module_name == "attr.converters":
|
|
128
|
+
self.record_change(
|
|
129
|
+
description="Change 'from attr.converters' to 'from attrs.converters'",
|
|
130
|
+
line_number=1,
|
|
131
|
+
original="from attr.converters import ...",
|
|
132
|
+
replacement="from attrs.converters import ...",
|
|
133
|
+
transform_name="attr_converters_to_attrs_converters",
|
|
134
|
+
)
|
|
135
|
+
return updated_node.with_changes(
|
|
136
|
+
module=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("converters"))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return updated_node
|
|
140
|
+
|
|
141
|
+
def _transform_import_names(
|
|
142
|
+
self, names: cst.ImportStar | tuple[cst.ImportAlias, ...]
|
|
143
|
+
) -> cst.ImportStar | list[cst.ImportAlias]:
|
|
144
|
+
"""Transform imported names from attr to attrs naming conventions."""
|
|
145
|
+
if isinstance(names, cst.ImportStar):
|
|
146
|
+
return names
|
|
147
|
+
|
|
148
|
+
new_names = []
|
|
149
|
+
name_mappings = {
|
|
150
|
+
"s": "define",
|
|
151
|
+
"attrs": "define",
|
|
152
|
+
"ib": "field",
|
|
153
|
+
"attrib": "field",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for name in names:
|
|
157
|
+
if isinstance(name, cst.ImportAlias) and isinstance(name.name, cst.Name):
|
|
158
|
+
old_name = name.name.value
|
|
159
|
+
if old_name in name_mappings:
|
|
160
|
+
new_name = name_mappings[old_name]
|
|
161
|
+
new_names.append(name.with_changes(name=cst.Name(new_name)))
|
|
162
|
+
self.record_change(
|
|
163
|
+
description=f"Rename import '{old_name}' to '{new_name}'",
|
|
164
|
+
line_number=1,
|
|
165
|
+
original=old_name,
|
|
166
|
+
replacement=new_name,
|
|
167
|
+
transform_name=f"import_{old_name}_to_{new_name}",
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
new_names.append(name)
|
|
171
|
+
else:
|
|
172
|
+
new_names.append(name)
|
|
173
|
+
|
|
174
|
+
return new_names
|
|
175
|
+
|
|
176
|
+
def leave_Decorator(
|
|
177
|
+
self, original_node: cst.Decorator, updated_node: cst.Decorator
|
|
178
|
+
) -> cst.Decorator:
|
|
179
|
+
"""Transform decorators like @attr.s to @attrs.define."""
|
|
180
|
+
decorator = updated_node.decorator
|
|
181
|
+
|
|
182
|
+
# Handle @attr.s or @attr.attrs
|
|
183
|
+
if isinstance(decorator, cst.Attribute):
|
|
184
|
+
if isinstance(decorator.value, cst.Name) and decorator.value.value == "attr":
|
|
185
|
+
attr_name = decorator.attr.value
|
|
186
|
+
if attr_name in ("s", "attrs"):
|
|
187
|
+
self.record_change(
|
|
188
|
+
description=f"Transform @attr.{attr_name} to @attrs.define",
|
|
189
|
+
line_number=1,
|
|
190
|
+
original=f"@attr.{attr_name}",
|
|
191
|
+
replacement="@attrs.define",
|
|
192
|
+
transform_name="attr_s_to_attrs_define",
|
|
193
|
+
)
|
|
194
|
+
return updated_node.with_changes(
|
|
195
|
+
decorator=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("define"))
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Handle @attr.s(...) with arguments
|
|
199
|
+
if isinstance(decorator, cst.Call):
|
|
200
|
+
func = decorator.func
|
|
201
|
+
if isinstance(func, cst.Attribute):
|
|
202
|
+
if isinstance(func.value, cst.Name) and func.value.value == "attr":
|
|
203
|
+
attr_name = func.attr.value
|
|
204
|
+
if attr_name in ("s", "attrs"):
|
|
205
|
+
return self._transform_attr_s_call(updated_node, decorator)
|
|
206
|
+
|
|
207
|
+
return updated_node
|
|
208
|
+
|
|
209
|
+
def _transform_attr_s_call(
|
|
210
|
+
self, decorator_node: cst.Decorator, call: cst.Call
|
|
211
|
+
) -> cst.Decorator:
|
|
212
|
+
"""Transform @attr.s(...) to appropriate attrs decorator."""
|
|
213
|
+
# Check for frozen=True -> @attrs.frozen
|
|
214
|
+
# Check for auto_attribs=True, slots=True -> @attrs.define (these are defaults)
|
|
215
|
+
# Handle cmp parameter -> eq, order
|
|
216
|
+
|
|
217
|
+
has_frozen = False
|
|
218
|
+
new_args = []
|
|
219
|
+
removed_args = []
|
|
220
|
+
|
|
221
|
+
for arg in call.args:
|
|
222
|
+
if isinstance(arg.keyword, cst.Name):
|
|
223
|
+
keyword_name = arg.keyword.value
|
|
224
|
+
|
|
225
|
+
# frozen=True -> use @attrs.frozen instead
|
|
226
|
+
if keyword_name == "frozen":
|
|
227
|
+
if isinstance(arg.value, cst.Name) and arg.value.value == "True":
|
|
228
|
+
has_frozen = True
|
|
229
|
+
removed_args.append("frozen=True")
|
|
230
|
+
continue
|
|
231
|
+
else:
|
|
232
|
+
new_args.append(arg)
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# auto_attribs=True is default in @attrs.define, remove it
|
|
236
|
+
if keyword_name == "auto_attribs":
|
|
237
|
+
if isinstance(arg.value, cst.Name) and arg.value.value == "True":
|
|
238
|
+
removed_args.append("auto_attribs=True")
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
# slots=True is default in @attrs.define, remove it
|
|
242
|
+
if keyword_name == "slots":
|
|
243
|
+
if isinstance(arg.value, cst.Name) and arg.value.value == "True":
|
|
244
|
+
removed_args.append("slots=True")
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# cmp parameter -> eq and order
|
|
248
|
+
if keyword_name == "cmp":
|
|
249
|
+
eq_order_args = self._transform_cmp_arg(arg)
|
|
250
|
+
new_args.extend(eq_order_args)
|
|
251
|
+
self.record_change(
|
|
252
|
+
description="Transform cmp parameter to eq and order",
|
|
253
|
+
line_number=1,
|
|
254
|
+
original="cmp=...",
|
|
255
|
+
replacement="eq=..., order=...",
|
|
256
|
+
transform_name="cmp_to_eq_order",
|
|
257
|
+
)
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
new_args.append(arg)
|
|
261
|
+
else:
|
|
262
|
+
new_args.append(arg)
|
|
263
|
+
|
|
264
|
+
# Determine target decorator
|
|
265
|
+
if has_frozen:
|
|
266
|
+
target_decorator = "frozen"
|
|
267
|
+
self.record_change(
|
|
268
|
+
description="Transform @attr.s(frozen=True) to @attrs.frozen",
|
|
269
|
+
line_number=1,
|
|
270
|
+
original="@attr.s(frozen=True, ...)",
|
|
271
|
+
replacement="@attrs.frozen(...)",
|
|
272
|
+
transform_name="attr_s_frozen_to_attrs_frozen",
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
target_decorator = "define"
|
|
276
|
+
self.record_change(
|
|
277
|
+
description="Transform @attr.s(...) to @attrs.define(...)",
|
|
278
|
+
line_number=1,
|
|
279
|
+
original="@attr.s(...)",
|
|
280
|
+
replacement="@attrs.define(...)",
|
|
281
|
+
transform_name="attr_s_to_attrs_define",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Build new decorator
|
|
285
|
+
new_func = cst.Attribute(value=cst.Name("attrs"), attr=cst.Name(target_decorator))
|
|
286
|
+
|
|
287
|
+
if new_args:
|
|
288
|
+
# Fix trailing comma
|
|
289
|
+
if new_args:
|
|
290
|
+
last_arg = new_args[-1]
|
|
291
|
+
if last_arg.comma != cst.MaybeSentinel.DEFAULT:
|
|
292
|
+
new_args[-1] = last_arg.with_changes(comma=cst.MaybeSentinel.DEFAULT)
|
|
293
|
+
new_call = call.with_changes(func=new_func, args=new_args)
|
|
294
|
+
else:
|
|
295
|
+
# No args left, use simple attribute
|
|
296
|
+
return decorator_node.with_changes(decorator=new_func)
|
|
297
|
+
|
|
298
|
+
return decorator_node.with_changes(decorator=new_call)
|
|
299
|
+
|
|
300
|
+
def _transform_cmp_arg(self, arg: cst.Arg) -> list[cst.Arg]:
|
|
301
|
+
"""Transform cmp=X to eq=X, order=X."""
|
|
302
|
+
value = arg.value
|
|
303
|
+
return [
|
|
304
|
+
cst.Arg(
|
|
305
|
+
keyword=cst.Name("eq"),
|
|
306
|
+
value=value,
|
|
307
|
+
equal=cst.AssignEqual(
|
|
308
|
+
whitespace_before=cst.SimpleWhitespace(""),
|
|
309
|
+
whitespace_after=cst.SimpleWhitespace(""),
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
cst.Arg(
|
|
313
|
+
keyword=cst.Name("order"),
|
|
314
|
+
value=value,
|
|
315
|
+
equal=cst.AssignEqual(
|
|
316
|
+
whitespace_before=cst.SimpleWhitespace(""),
|
|
317
|
+
whitespace_after=cst.SimpleWhitespace(""),
|
|
318
|
+
),
|
|
319
|
+
),
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
def leave_Call(self, original_node: cst.Call, updated_node: cst.Call) -> cst.Call:
|
|
323
|
+
"""Transform function calls like attr.ib() to attrs.field()."""
|
|
324
|
+
func = updated_node.func
|
|
325
|
+
|
|
326
|
+
# Handle attr.ib() or attr.attrib()
|
|
327
|
+
if isinstance(func, cst.Attribute):
|
|
328
|
+
if isinstance(func.value, cst.Name) and func.value.value == "attr":
|
|
329
|
+
attr_name = func.attr.value
|
|
330
|
+
|
|
331
|
+
# attr.ib() / attr.attrib() -> attrs.field()
|
|
332
|
+
if attr_name in ("ib", "attrib"):
|
|
333
|
+
self.record_change(
|
|
334
|
+
description=f"Transform attr.{attr_name}() to attrs.field()",
|
|
335
|
+
line_number=1,
|
|
336
|
+
original=f"attr.{attr_name}(...)",
|
|
337
|
+
replacement="attrs.field(...)",
|
|
338
|
+
transform_name="attr_ib_to_attrs_field",
|
|
339
|
+
)
|
|
340
|
+
# Transform cmp parameter in field calls too
|
|
341
|
+
new_args = self._transform_field_args(tuple(updated_node.args))
|
|
342
|
+
return updated_node.with_changes(
|
|
343
|
+
func=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("field")),
|
|
344
|
+
args=new_args,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# attr.Factory -> attrs.Factory
|
|
348
|
+
if attr_name == "Factory":
|
|
349
|
+
self.record_change(
|
|
350
|
+
description="Transform attr.Factory to attrs.Factory",
|
|
351
|
+
line_number=1,
|
|
352
|
+
original="attr.Factory(...)",
|
|
353
|
+
replacement="attrs.Factory(...)",
|
|
354
|
+
transform_name="attr_factory_to_attrs_factory",
|
|
355
|
+
)
|
|
356
|
+
return updated_node.with_changes(
|
|
357
|
+
func=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("Factory"))
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# attr.asdict -> attrs.asdict
|
|
361
|
+
if attr_name == "asdict":
|
|
362
|
+
self.record_change(
|
|
363
|
+
description="Transform attr.asdict() to attrs.asdict()",
|
|
364
|
+
line_number=1,
|
|
365
|
+
original="attr.asdict(...)",
|
|
366
|
+
replacement="attrs.asdict(...)",
|
|
367
|
+
transform_name="attr_asdict_to_attrs_asdict",
|
|
368
|
+
)
|
|
369
|
+
return updated_node.with_changes(
|
|
370
|
+
func=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("asdict"))
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# attr.astuple -> attrs.astuple
|
|
374
|
+
if attr_name == "astuple":
|
|
375
|
+
self.record_change(
|
|
376
|
+
description="Transform attr.astuple() to attrs.astuple()",
|
|
377
|
+
line_number=1,
|
|
378
|
+
original="attr.astuple(...)",
|
|
379
|
+
replacement="attrs.astuple(...)",
|
|
380
|
+
transform_name="attr_astuple_to_attrs_astuple",
|
|
381
|
+
)
|
|
382
|
+
return updated_node.with_changes(
|
|
383
|
+
func=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("astuple"))
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# attr.fields -> attrs.fields
|
|
387
|
+
if attr_name == "fields":
|
|
388
|
+
self.record_change(
|
|
389
|
+
description="Transform attr.fields() to attrs.fields()",
|
|
390
|
+
line_number=1,
|
|
391
|
+
original="attr.fields(...)",
|
|
392
|
+
replacement="attrs.fields(...)",
|
|
393
|
+
transform_name="attr_fields_to_attrs_fields",
|
|
394
|
+
)
|
|
395
|
+
return updated_node.with_changes(
|
|
396
|
+
func=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("fields"))
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# attr.has -> attrs.has
|
|
400
|
+
if attr_name == "has":
|
|
401
|
+
self.record_change(
|
|
402
|
+
description="Transform attr.has() to attrs.has()",
|
|
403
|
+
line_number=1,
|
|
404
|
+
original="attr.has(...)",
|
|
405
|
+
replacement="attrs.has(...)",
|
|
406
|
+
transform_name="attr_has_to_attrs_has",
|
|
407
|
+
)
|
|
408
|
+
return updated_node.with_changes(
|
|
409
|
+
func=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("has"))
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# attr.evolve -> attrs.evolve
|
|
413
|
+
if attr_name == "evolve":
|
|
414
|
+
self.record_change(
|
|
415
|
+
description="Transform attr.evolve() to attrs.evolve()",
|
|
416
|
+
line_number=1,
|
|
417
|
+
original="attr.evolve(...)",
|
|
418
|
+
replacement="attrs.evolve(...)",
|
|
419
|
+
transform_name="attr_evolve_to_attrs_evolve",
|
|
420
|
+
)
|
|
421
|
+
return updated_node.with_changes(
|
|
422
|
+
func=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("evolve"))
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# attr.validate -> attrs.validate
|
|
426
|
+
if attr_name == "validate":
|
|
427
|
+
self.record_change(
|
|
428
|
+
description="Transform attr.validate() to attrs.validate()",
|
|
429
|
+
line_number=1,
|
|
430
|
+
original="attr.validate(...)",
|
|
431
|
+
replacement="attrs.validate(...)",
|
|
432
|
+
transform_name="attr_validate_to_attrs_validate",
|
|
433
|
+
)
|
|
434
|
+
return updated_node.with_changes(
|
|
435
|
+
func=cst.Attribute(value=cst.Name("attrs"), attr=cst.Name("validate"))
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Handle attr.validators.* calls
|
|
439
|
+
if isinstance(func, cst.Attribute):
|
|
440
|
+
if isinstance(func.value, cst.Attribute):
|
|
441
|
+
if (
|
|
442
|
+
isinstance(func.value.value, cst.Name)
|
|
443
|
+
and func.value.value.value == "attr"
|
|
444
|
+
and func.value.attr.value == "validators"
|
|
445
|
+
):
|
|
446
|
+
validator_name = func.attr.value
|
|
447
|
+
self.record_change(
|
|
448
|
+
description=f"Transform attr.validators.{validator_name}() to attrs.validators.{validator_name}()",
|
|
449
|
+
line_number=1,
|
|
450
|
+
original=f"attr.validators.{validator_name}(...)",
|
|
451
|
+
replacement=f"attrs.validators.{validator_name}(...)",
|
|
452
|
+
transform_name="attr_validators_to_attrs_validators",
|
|
453
|
+
)
|
|
454
|
+
return updated_node.with_changes(
|
|
455
|
+
func=cst.Attribute(
|
|
456
|
+
value=cst.Attribute(
|
|
457
|
+
value=cst.Name("attrs"), attr=cst.Name("validators")
|
|
458
|
+
),
|
|
459
|
+
attr=cst.Name(validator_name),
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Handle attr.converters.* calls
|
|
464
|
+
if isinstance(func, cst.Attribute):
|
|
465
|
+
if isinstance(func.value, cst.Attribute):
|
|
466
|
+
if (
|
|
467
|
+
isinstance(func.value.value, cst.Name)
|
|
468
|
+
and func.value.value.value == "attr"
|
|
469
|
+
and func.value.attr.value == "converters"
|
|
470
|
+
):
|
|
471
|
+
converter_name = func.attr.value
|
|
472
|
+
self.record_change(
|
|
473
|
+
description=f"Transform attr.converters.{converter_name}() to attrs.converters.{converter_name}()",
|
|
474
|
+
line_number=1,
|
|
475
|
+
original=f"attr.converters.{converter_name}(...)",
|
|
476
|
+
replacement=f"attrs.converters.{converter_name}(...)",
|
|
477
|
+
transform_name="attr_converters_to_attrs_converters",
|
|
478
|
+
)
|
|
479
|
+
return updated_node.with_changes(
|
|
480
|
+
func=cst.Attribute(
|
|
481
|
+
value=cst.Attribute(
|
|
482
|
+
value=cst.Name("attrs"), attr=cst.Name("converters")
|
|
483
|
+
),
|
|
484
|
+
attr=cst.Name(converter_name),
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
return updated_node
|
|
489
|
+
|
|
490
|
+
def _transform_field_args(self, args: tuple[cst.Arg, ...]) -> list[cst.Arg]:
|
|
491
|
+
"""Transform field arguments, handling cmp -> eq, order."""
|
|
492
|
+
new_args = []
|
|
493
|
+
for arg in args:
|
|
494
|
+
if isinstance(arg.keyword, cst.Name) and arg.keyword.value == "cmp":
|
|
495
|
+
# cmp=X -> eq=X, order=X
|
|
496
|
+
new_args.extend(self._transform_cmp_arg(arg))
|
|
497
|
+
self.record_change(
|
|
498
|
+
description="Transform cmp parameter to eq and order in field",
|
|
499
|
+
line_number=1,
|
|
500
|
+
original="cmp=...",
|
|
501
|
+
replacement="eq=..., order=...",
|
|
502
|
+
transform_name="cmp_to_eq_order",
|
|
503
|
+
)
|
|
504
|
+
else:
|
|
505
|
+
new_args.append(arg)
|
|
506
|
+
return new_args
|
|
507
|
+
|
|
508
|
+
def leave_Attribute(
|
|
509
|
+
self, original_node: cst.Attribute, updated_node: cst.Attribute
|
|
510
|
+
) -> cst.Attribute:
|
|
511
|
+
"""Transform attribute accesses like attr.validators to attrs.validators."""
|
|
512
|
+
# Handle attr.validators, attr.converters as module access (not calls)
|
|
513
|
+
if isinstance(updated_node.value, cst.Name) and updated_node.value.value == "attr":
|
|
514
|
+
attr_name = updated_node.attr.value
|
|
515
|
+
|
|
516
|
+
# attr.validators -> attrs.validators
|
|
517
|
+
if attr_name == "validators":
|
|
518
|
+
self.record_change(
|
|
519
|
+
description="Transform attr.validators to attrs.validators",
|
|
520
|
+
line_number=1,
|
|
521
|
+
original="attr.validators",
|
|
522
|
+
replacement="attrs.validators",
|
|
523
|
+
transform_name="attr_validators_to_attrs_validators",
|
|
524
|
+
)
|
|
525
|
+
return updated_node.with_changes(value=cst.Name("attrs"))
|
|
526
|
+
|
|
527
|
+
# attr.converters -> attrs.converters
|
|
528
|
+
if attr_name == "converters":
|
|
529
|
+
self.record_change(
|
|
530
|
+
description="Transform attr.converters to attrs.converters",
|
|
531
|
+
line_number=1,
|
|
532
|
+
original="attr.converters",
|
|
533
|
+
replacement="attrs.converters",
|
|
534
|
+
transform_name="attr_converters_to_attrs_converters",
|
|
535
|
+
)
|
|
536
|
+
return updated_node.with_changes(value=cst.Name("attrs"))
|
|
537
|
+
|
|
538
|
+
return updated_node
|
|
539
|
+
|
|
540
|
+
def _get_module_name(self, module: cst.BaseExpression) -> str:
|
|
541
|
+
"""Get the full module name from a Name or Attribute node."""
|
|
542
|
+
if isinstance(module, cst.Name):
|
|
543
|
+
return str(module.value)
|
|
544
|
+
elif isinstance(module, cst.Attribute):
|
|
545
|
+
return f"{self._get_module_name(module.value)}.{module.attr.value}"
|
|
546
|
+
return ""
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def transform_attrs(source_code: str) -> tuple[str, list]:
|
|
550
|
+
"""Transform attrs code from 21.x to 23.x+.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
source_code: The source code to transform
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
Tuple of (transformed_code, list of changes)
|
|
557
|
+
"""
|
|
558
|
+
try:
|
|
559
|
+
tree = cst.parse_module(source_code)
|
|
560
|
+
except cst.ParserSyntaxError:
|
|
561
|
+
return source_code, []
|
|
562
|
+
|
|
563
|
+
transformer = AttrsTransformer()
|
|
564
|
+
transformer.set_source(source_code)
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
transformed_tree = tree.visit(transformer)
|
|
568
|
+
return transformed_tree.code, transformer.changes
|
|
569
|
+
except Exception:
|
|
570
|
+
return source_code, []
|