cadwyn 3.15.10__py3-none-any.whl → 4.1.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.
Potentially problematic release.
This version of cadwyn might be problematic. Click here for more details.
- cadwyn/__init__.py +26 -8
- cadwyn/__main__.py +46 -90
- cadwyn/_asts.py +9 -133
- cadwyn/_importer.py +31 -0
- cadwyn/_render.py +152 -0
- cadwyn/_utils.py +7 -107
- cadwyn/applications.py +5 -34
- cadwyn/exceptions.py +11 -3
- cadwyn/middleware.py +4 -4
- cadwyn/route_generation.py +22 -450
- cadwyn/routing.py +2 -5
- cadwyn/schema_generation.py +946 -0
- cadwyn/structure/__init__.py +0 -2
- cadwyn/structure/schemas.py +50 -49
- cadwyn/structure/versions.py +24 -137
- {cadwyn-3.15.10.dist-info → cadwyn-4.1.0.dist-info}/METADATA +4 -5
- cadwyn-4.1.0.dist-info/RECORD +27 -0
- {cadwyn-3.15.10.dist-info → cadwyn-4.1.0.dist-info}/WHEEL +1 -1
- cadwyn/_compat.py +0 -151
- cadwyn/_package_utils.py +0 -45
- cadwyn/codegen/README.md +0 -10
- cadwyn/codegen/__init__.py +0 -10
- cadwyn/codegen/_common.py +0 -168
- cadwyn/codegen/_main.py +0 -279
- cadwyn/codegen/_plugins/__init__.py +0 -0
- cadwyn/codegen/_plugins/class_migrations.py +0 -423
- cadwyn/codegen/_plugins/class_rebuilding.py +0 -109
- cadwyn/codegen/_plugins/class_renaming.py +0 -49
- cadwyn/codegen/_plugins/import_auto_adding.py +0 -64
- cadwyn/codegen/_plugins/module_migrations.py +0 -15
- cadwyn/main.py +0 -11
- cadwyn/structure/modules.py +0 -39
- cadwyn-3.15.10.dist-info/RECORD +0 -38
- {cadwyn-3.15.10.dist-info → cadwyn-4.1.0.dist-info}/LICENSE +0 -0
- {cadwyn-3.15.10.dist-info → cadwyn-4.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
from collections.abc import Sequence
|
|
3
|
-
from typing import Annotated, Any, cast, get_args, get_origin
|
|
4
|
-
|
|
5
|
-
from typing_extensions import assert_never
|
|
6
|
-
|
|
7
|
-
from cadwyn._asts import add_keyword_to_call, delete_keyword_from_call, get_fancy_repr
|
|
8
|
-
from cadwyn._compat import (
|
|
9
|
-
PYDANTIC_V2,
|
|
10
|
-
FieldInfo,
|
|
11
|
-
PydanticFieldWrapper,
|
|
12
|
-
dict_of_empty_field_info,
|
|
13
|
-
is_constrained_type,
|
|
14
|
-
)
|
|
15
|
-
from cadwyn._package_utils import IdentifierPythonPath, get_cls_pythonpath
|
|
16
|
-
from cadwyn._utils import Sentinel
|
|
17
|
-
from cadwyn.codegen._common import GlobalCodegenContext, PydanticModelWrapper, _EnumWrapper
|
|
18
|
-
from cadwyn.exceptions import InvalidGenerationInstructionError
|
|
19
|
-
from cadwyn.structure.enums import AlterEnumSubInstruction, EnumDidntHaveMembersInstruction, EnumHadMembersInstruction
|
|
20
|
-
from cadwyn.structure.schemas import (
|
|
21
|
-
AlterSchemaSubInstruction,
|
|
22
|
-
FieldDidntExistInstruction,
|
|
23
|
-
FieldDidntHaveInstruction,
|
|
24
|
-
FieldExistedAsInstruction,
|
|
25
|
-
FieldHadInstruction,
|
|
26
|
-
SchemaHadInstruction,
|
|
27
|
-
ValidatorDidntExistInstruction,
|
|
28
|
-
ValidatorExistedInstruction,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def class_migration_plugin(context: GlobalCodegenContext) -> None:
|
|
33
|
-
for version_change in context.current_version.version_changes:
|
|
34
|
-
_apply_alter_schema_instructions(
|
|
35
|
-
context.schemas,
|
|
36
|
-
version_change.alter_schema_instructions,
|
|
37
|
-
version_change.__name__,
|
|
38
|
-
)
|
|
39
|
-
_apply_alter_enum_instructions(
|
|
40
|
-
context.enums,
|
|
41
|
-
version_change.alter_enum_instructions,
|
|
42
|
-
version_change.__name__,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _apply_alter_schema_instructions(
|
|
47
|
-
modified_schemas: dict[IdentifierPythonPath, PydanticModelWrapper],
|
|
48
|
-
alter_schema_instructions: Sequence[AlterSchemaSubInstruction | SchemaHadInstruction],
|
|
49
|
-
version_change_name: str,
|
|
50
|
-
):
|
|
51
|
-
for alter_schema_instruction in alter_schema_instructions:
|
|
52
|
-
schema = alter_schema_instruction.schema
|
|
53
|
-
schema_path = get_cls_pythonpath(schema)
|
|
54
|
-
schema_info = modified_schemas[schema_path]
|
|
55
|
-
if isinstance(alter_schema_instruction, FieldExistedAsInstruction):
|
|
56
|
-
_add_field_to_model(schema_info, modified_schemas, alter_schema_instruction, version_change_name)
|
|
57
|
-
elif isinstance(alter_schema_instruction, FieldHadInstruction | FieldDidntHaveInstruction):
|
|
58
|
-
_change_field_in_model(
|
|
59
|
-
schema_info,
|
|
60
|
-
modified_schemas,
|
|
61
|
-
alter_schema_instruction,
|
|
62
|
-
version_change_name,
|
|
63
|
-
)
|
|
64
|
-
elif isinstance(alter_schema_instruction, FieldDidntExistInstruction):
|
|
65
|
-
_delete_field_from_model(schema_info, alter_schema_instruction.name, version_change_name)
|
|
66
|
-
elif isinstance(alter_schema_instruction, ValidatorExistedInstruction):
|
|
67
|
-
validator_name = alter_schema_instruction.validator.__name__
|
|
68
|
-
schema_info.validators[validator_name] = alter_schema_instruction.validator_info
|
|
69
|
-
elif isinstance(alter_schema_instruction, ValidatorDidntExistInstruction):
|
|
70
|
-
if alter_schema_instruction.name not in schema_info.validators:
|
|
71
|
-
raise InvalidGenerationInstructionError(
|
|
72
|
-
f'You tried to delete a validator "{alter_schema_instruction.name}" from "{schema_info.name}" '
|
|
73
|
-
f'in "{version_change_name}" but it doesn\'t have such a validator.',
|
|
74
|
-
)
|
|
75
|
-
if schema_info.validators[alter_schema_instruction.name].is_deleted:
|
|
76
|
-
raise InvalidGenerationInstructionError(
|
|
77
|
-
f'You tried to delete a validator "{alter_schema_instruction.name}" from "{schema_info.name}" '
|
|
78
|
-
f'in "{version_change_name}" but it is already deleted.',
|
|
79
|
-
)
|
|
80
|
-
schema_info.validators[alter_schema_instruction.name].is_deleted = True
|
|
81
|
-
elif isinstance(alter_schema_instruction, SchemaHadInstruction):
|
|
82
|
-
_change_model(schema_info, alter_schema_instruction, version_change_name)
|
|
83
|
-
else:
|
|
84
|
-
assert_never(alter_schema_instruction)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _apply_alter_enum_instructions(
|
|
88
|
-
enums: dict[IdentifierPythonPath, _EnumWrapper],
|
|
89
|
-
alter_enum_instructions: Sequence[AlterEnumSubInstruction],
|
|
90
|
-
version_change_name: str,
|
|
91
|
-
):
|
|
92
|
-
for alter_enum_instruction in alter_enum_instructions:
|
|
93
|
-
enum = alter_enum_instruction.enum
|
|
94
|
-
enum_path = get_cls_pythonpath(enum)
|
|
95
|
-
enum = enums[enum_path]
|
|
96
|
-
if isinstance(alter_enum_instruction, EnumDidntHaveMembersInstruction):
|
|
97
|
-
for member in alter_enum_instruction.members:
|
|
98
|
-
if member not in enum.members:
|
|
99
|
-
raise InvalidGenerationInstructionError(
|
|
100
|
-
f'You tried to delete a member "{member}" from "{enum.cls.__name__}" '
|
|
101
|
-
f'in "{version_change_name}" but it doesn\'t have such a member.',
|
|
102
|
-
)
|
|
103
|
-
enum.members.pop(member)
|
|
104
|
-
elif isinstance(alter_enum_instruction, EnumHadMembersInstruction):
|
|
105
|
-
for member, member_value in alter_enum_instruction.members.items():
|
|
106
|
-
if member in enum.members and enum.members[member] == member_value:
|
|
107
|
-
raise InvalidGenerationInstructionError(
|
|
108
|
-
f'You tried to add a member "{member}" to "{enum.cls.__name__}" '
|
|
109
|
-
f'in "{version_change_name}" but there is already a member with that name and value.',
|
|
110
|
-
)
|
|
111
|
-
enum.members[member] = member_value
|
|
112
|
-
else:
|
|
113
|
-
assert_never(alter_enum_instruction)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _change_model(
|
|
117
|
-
model: PydanticModelWrapper,
|
|
118
|
-
alter_schema_instruction: SchemaHadInstruction,
|
|
119
|
-
version_change_name: str,
|
|
120
|
-
):
|
|
121
|
-
# We only handle names right now so we just go ahead and check
|
|
122
|
-
if alter_schema_instruction.name == model.name:
|
|
123
|
-
raise InvalidGenerationInstructionError(
|
|
124
|
-
f'You tried to change the name of "{model.name}" in "{version_change_name}" '
|
|
125
|
-
"but it already has the name you tried to assign.",
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
model.name = alter_schema_instruction.name
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _add_field_to_model(
|
|
132
|
-
model: PydanticModelWrapper,
|
|
133
|
-
schemas: "dict[IdentifierPythonPath, PydanticModelWrapper]",
|
|
134
|
-
alter_schema_instruction: FieldExistedAsInstruction,
|
|
135
|
-
version_change_name: str,
|
|
136
|
-
):
|
|
137
|
-
defined_fields = model._get_defined_fields_through_mro(schemas)
|
|
138
|
-
if alter_schema_instruction.name in defined_fields:
|
|
139
|
-
raise InvalidGenerationInstructionError(
|
|
140
|
-
f'You tried to add a field "{alter_schema_instruction.name}" to "{model.name}" '
|
|
141
|
-
f'in "{version_change_name}" but there is already a field with that name.',
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
fancy_type_repr = get_fancy_repr(alter_schema_instruction.type)
|
|
145
|
-
field = PydanticFieldWrapper(
|
|
146
|
-
annotation_ast=ast.parse(fancy_type_repr, mode="eval").body,
|
|
147
|
-
annotation=alter_schema_instruction.type,
|
|
148
|
-
init_model_field=alter_schema_instruction.field,
|
|
149
|
-
value_ast=None,
|
|
150
|
-
)
|
|
151
|
-
model.fields[alter_schema_instruction.name] = field
|
|
152
|
-
|
|
153
|
-
passed_field_attributes = field.passed_field_attributes
|
|
154
|
-
if passed_field_attributes:
|
|
155
|
-
field_call_ast = cast(ast.Call, ast.parse("Field()", mode="eval").body)
|
|
156
|
-
for attr_name, attr_value in passed_field_attributes.items():
|
|
157
|
-
add_keyword_to_call(attr_name, attr_value, field_call_ast)
|
|
158
|
-
field.value_ast = field_call_ast
|
|
159
|
-
model.annotations[alter_schema_instruction.name] = alter_schema_instruction.type
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _change_field_in_model(
|
|
163
|
-
model: PydanticModelWrapper,
|
|
164
|
-
schemas: "dict[IdentifierPythonPath, PydanticModelWrapper]",
|
|
165
|
-
alter_schema_instruction: FieldHadInstruction | FieldDidntHaveInstruction,
|
|
166
|
-
version_change_name: str,
|
|
167
|
-
):
|
|
168
|
-
defined_annotations = model._get_defined_annotations_through_mro(schemas)
|
|
169
|
-
defined_fields = model._get_defined_fields_through_mro(schemas)
|
|
170
|
-
if alter_schema_instruction.name not in defined_fields:
|
|
171
|
-
raise InvalidGenerationInstructionError(
|
|
172
|
-
f'You tried to change the field "{alter_schema_instruction.name}" from '
|
|
173
|
-
f'"{model.name}" in "{version_change_name}" but it doesn\'t have such a field.',
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
field = defined_fields[alter_schema_instruction.name]
|
|
177
|
-
model.fields[alter_schema_instruction.name] = field
|
|
178
|
-
model.annotations[alter_schema_instruction.name] = defined_annotations[alter_schema_instruction.name]
|
|
179
|
-
|
|
180
|
-
annotation_ast, field_call_ast, contype_is_definitely_used = _get_constraint_asts_and_field_call_ast(
|
|
181
|
-
schemas, model, alter_schema_instruction.name, field
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
constrained_type_annotation = None
|
|
185
|
-
|
|
186
|
-
if annotation_ast is not None:
|
|
187
|
-
field_type_is_annotated = True # I.e. typing.Annotated
|
|
188
|
-
# PydanticV2 changed field annotation handling so field.annotation lies to us
|
|
189
|
-
real_annotation = model._get_defined_annotations_through_mro(schemas)[alter_schema_instruction.name]
|
|
190
|
-
type_annotation = get_args(real_annotation)[0]
|
|
191
|
-
if is_constrained_type(type_annotation):
|
|
192
|
-
constrained_type_annotation = type_annotation
|
|
193
|
-
else:
|
|
194
|
-
field_type_is_annotated = False
|
|
195
|
-
type_annotation = field.annotation
|
|
196
|
-
if is_constrained_type(type_annotation):
|
|
197
|
-
constrained_type_annotation = type_annotation
|
|
198
|
-
annotation_ast = field.annotation_ast
|
|
199
|
-
if field_call_ast is None:
|
|
200
|
-
field_call_ast = field.value_ast
|
|
201
|
-
|
|
202
|
-
if isinstance(alter_schema_instruction, FieldHadInstruction):
|
|
203
|
-
# TODO: This naming sucks
|
|
204
|
-
_change_field(
|
|
205
|
-
model,
|
|
206
|
-
alter_schema_instruction,
|
|
207
|
-
version_change_name,
|
|
208
|
-
defined_annotations,
|
|
209
|
-
field,
|
|
210
|
-
annotation_ast,
|
|
211
|
-
field_call_ast,
|
|
212
|
-
type_annotation,
|
|
213
|
-
field_type_is_annotated,
|
|
214
|
-
constrained_type_annotation,
|
|
215
|
-
)
|
|
216
|
-
else:
|
|
217
|
-
_delete_field_attributes(
|
|
218
|
-
model,
|
|
219
|
-
alter_schema_instruction,
|
|
220
|
-
version_change_name,
|
|
221
|
-
field,
|
|
222
|
-
annotation_ast,
|
|
223
|
-
field_call_ast,
|
|
224
|
-
type_annotation,
|
|
225
|
-
constrained_type_annotation,
|
|
226
|
-
contype_is_definitely_used,
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _change_field( # noqa: C901
|
|
231
|
-
model: PydanticModelWrapper,
|
|
232
|
-
alter_schema_instruction: FieldHadInstruction,
|
|
233
|
-
version_change_name: str,
|
|
234
|
-
defined_annotations: dict[str, Any],
|
|
235
|
-
field: PydanticFieldWrapper,
|
|
236
|
-
annotation_ast: ast.expr | None,
|
|
237
|
-
field_call_ast: ast.expr | None,
|
|
238
|
-
type_annotation: Any,
|
|
239
|
-
field_type_is_annotated: bool,
|
|
240
|
-
constrained_type_annotation: Any | None,
|
|
241
|
-
):
|
|
242
|
-
if alter_schema_instruction.type is not Sentinel:
|
|
243
|
-
if field.annotation == alter_schema_instruction.type:
|
|
244
|
-
raise InvalidGenerationInstructionError(
|
|
245
|
-
f'You tried to change the type of field "{alter_schema_instruction.name}" to '
|
|
246
|
-
f'"{alter_schema_instruction.type}" from "{model.name}" in "{version_change_name}" '
|
|
247
|
-
f'but it already has type "{field.annotation}"',
|
|
248
|
-
)
|
|
249
|
-
field.annotation = alter_schema_instruction.type
|
|
250
|
-
model.annotations[alter_schema_instruction.name] = alter_schema_instruction.type
|
|
251
|
-
fancy_type_repr = get_fancy_repr(alter_schema_instruction.type)
|
|
252
|
-
field.annotation_ast = ast.parse(fancy_type_repr, mode="eval").body
|
|
253
|
-
|
|
254
|
-
if alter_schema_instruction.new_name is not Sentinel:
|
|
255
|
-
if alter_schema_instruction.new_name == alter_schema_instruction.name:
|
|
256
|
-
raise InvalidGenerationInstructionError(
|
|
257
|
-
f'You tried to change the name of field "{alter_schema_instruction.name}" '
|
|
258
|
-
f'from "{model.name}" in "{version_change_name}" '
|
|
259
|
-
"but it already has that name.",
|
|
260
|
-
)
|
|
261
|
-
model.fields[alter_schema_instruction.new_name] = model.fields.pop(alter_schema_instruction.name)
|
|
262
|
-
model.annotations[alter_schema_instruction.new_name] = model.annotations.pop(
|
|
263
|
-
alter_schema_instruction.name,
|
|
264
|
-
defined_annotations[alter_schema_instruction.name],
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
field_info = field.field_info
|
|
268
|
-
|
|
269
|
-
dict_of_field_info = {k: getattr(field_info, k) for k in field_info.__slots__}
|
|
270
|
-
if dict_of_field_info == dict_of_empty_field_info:
|
|
271
|
-
field_info = FieldInfo()
|
|
272
|
-
field.field_info = field_info
|
|
273
|
-
for attr_name in alter_schema_instruction.field_changes.__dataclass_fields__:
|
|
274
|
-
attr_value = getattr(alter_schema_instruction.field_changes, attr_name)
|
|
275
|
-
if attr_value is not Sentinel:
|
|
276
|
-
if field.passed_field_attributes.get(attr_name, Sentinel) == attr_value:
|
|
277
|
-
raise InvalidGenerationInstructionError(
|
|
278
|
-
f'You tried to change the attribute "{attr_name}" of field '
|
|
279
|
-
f'"{alter_schema_instruction.name}" '
|
|
280
|
-
f'from "{model.name}" to {attr_value!r} in "{version_change_name}" '
|
|
281
|
-
"but it already has that value.",
|
|
282
|
-
)
|
|
283
|
-
if constrained_type_annotation is not None and hasattr(constrained_type_annotation, attr_name):
|
|
284
|
-
_setattr_on_constrained_type(constrained_type_annotation, attr_name, attr_value)
|
|
285
|
-
if isinstance(annotation_ast, ast.Call):
|
|
286
|
-
add_keyword_to_call(attr_name, attr_value, annotation_ast)
|
|
287
|
-
elif not PYDANTIC_V2: # pragma: no branch
|
|
288
|
-
field.update_attribute(name=attr_name, value=attr_value)
|
|
289
|
-
if isinstance(field_call_ast, ast.Call): # pragma: no branch
|
|
290
|
-
add_keyword_to_call(attr_name, attr_value, field_call_ast)
|
|
291
|
-
|
|
292
|
-
else:
|
|
293
|
-
field.update_attribute(name=attr_name, value=attr_value)
|
|
294
|
-
if field_type_is_annotated and attr_name == "default":
|
|
295
|
-
field.value_ast = ast.parse(get_fancy_repr(attr_value), mode="eval").body
|
|
296
|
-
elif isinstance(field_call_ast, ast.Call):
|
|
297
|
-
add_keyword_to_call(attr_name, attr_value, field_call_ast)
|
|
298
|
-
elif field.value_ast is not None:
|
|
299
|
-
field_call_ast = cast(ast.Call, ast.parse("Field()", mode="eval").body)
|
|
300
|
-
add_keyword_to_call("default", field.value_ast, field_call_ast)
|
|
301
|
-
add_keyword_to_call(attr_name, attr_value, field_call_ast)
|
|
302
|
-
field.value_ast = field_call_ast
|
|
303
|
-
else:
|
|
304
|
-
field.value_ast = cast(ast.Call, ast.parse("Field()", mode="eval").body)
|
|
305
|
-
field_call_ast = field.value_ast
|
|
306
|
-
add_keyword_to_call(attr_name, attr_value, field_call_ast)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def _setattr_on_constrained_type(constrained_type_annotation: Any, attr_name: str, attr_value: Any) -> None:
|
|
310
|
-
setattr(constrained_type_annotation, attr_name, attr_value)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def _delete_field_attributes(
|
|
314
|
-
model: PydanticModelWrapper,
|
|
315
|
-
alter_schema_instruction: FieldDidntHaveInstruction,
|
|
316
|
-
version_change_name: str,
|
|
317
|
-
field: PydanticFieldWrapper,
|
|
318
|
-
type_annotation_ast: ast.expr | None,
|
|
319
|
-
field_call_ast: ast.expr | None,
|
|
320
|
-
type_annotation: Any,
|
|
321
|
-
constrained_type_annotation: Any,
|
|
322
|
-
contype_is_definitely_used: bool,
|
|
323
|
-
) -> None:
|
|
324
|
-
for attr_name in alter_schema_instruction.attributes:
|
|
325
|
-
if attr_name in field.passed_field_attributes:
|
|
326
|
-
field.delete_attribute(name=attr_name)
|
|
327
|
-
if isinstance(field_call_ast, ast.Call):
|
|
328
|
-
delete_keyword_from_call(attr_name, field_call_ast)
|
|
329
|
-
elif attr_name == "default": # pragma: no branch
|
|
330
|
-
field.value_ast = None
|
|
331
|
-
# In case annotation_ast is a conint/constr/etc. Notice how we do not support
|
|
332
|
-
# the same operation for **adding** constraints for simplicity.
|
|
333
|
-
elif (hasattr(constrained_type_annotation, attr_name)) or contype_is_definitely_used:
|
|
334
|
-
if hasattr(constrained_type_annotation, attr_name):
|
|
335
|
-
_setattr_on_constrained_type(constrained_type_annotation, attr_name, None)
|
|
336
|
-
if isinstance(type_annotation_ast, ast.Call): # pragma: no branch
|
|
337
|
-
delete_keyword_from_call(attr_name, type_annotation_ast)
|
|
338
|
-
else:
|
|
339
|
-
raise InvalidGenerationInstructionError(
|
|
340
|
-
f'You tried to delete the attribute "{attr_name}" of field "{alter_schema_instruction.name}" '
|
|
341
|
-
f'from "{model.name}" in "{version_change_name}" '
|
|
342
|
-
"but it already doesn't have that attribute.",
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
ContypeIsDefinitelyUsed = bool
|
|
347
|
-
CONTYPES = (
|
|
348
|
-
"conbytes",
|
|
349
|
-
"condate",
|
|
350
|
-
"condecimal",
|
|
351
|
-
"confloat",
|
|
352
|
-
"conint",
|
|
353
|
-
"conlist",
|
|
354
|
-
"conset",
|
|
355
|
-
"constr",
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def _get_constraint_asts_and_field_call_ast(
|
|
360
|
-
schemas: dict[IdentifierPythonPath, PydanticModelWrapper],
|
|
361
|
-
model: PydanticModelWrapper,
|
|
362
|
-
field_name: str,
|
|
363
|
-
field: PydanticFieldWrapper,
|
|
364
|
-
) -> tuple[ast.expr | None, ast.Call | None, ContypeIsDefinitelyUsed]:
|
|
365
|
-
"""If the field type is Annotated and contains "Field" """
|
|
366
|
-
# We return both annotation ast and field call ast because annotation might be a constrained type such as conint
|
|
367
|
-
# and therefore contain constraints that we might want to remove.
|
|
368
|
-
|
|
369
|
-
# ContypeIsDefinitely used is used to determine whether constr/conint/etc is used in Pydantic 2
|
|
370
|
-
# because pydantic 2 changes original type hints to make sure that conint/constr/etc do not appear in annotations.
|
|
371
|
-
|
|
372
|
-
real_annotation = model._get_defined_annotations_through_mro(schemas)[field_name]
|
|
373
|
-
# typing.Annotated scenario
|
|
374
|
-
if get_origin(real_annotation) == Annotated:
|
|
375
|
-
index_of_field_info = _find_index_of_field_info_in_annotated(real_annotation)
|
|
376
|
-
if not (isinstance(field.annotation_ast, ast.Subscript) and isinstance(field.annotation_ast.slice, ast.Tuple)):
|
|
377
|
-
return (field.annotation_ast, None, True)
|
|
378
|
-
|
|
379
|
-
unparsed_annotation = ast.unparse(field.annotation_ast.slice.elts[0])
|
|
380
|
-
contype_is_definitely_used = any(contype in unparsed_annotation for contype in CONTYPES)
|
|
381
|
-
|
|
382
|
-
# In pydantic 2, this means that in fact there is conint/constr/etc instead of an actual Annotated.
|
|
383
|
-
# Yes, pydantic 2 changes original type hints to make sure that conint/constr/etc do not appear in types.
|
|
384
|
-
|
|
385
|
-
if index_of_field_info is not None:
|
|
386
|
-
field_call_ast = field.annotation_ast.slice.elts[index_of_field_info]
|
|
387
|
-
|
|
388
|
-
return (field.annotation_ast.slice.elts[0], cast(ast.Call, field_call_ast), contype_is_definitely_used)
|
|
389
|
-
return (field.annotation_ast.slice.elts[0], None, contype_is_definitely_used)
|
|
390
|
-
return (None, None, False)
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def _find_index_of_field_info_in_annotated(real_annotation: Any):
|
|
394
|
-
# Pydantic turns `Annotated[conint(lt=2 + 5), Field(default=11), annotated_types.Gt(0)]` into:
|
|
395
|
-
# `Annotated[int, None, Interval(lt=7), None, FieldInfo(default=11), annotated_types.Gt(0)]`
|
|
396
|
-
# Why? No idea. Probably due to its weird handling of constrained types. So we gotta go from the last element
|
|
397
|
-
# because constrained types can only appear and mess up indexing in the first element.
|
|
398
|
-
for i, arg in enumerate(reversed(get_args(real_annotation)), start=1):
|
|
399
|
-
if isinstance(arg, FieldInfo):
|
|
400
|
-
return -i
|
|
401
|
-
return None
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def _delete_field_from_model(model: PydanticModelWrapper, field_name: str, version_change_name: str):
|
|
405
|
-
if field_name not in model.fields:
|
|
406
|
-
raise InvalidGenerationInstructionError(
|
|
407
|
-
f'You tried to delete a field "{field_name}" from "{model.name}" '
|
|
408
|
-
f'in "{version_change_name}" but it doesn\'t have such a field.',
|
|
409
|
-
)
|
|
410
|
-
model.fields.pop(field_name)
|
|
411
|
-
for validator_name, validator in model.validators.copy().items():
|
|
412
|
-
if validator.field_names is not None and field_name in validator.field_names:
|
|
413
|
-
validator.field_names.remove(field_name)
|
|
414
|
-
|
|
415
|
-
validator_decorator = cast(
|
|
416
|
-
ast.Call, validator.func_ast.decorator_list[validator.index_of_validator_decorator]
|
|
417
|
-
)
|
|
418
|
-
for arg in validator_decorator.args.copy():
|
|
419
|
-
if isinstance(arg, ast.Constant) and arg.value == field_name:
|
|
420
|
-
validator_decorator.args.remove(arg)
|
|
421
|
-
validator.func_ast.decorator_list[0]
|
|
422
|
-
if not validator.field_names:
|
|
423
|
-
model.validators[validator_name].is_deleted = True
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
import copy
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from cadwyn._asts import (
|
|
6
|
-
get_fancy_repr,
|
|
7
|
-
pop_docstring_from_cls_body,
|
|
8
|
-
)
|
|
9
|
-
from cadwyn._package_utils import IdentifierPythonPath, get_absolute_python_path_of_import
|
|
10
|
-
from cadwyn.codegen._common import CodegenContext, PydanticModelWrapper, _EnumWrapper
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ClassRebuildingPlugin:
|
|
14
|
-
node_type = ast.Module
|
|
15
|
-
|
|
16
|
-
@staticmethod
|
|
17
|
-
def __call__(node: ast.Module, context: CodegenContext) -> Any:
|
|
18
|
-
node.body = [_migrate_ast_node_to_another_version(n, context) for n in node.body]
|
|
19
|
-
return node
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _migrate_ast_node_to_another_version(
|
|
23
|
-
node: ast.stmt,
|
|
24
|
-
context: CodegenContext,
|
|
25
|
-
):
|
|
26
|
-
if isinstance(node, ast.ClassDef):
|
|
27
|
-
return _migrate_cls_to_another_version(node, context)
|
|
28
|
-
elif isinstance(node, ast.ImportFrom):
|
|
29
|
-
python_path = get_absolute_python_path_of_import(node, context.module_python_path)
|
|
30
|
-
node.names = [
|
|
31
|
-
name
|
|
32
|
-
if (name_path := f"{python_path}.{name.name}") not in context.schemas
|
|
33
|
-
else ast.alias(name=context.schemas[name_path].name, asname=name.asname)
|
|
34
|
-
for name in node.names
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
return node
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _migrate_cls_to_another_version(
|
|
41
|
-
cls_node: ast.ClassDef,
|
|
42
|
-
context: CodegenContext,
|
|
43
|
-
) -> ast.ClassDef:
|
|
44
|
-
cls_python_path = f"{context.module_python_path}.{cls_node.name}"
|
|
45
|
-
|
|
46
|
-
if cls_python_path in context.schemas:
|
|
47
|
-
cls_node = _modify_schema_cls(cls_node, context.schemas, cls_python_path)
|
|
48
|
-
elif cls_python_path in context.enums:
|
|
49
|
-
cls_node = _modify_enum_cls(cls_node, context.enums[cls_python_path])
|
|
50
|
-
|
|
51
|
-
if not cls_node.body:
|
|
52
|
-
cls_node.body = [ast.Pass()]
|
|
53
|
-
return cls_node
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _modify_enum_cls(cls_node: ast.ClassDef, enum: _EnumWrapper) -> ast.ClassDef:
|
|
57
|
-
new_body = [
|
|
58
|
-
ast.Assign(
|
|
59
|
-
targets=[ast.Name(member, ctx=ast.Store())],
|
|
60
|
-
value=ast.Name(get_fancy_repr(member_value)),
|
|
61
|
-
lineno=0,
|
|
62
|
-
)
|
|
63
|
-
for member, member_value in enum.members.items()
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
old_body = [n for n in cls_node.body if not isinstance(n, ast.AnnAssign | ast.Assign | ast.Pass | ast.Constant)]
|
|
67
|
-
docstring = pop_docstring_from_cls_body(old_body)
|
|
68
|
-
|
|
69
|
-
cls_node.body = docstring + new_body + old_body
|
|
70
|
-
return cls_node
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _modify_schema_cls(
|
|
74
|
-
cls_node: ast.ClassDef,
|
|
75
|
-
modified_schemas: dict[IdentifierPythonPath, PydanticModelWrapper],
|
|
76
|
-
cls_python_path: str,
|
|
77
|
-
) -> ast.ClassDef:
|
|
78
|
-
model_info = modified_schemas[cls_python_path]
|
|
79
|
-
# This is for possible schema renaming
|
|
80
|
-
cls_node.name = model_info.name
|
|
81
|
-
|
|
82
|
-
field_definitions = [
|
|
83
|
-
ast.AnnAssign( # pyright: ignore[reportCallIssue]
|
|
84
|
-
target=ast.Name(name, ctx=ast.Store()),
|
|
85
|
-
annotation=copy.deepcopy(field.annotation_ast), # pyright: ignore[reportArgumentType]
|
|
86
|
-
# We do this because next plugins **might** use a transformer which will edit the ast within the field
|
|
87
|
-
# and break rendering
|
|
88
|
-
value=copy.deepcopy(field.value_ast),
|
|
89
|
-
simple=1,
|
|
90
|
-
)
|
|
91
|
-
for name, field in model_info.fields.items()
|
|
92
|
-
]
|
|
93
|
-
validator_definitions = [
|
|
94
|
-
validator.func_ast for validator in model_info.validators.values() if not validator.is_deleted
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
old_body = [
|
|
98
|
-
n
|
|
99
|
-
for n in cls_node.body
|
|
100
|
-
if not (
|
|
101
|
-
isinstance(n, ast.AnnAssign | ast.Assign | ast.Pass | ast.Constant)
|
|
102
|
-
or (isinstance(n, ast.FunctionDef) and n.name in model_info.validators)
|
|
103
|
-
)
|
|
104
|
-
]
|
|
105
|
-
docstring = pop_docstring_from_cls_body(old_body)
|
|
106
|
-
cls_node.body = docstring + field_definitions + validator_definitions + old_body
|
|
107
|
-
if not cls_node.body:
|
|
108
|
-
cls_node.body = [ast.Pass()]
|
|
109
|
-
return cls_node
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from cadwyn._package_utils import IdentifierPythonPath
|
|
5
|
-
from cadwyn.codegen._common import CodegenContext, PydanticModelWrapper
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ClassRenamingPlugin:
|
|
9
|
-
node_type = ast.Module
|
|
10
|
-
|
|
11
|
-
@staticmethod
|
|
12
|
-
def __call__(
|
|
13
|
-
node: ast.Module,
|
|
14
|
-
context: CodegenContext,
|
|
15
|
-
):
|
|
16
|
-
return _AnnotationASTNodeTransformerWithSchemaRenaming(
|
|
17
|
-
context.schemas,
|
|
18
|
-
context.all_names_defined_on_toplevel_of_file,
|
|
19
|
-
context.module_python_path,
|
|
20
|
-
).visit(node)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class _AnnotationASTNodeTransformerWithSchemaRenaming(ast.NodeTransformer):
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
modified_schemas: dict[IdentifierPythonPath, PydanticModelWrapper],
|
|
27
|
-
all_names_in_module: dict[str, str],
|
|
28
|
-
module_python_path: str,
|
|
29
|
-
):
|
|
30
|
-
super().__init__()
|
|
31
|
-
self.modified_schemas = modified_schemas
|
|
32
|
-
self.module_python_path = module_python_path
|
|
33
|
-
self.all_names_in_module = all_names_in_module
|
|
34
|
-
|
|
35
|
-
def visit_AnnAssign(self, node: ast.AnnAssign): # noqa: N802
|
|
36
|
-
# Handles Pydantic annotations that are strings
|
|
37
|
-
if isinstance(node.annotation, ast.Constant) and isinstance(node.annotation.value, str):
|
|
38
|
-
altered = self.visit(ast.parse(node.annotation.value, mode="eval").body)
|
|
39
|
-
node.annotation.value = ast.unparse(altered)
|
|
40
|
-
return self.generic_visit(node)
|
|
41
|
-
|
|
42
|
-
def visit_Name(self, node: ast.Name) -> Any: # noqa: N802
|
|
43
|
-
return self._get_name(node, node.id)
|
|
44
|
-
|
|
45
|
-
def _get_name(self, node: ast.AST, name: str):
|
|
46
|
-
model_info = self.modified_schemas.get(f"{self.all_names_in_module.get(name, self.module_python_path)}.{name}")
|
|
47
|
-
if model_info is not None:
|
|
48
|
-
return ast.Name(model_info.name)
|
|
49
|
-
return node
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
|
|
3
|
-
from cadwyn._compat import PYDANTIC_V2
|
|
4
|
-
from cadwyn.codegen._common import CodegenContext
|
|
5
|
-
|
|
6
|
-
_extra_imports: list[tuple[str, str]] = [
|
|
7
|
-
("typing", "import typing"),
|
|
8
|
-
("pydantic", "import pydantic"),
|
|
9
|
-
("Any", "from typing import Any"),
|
|
10
|
-
("Annotated", "from typing import Annotated"),
|
|
11
|
-
("Field", "from pydantic import Field"),
|
|
12
|
-
("conbytes", "from pydantic import conbytes"),
|
|
13
|
-
("conlist", "from pydantic import conlist"),
|
|
14
|
-
("conset", "from pydantic import conset"),
|
|
15
|
-
("constr", "from pydantic import constr"),
|
|
16
|
-
("conint", "from pydantic import conint"),
|
|
17
|
-
("confloat", "from pydantic import confloat"),
|
|
18
|
-
("condecimal", "from pydantic import condecimal"),
|
|
19
|
-
("condate", "from pydantic import condate"),
|
|
20
|
-
("validator", "from pydantic import validator"),
|
|
21
|
-
("root_validator", "from pydantic import root_validator"),
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if PYDANTIC_V2:
|
|
26
|
-
_extra_imports.extend(
|
|
27
|
-
[
|
|
28
|
-
("confrozenset", "from pydantic import conset"),
|
|
29
|
-
("StringConstraints", "from pydantic import StringConstraints"),
|
|
30
|
-
("StrictBool", "from pydantic import StrictBool"),
|
|
31
|
-
("StrictBytes", "from pydantic import StrictBytes"),
|
|
32
|
-
("StrictFloat", "from pydantic import StrictFloat"),
|
|
33
|
-
("StrictInt", "from pydantic import StrictInt"),
|
|
34
|
-
("StrictStr", "from pydantic import StrictStr"),
|
|
35
|
-
("model_validator", "from pydantic import model_validator"),
|
|
36
|
-
("field_validator", "from pydantic import field_validator"),
|
|
37
|
-
],
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
_rendered_extra_imports = [(seek_str, ast.parse(imp).body[0]) for seek_str, imp in _extra_imports]
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class ImportAutoAddingPlugin:
|
|
44
|
-
node_type = ast.Module
|
|
45
|
-
|
|
46
|
-
@staticmethod
|
|
47
|
-
def __call__(node: ast.Module, context: CodegenContext):
|
|
48
|
-
source = ast.unparse(node)
|
|
49
|
-
extra_lib_imports = [
|
|
50
|
-
import_
|
|
51
|
-
for seek_str, import_ in _rendered_extra_imports
|
|
52
|
-
if seek_str in source and seek_str not in context.all_names_defined_on_toplevel_of_file
|
|
53
|
-
]
|
|
54
|
-
# We do this because when we import our module, we import not the package but
|
|
55
|
-
# the __init__.py file directly which produces this extra `.__init__` suffix in the name
|
|
56
|
-
module_name = context.template_module.__name__.removesuffix(".__init__")
|
|
57
|
-
if module_name in context.modules:
|
|
58
|
-
manual_imports = context.modules[module_name].extra_imports
|
|
59
|
-
else:
|
|
60
|
-
manual_imports = []
|
|
61
|
-
|
|
62
|
-
node.body = extra_lib_imports + manual_imports + node.body
|
|
63
|
-
|
|
64
|
-
return node
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from typing_extensions import assert_never
|
|
2
|
-
|
|
3
|
-
from cadwyn.codegen._common import GlobalCodegenContext
|
|
4
|
-
from cadwyn.structure.modules import AlterModuleInstruction
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def module_migration_plugin(context: GlobalCodegenContext):
|
|
8
|
-
for version_change in context.current_version.version_changes:
|
|
9
|
-
for alter_module_instruction in version_change.alter_module_instructions:
|
|
10
|
-
module = alter_module_instruction.module
|
|
11
|
-
mutable_module = context.modules[module.__name__]
|
|
12
|
-
if isinstance(alter_module_instruction, AlterModuleInstruction):
|
|
13
|
-
mutable_module.extra_imports.append(alter_module_instruction.import_)
|
|
14
|
-
else:
|
|
15
|
-
assert_never(alter_module_instruction)
|