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.

Files changed (35) hide show
  1. {cadwyn-3.3.4 → cadwyn-3.4.0}/PKG-INFO +1 -1
  2. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/_compat.py +27 -31
  3. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_asts.py +55 -28
  4. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_common.py +47 -19
  5. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_main.py +6 -5
  6. cadwyn-3.4.0/cadwyn/codegen/_plugins/class_migrations.py +423 -0
  7. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/class_rebuilding.py +16 -42
  8. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/import_auto_adding.py +5 -0
  9. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/data.py +1 -1
  10. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/schemas.py +121 -23
  11. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/versions.py +7 -7
  12. {cadwyn-3.3.4 → cadwyn-3.4.0}/pyproject.toml +1 -1
  13. {cadwyn-3.3.4 → cadwyn-3.4.0}/setup.py +1 -1
  14. cadwyn-3.3.4/cadwyn/codegen/_plugins/class_migrations.py +0 -207
  15. {cadwyn-3.3.4 → cadwyn-3.4.0}/LICENSE +0 -0
  16. {cadwyn-3.3.4 → cadwyn-3.4.0}/README.md +0 -0
  17. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/__init__.py +0 -0
  18. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/__main__.py +0 -0
  19. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/_package_utils.py +0 -0
  20. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/_utils.py +0 -0
  21. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/README.md +0 -0
  22. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/__init__.py +0 -0
  23. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/__init__.py +0 -0
  24. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/class_renaming.py +0 -0
  25. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/latest_version_aliasing.py +0 -0
  26. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/codegen/_plugins/module_migrations.py +0 -0
  27. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/exceptions.py +0 -0
  28. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/main.py +0 -0
  29. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/py.typed +0 -0
  30. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/routing.py +0 -0
  31. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/__init__.py +0 -0
  32. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/common.py +0 -0
  33. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/endpoints.py +0 -0
  34. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/enums.py +0 -0
  35. {cadwyn-3.3.4 → cadwyn-3.4.0}/cadwyn/structure/modules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cadwyn
3
- Version: 3.3.4
3
+ Version: 3.4.0
4
4
  Summary: Production-ready community-driven modern Stripe-like API versioning in FastAPI
5
5
  Home-page: https://github.com/zmievsa/cadwyn
6
6
  License: MIT
@@ -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
- Undefined: TypeAlias = Any
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, Undefined # pyright: ignore # noqa: PGH003
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 is_pydantic_constrained_type(value: object):
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 get_attrs_that_are_not_from_field_and_that_are_from_field(value: type):
46
- parent_public_attrs = {k: v for k, v in value.mro()[1].__dict__.items() if not k.startswith("_")}
47
- value_private_attrs = {k: v for k, v in value.__dict__.items() if not k.startswith("_")}
48
- attrs_in_value_different_from_parent = {
49
- k: v for k, v in value_private_attrs.items() if k in parent_public_attrs and parent_public_attrs[k] != v
50
- }
51
- attrs_in_value_different_from_parent_that_are_not_in_field_def = {
52
- k: v for k, v in attrs_in_value_different_from_parent.items() if k not in dict_of_empty_field_info
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
- field_ast: ast.expr | None = None
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
- get_attrs_that_are_not_from_field_and_that_are_from_field,
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 is_pydantic_constrained_type(value):
112
- if get_attrs_that_are_not_from_field_and_that_are_from_field(value)[0]:
113
- parent = value.mro()[1]
114
- snake_case = _RE_CAMEL_TO_SNAKE.sub("_", value.__name__)
115
- cls_name = "con" + "".join(snake_case.split("_")[1:-1])
116
- return (
117
- cls_name.lower()
118
- + "("
119
- + ", ".join(
120
- [
121
- f"{key}={get_fancy_repr(val)}"
122
- for key, val in value.__dict__.items()
123
- if not key.startswith("_") and val is not None and val != parent.__dict__[key]
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
- else:
129
- # In pydantic V1:
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
- return ast.keyword(
235
- arg=name,
236
- value=ast.parse(get_fancy_repr(value), mode="eval").body,
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
- parents.append(type(self)(base, base.__name__, get_fields_from_model(base)))
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 _get_defined_fields(self, schemas: "dict[IdentifierPythonPath, Self]") -> dict[str, PydanticFieldWrapper]:
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 get_fields_from_model(cls: type) -> dict[str, PydanticFieldWrapper]:
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
- field_name: PydanticFieldWrapper(
64
- annotation=field.annotation,
65
- init_model_field=field,
66
- )
67
- for field_name, field in fields.items()
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
- return {
72
- node.target.id: PydanticFieldWrapper(
73
- annotation=fields[node.target.id].annotation,
74
- init_model_field=fields[node.target.id],
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.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id in fields
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
- get_fields_from_model,
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()