cadwyn 3.3.4__tar.gz → 3.4.0__tar.gz
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-3.3.4 → cadwyn-3.4.0}/PKG-INFO +1 -1
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/_compat.py +27 -31
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_asts.py +55 -28
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_common.py +47 -19
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_main.py +6 -5
- cadwyn-3.4.0/cadwyn/codegen/_plugins/class_migrations.py +423 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/class_rebuilding.py +16 -42
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/import_auto_adding.py +5 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/data.py +1 -1
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/schemas.py +121 -23
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/versions.py +7 -7
- {cadwyn-3.3.4 → cadwyn-3.4.0}/pyproject.toml +1 -1
- {cadwyn-3.3.4 → cadwyn-3.4.0}/setup.py +1 -1
- cadwyn-3.3.4/cadwyn/codegen/_plugins/class_migrations.py +0 -207
- {cadwyn-3.3.4 → cadwyn-3.4.0}/LICENSE +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/README.md +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/__init__.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/__main__.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/_package_utils.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/_utils.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/README.md +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/__init__.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/__init__.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/class_renaming.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/latest_version_aliasing.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/module_migrations.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/exceptions.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/main.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/py.typed +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/routing.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/__init__.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/common.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/endpoints.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/enums.py +0 -0
- {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/modules.py +0 -0
|
@@ -3,16 +3,19 @@ import dataclasses
|
|
|
3
3
|
import inspect
|
|
4
4
|
from typing import Any, TypeAlias
|
|
5
5
|
|
|
6
|
+
import pydantic
|
|
6
7
|
from fastapi._compat import ModelField as FastAPIModelField
|
|
7
8
|
from pydantic import BaseModel, Field
|
|
8
9
|
|
|
9
10
|
ModelField: TypeAlias = Any # pyright: ignore[reportGeneralTypeIssues]
|
|
10
|
-
|
|
11
|
+
PydanticUndefined: TypeAlias = Any
|
|
12
|
+
VALIDATOR_CONFIG_KEY = "__validators__"
|
|
11
13
|
|
|
12
14
|
try:
|
|
13
15
|
PYDANTIC_V2 = False
|
|
14
16
|
|
|
15
|
-
from pydantic.fields import FieldInfo, ModelField
|
|
17
|
+
from pydantic.fields import FieldInfo, ModelField # pyright: ignore # noqa: PGH003
|
|
18
|
+
from pydantic.fields import Undefined as PydanticUndefined # pyright: ignore # noqa: PGH003
|
|
16
19
|
|
|
17
20
|
_all_field_arg_names = []
|
|
18
21
|
EXTRA_FIELD_NAME = "extra"
|
|
@@ -20,10 +23,9 @@ except ImportError:
|
|
|
20
23
|
PYDANTIC_V2 = True
|
|
21
24
|
|
|
22
25
|
from pydantic.fields import FieldInfo
|
|
23
|
-
from pydantic_core import PydanticUndefined
|
|
26
|
+
from pydantic_core import PydanticUndefined # pyright: ignore # noqa: PGH003
|
|
24
27
|
|
|
25
28
|
ModelField: TypeAlias = FieldInfo # pyright: ignore # noqa: PGH003
|
|
26
|
-
Undefined = PydanticUndefined # pyright: ignore # noqa: PGH003
|
|
27
29
|
_all_field_arg_names = sorted(
|
|
28
30
|
[
|
|
29
31
|
name
|
|
@@ -38,38 +40,34 @@ _empty_field_info = Field()
|
|
|
38
40
|
dict_of_empty_field_info = {k: getattr(_empty_field_info, k) for k in FieldInfo.__slots__}
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
def
|
|
43
|
+
def is_pydantic_1_constrained_type(value: object):
|
|
44
|
+
"""This method only works for pydanticV1. It is always False in PydanticV2"""
|
|
42
45
|
return isinstance(value, type) and value.__name__.startswith("Constrained") and value.__name__.endswith("Value")
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
attrs_in_value_different_from_parent_that_are_in_field_def = {
|
|
55
|
-
k: v for k, v in attrs_in_value_different_from_parent.items() if k in dict_of_empty_field_info
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
attrs_in_value_different_from_parent_that_are_not_in_field_def,
|
|
60
|
-
attrs_in_value_different_from_parent_that_are_in_field_def,
|
|
61
|
-
)
|
|
48
|
+
def is_constrained_type(value: object):
|
|
49
|
+
if PYDANTIC_V2:
|
|
50
|
+
import annotated_types
|
|
51
|
+
|
|
52
|
+
return isinstance(value, annotated_types.Len | annotated_types.Interval | pydantic.StringConstraints)
|
|
53
|
+
|
|
54
|
+
else:
|
|
55
|
+
return is_pydantic_1_constrained_type(value)
|
|
62
56
|
|
|
63
57
|
|
|
64
58
|
@dataclasses.dataclass(slots=True)
|
|
65
59
|
class PydanticFieldWrapper:
|
|
60
|
+
"""We DO NOT maintain field.metadata at all"""
|
|
61
|
+
|
|
66
62
|
annotation: Any
|
|
67
63
|
|
|
68
64
|
init_model_field: dataclasses.InitVar[ModelField] # pyright: ignore[reportGeneralTypeIssues]
|
|
69
65
|
field_info: FieldInfo = dataclasses.field(init=False)
|
|
70
66
|
|
|
71
67
|
annotation_ast: ast.expr | None = None
|
|
72
|
-
|
|
68
|
+
# In the expressions "foo: str | None = None" and "foo: str | None = Field(default=None)"
|
|
69
|
+
# the value_ast is "None" and "Field(default=None)" respectively
|
|
70
|
+
value_ast: ast.expr | None = None
|
|
73
71
|
|
|
74
72
|
def __post_init__(self, init_model_field: ModelField): # pyright: ignore[reportGeneralTypeIssues]
|
|
75
73
|
if isinstance(init_model_field, FieldInfo):
|
|
@@ -77,20 +75,18 @@ class PydanticFieldWrapper:
|
|
|
77
75
|
else:
|
|
78
76
|
self.field_info = init_model_field.field_info
|
|
79
77
|
|
|
80
|
-
def get_annotation_for_rendering(self):
|
|
81
|
-
if self.annotation_ast:
|
|
82
|
-
return self.annotation_ast
|
|
83
|
-
else:
|
|
84
|
-
return self.annotation
|
|
85
|
-
|
|
86
78
|
def update_attribute(self, *, name: str, value: Any):
|
|
87
79
|
if PYDANTIC_V2:
|
|
88
|
-
if name in FieldInfo.metadata_lookup:
|
|
89
|
-
self.field_info.metadata.extend(FieldInfo._collect_metadata({name: value}))
|
|
90
80
|
self.field_info._attributes_set[name] = value
|
|
91
81
|
else:
|
|
92
82
|
setattr(self.field_info, name, value)
|
|
93
83
|
|
|
84
|
+
def delete_attribute(self, *, name: str) -> None:
|
|
85
|
+
if PYDANTIC_V2:
|
|
86
|
+
self.field_info._attributes_set.pop(name)
|
|
87
|
+
else:
|
|
88
|
+
setattr(self.field_info, name, PydanticUndefined)
|
|
89
|
+
|
|
94
90
|
@property
|
|
95
91
|
def passed_field_attributes(self):
|
|
96
92
|
if PYDANTIC_V2:
|
|
@@ -2,6 +2,7 @@ import ast
|
|
|
2
2
|
import inspect
|
|
3
3
|
import re
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from enum import Enum, auto
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from types import GenericAlias, LambdaType, ModuleType, NoneType
|
|
@@ -15,8 +16,7 @@ from typing import ( # noqa: UP035
|
|
|
15
16
|
|
|
16
17
|
from cadwyn._compat import (
|
|
17
18
|
PYDANTIC_V2,
|
|
18
|
-
|
|
19
|
-
is_pydantic_constrained_type,
|
|
19
|
+
is_pydantic_1_constrained_type,
|
|
20
20
|
)
|
|
21
21
|
from cadwyn._package_utils import (
|
|
22
22
|
get_absolute_python_path_of_import,
|
|
@@ -108,29 +108,22 @@ def transform_none(_: NoneType) -> Any:
|
|
|
108
108
|
|
|
109
109
|
def transform_type(value: type) -> Any:
|
|
110
110
|
# This is a hack for pydantic's Constrained types
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
],
|
|
125
|
-
)
|
|
126
|
-
+ ")"
|
|
111
|
+
if is_pydantic_1_constrained_type(value):
|
|
112
|
+
parent = value.mro()[1]
|
|
113
|
+
snake_case = _RE_CAMEL_TO_SNAKE.sub("_", value.__name__)
|
|
114
|
+
cls_name = "con" + "".join(snake_case.split("_")[1:-1])
|
|
115
|
+
return (
|
|
116
|
+
cls_name.lower()
|
|
117
|
+
+ "("
|
|
118
|
+
+ ", ".join(
|
|
119
|
+
[
|
|
120
|
+
f"{key}={get_fancy_repr(val)}"
|
|
121
|
+
for key, val in value.__dict__.items()
|
|
122
|
+
if not key.startswith("_") and val is not None and val != parent.__dict__[key]
|
|
123
|
+
],
|
|
127
124
|
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# MRO of constr looks like: [ConstrainedStrValue, pydantic.types.ConstrainedStr, str, object]
|
|
131
|
-
# -2 -1
|
|
132
|
-
# ^^^
|
|
133
|
-
value = value.mro()[-2]
|
|
125
|
+
+ ")"
|
|
126
|
+
)
|
|
134
127
|
|
|
135
128
|
return value.__name__
|
|
136
129
|
|
|
@@ -230,11 +223,17 @@ def add_keyword_to_call(attr_name: str, attr_value: Any, call: ast.Call):
|
|
|
230
223
|
call.keywords.append(new_keyword)
|
|
231
224
|
|
|
232
225
|
|
|
226
|
+
def delete_keyword_from_call(attr_name: str, call: ast.Call):
|
|
227
|
+
for i, keyword in enumerate(call.keywords): # pragma: no branch
|
|
228
|
+
if keyword.arg == attr_name:
|
|
229
|
+
call.keywords.pop(i)
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
|
|
233
233
|
def get_ast_keyword_from_argument_name_and_value(name: str, value: Any):
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
)
|
|
234
|
+
if not isinstance(value, ast.AST):
|
|
235
|
+
value = ast.parse(get_fancy_repr(value), mode="eval").body
|
|
236
|
+
return ast.keyword(arg=name, value=value)
|
|
238
237
|
|
|
239
238
|
|
|
240
239
|
def pop_docstring_from_cls_body(cls_body: list[ast.stmt]) -> list[ast.stmt]:
|
|
@@ -247,3 +246,31 @@ def pop_docstring_from_cls_body(cls_body: list[ast.stmt]) -> list[ast.stmt]:
|
|
|
247
246
|
return [cls_body.pop(0)]
|
|
248
247
|
else:
|
|
249
248
|
return []
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass(slots=True)
|
|
252
|
+
class _ValidatorWrapper:
|
|
253
|
+
func_ast: ast.FunctionDef
|
|
254
|
+
index_of_validator_decorator: int
|
|
255
|
+
field_names: set[str | ast.expr] | None
|
|
256
|
+
is_deleted: bool = False
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_validator_info_or_none(method: ast.FunctionDef) -> _ValidatorWrapper | None:
|
|
260
|
+
for index, decorator in enumerate(method.decorator_list):
|
|
261
|
+
# The cases we handle here:
|
|
262
|
+
# * `Name(id="root_validator")`
|
|
263
|
+
# * `Call(func=Name(id="validator"), args=[Constant(value="foo")])`
|
|
264
|
+
# * `Attribute(value=Name(id="pydantic"), attr="root_validator")`
|
|
265
|
+
# * `Call(func=Attribute(value=Name(id="pydantic"), attr="root_validator"), args=[])`
|
|
266
|
+
|
|
267
|
+
if isinstance(decorator, ast.Call) and ast.unparse(decorator.func).endswith("validator"):
|
|
268
|
+
if len(decorator.args) == 0:
|
|
269
|
+
return _ValidatorWrapper(method, index, None)
|
|
270
|
+
else:
|
|
271
|
+
return _ValidatorWrapper(
|
|
272
|
+
method, index, {arg.value if isinstance(arg, ast.Constant) else arg for arg in decorator.args}
|
|
273
|
+
)
|
|
274
|
+
elif isinstance(decorator, ast.Name | ast.Attribute) and ast.unparse(decorator).endswith("validator"):
|
|
275
|
+
return _ValidatorWrapper(method, index, None)
|
|
276
|
+
return None
|
|
@@ -16,6 +16,8 @@ from cadwyn._package_utils import IdentifierPythonPath
|
|
|
16
16
|
from cadwyn.exceptions import CodeGenerationError
|
|
17
17
|
from cadwyn.structure.versions import Version
|
|
18
18
|
|
|
19
|
+
from ._asts import _ValidatorWrapper, get_validator_info_or_none
|
|
20
|
+
|
|
19
21
|
_FieldName: TypeAlias = str
|
|
20
22
|
_CodegenPluginASTType = TypeVar("_CodegenPluginASTType", bound=ast.AST)
|
|
21
23
|
|
|
@@ -25,6 +27,7 @@ class PydanticModelWrapper:
|
|
|
25
27
|
cls: type[BaseModel]
|
|
26
28
|
name: str
|
|
27
29
|
fields: dict[_FieldName, PydanticFieldWrapper]
|
|
30
|
+
validators: dict[_FieldName, _ValidatorWrapper]
|
|
28
31
|
_parents: list[Self] | None = dataclasses.field(init=False, default=None)
|
|
29
32
|
|
|
30
33
|
def _get_parents(self, schemas: "dict[IdentifierPythonPath, Self]"):
|
|
@@ -37,11 +40,14 @@ class PydanticModelWrapper:
|
|
|
37
40
|
if schema_path in schemas:
|
|
38
41
|
parents.append(schemas[schema_path])
|
|
39
42
|
elif issubclass(base, BaseModel):
|
|
40
|
-
|
|
43
|
+
fields, validators = get_fields_and_validators_from_model(base)
|
|
44
|
+
parents.append(type(self)(base, base.__name__, fields, validators))
|
|
41
45
|
self._parents = parents
|
|
42
46
|
return parents
|
|
43
47
|
|
|
44
|
-
def
|
|
48
|
+
def _get_defined_fields_through_mro(
|
|
49
|
+
self, schemas: "dict[IdentifierPythonPath, Self]"
|
|
50
|
+
) -> dict[str, PydanticFieldWrapper]:
|
|
45
51
|
fields = {}
|
|
46
52
|
|
|
47
53
|
for parent in reversed(self._get_parents(schemas)):
|
|
@@ -49,9 +55,19 @@ class PydanticModelWrapper:
|
|
|
49
55
|
|
|
50
56
|
return fields | self.fields
|
|
51
57
|
|
|
58
|
+
def _get_defined_annotations_through_mro(self, schemas: "dict[IdentifierPythonPath, Self]") -> dict[str, Any]:
|
|
59
|
+
annotations = {}
|
|
60
|
+
|
|
61
|
+
for parent in reversed(self._get_parents(schemas)):
|
|
62
|
+
annotations |= parent.cls.__annotations__
|
|
63
|
+
|
|
64
|
+
return annotations | self.cls.__annotations__
|
|
65
|
+
|
|
52
66
|
|
|
53
67
|
@cache
|
|
54
|
-
def
|
|
68
|
+
def get_fields_and_validators_from_model(
|
|
69
|
+
cls: type,
|
|
70
|
+
) -> tuple[dict[_FieldName, PydanticFieldWrapper], dict[_FieldName, _ValidatorWrapper]]:
|
|
55
71
|
if not isinstance(cls, type) or not issubclass(cls, BaseModel):
|
|
56
72
|
raise CodeGenerationError(f"Model {cls} is not a subclass of BaseModel")
|
|
57
73
|
|
|
@@ -59,25 +75,37 @@ def get_fields_from_model(cls: type) -> dict[str, PydanticFieldWrapper]:
|
|
|
59
75
|
try:
|
|
60
76
|
source = inspect.getsource(cls)
|
|
61
77
|
except OSError:
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
annotation=field.annotation,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
return (
|
|
79
|
+
{
|
|
80
|
+
field_name: PydanticFieldWrapper(annotation=field.annotation, init_model_field=field)
|
|
81
|
+
for field_name, field in fields.items()
|
|
82
|
+
},
|
|
83
|
+
{},
|
|
84
|
+
)
|
|
69
85
|
else:
|
|
70
86
|
cls_ast = cast(ast.ClassDef, ast.parse(source).body[0])
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
annotation_ast=node.annotation,
|
|
76
|
-
field_ast=node.value,
|
|
77
|
-
)
|
|
87
|
+
validators: dict[str, _ValidatorWrapper] = {}
|
|
88
|
+
|
|
89
|
+
validators_and_nones = (
|
|
90
|
+
get_validator_info_or_none(node)
|
|
78
91
|
for node in cls_ast.body
|
|
79
|
-
if isinstance(node, ast.
|
|
80
|
-
|
|
92
|
+
if isinstance(node, ast.FunctionDef) and node.decorator_list
|
|
93
|
+
)
|
|
94
|
+
validators = {validator.func_ast.name: validator for validator in validators_and_nones if validator is not None}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
{
|
|
98
|
+
node.target.id: PydanticFieldWrapper(
|
|
99
|
+
annotation=fields[node.target.id].annotation,
|
|
100
|
+
init_model_field=fields[node.target.id],
|
|
101
|
+
annotation_ast=node.annotation,
|
|
102
|
+
value_ast=node.value,
|
|
103
|
+
)
|
|
104
|
+
for node in cls_ast.body
|
|
105
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id in fields
|
|
106
|
+
},
|
|
107
|
+
validators,
|
|
108
|
+
)
|
|
81
109
|
|
|
82
110
|
|
|
83
111
|
@dataclass(slots=True)
|
|
@@ -21,7 +21,7 @@ from cadwyn.codegen._common import (
|
|
|
21
21
|
PydanticModelWrapper,
|
|
22
22
|
_EnumWrapper,
|
|
23
23
|
_ModuleWrapper,
|
|
24
|
-
|
|
24
|
+
get_fields_and_validators_from_model,
|
|
25
25
|
)
|
|
26
26
|
from cadwyn.codegen._plugins.class_migrations import class_migration_plugin
|
|
27
27
|
from cadwyn.codegen._plugins.class_rebuilding import ClassRebuildingPlugin
|
|
@@ -63,14 +63,15 @@ def generate_code_for_versioned_packages(
|
|
|
63
63
|
version of the latest module.
|
|
64
64
|
"""
|
|
65
65
|
extra_context = extra_context or {}
|
|
66
|
+
schemas = {}
|
|
67
|
+
for k, v in deepcopy(versions.versioned_schemas).items():
|
|
68
|
+
fields, validators = get_fields_and_validators_from_model(v)
|
|
69
|
+
schemas[k] = PydanticModelWrapper(v, v.__name__, fields, validators)
|
|
66
70
|
|
|
67
71
|
_generate_versioned_directories(
|
|
68
72
|
template_module,
|
|
69
73
|
versions=list(versions),
|
|
70
|
-
schemas=
|
|
71
|
-
k: PydanticModelWrapper(v, v.__name__, get_fields_from_model(v))
|
|
72
|
-
for k, v in deepcopy(versions.versioned_schemas).items()
|
|
73
|
-
},
|
|
74
|
+
schemas=schemas,
|
|
74
75
|
enums={
|
|
75
76
|
k: _EnumWrapper(v, {member.name: member.value for member in v})
|
|
76
77
|
for k, v in deepcopy(versions.versioned_enums).items()
|