Skip to content

Commit 0d81acd

Browse files
committed
added changelog and refactor
1 parent cc17623 commit 0d81acd

File tree

3 files changed

+146
-120
lines changed

3 files changed

+146
-120
lines changed

docs/changelog.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ Changelog
22
#########
33

44

5+
**2.3.1**
6+
*********
7+
8+
Pydantic validators inheritance fix
9+
====================================
10+
11+
* fixed bag with schema validators passthrough `#45 <https://github.com/mts-ai/FastAPI-JSONAPI/pull/45>`_
12+
13+
Authors
14+
"""""""
15+
16+
* `@CosmoV`_
17+
* `@mahenzon`_
18+
19+
520
**2.3.0**
621
*********
722

fastapi_jsonapi/schema_builder.py

Lines changed: 8 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""JSON API schemas builder class."""
2-
from copy import deepcopy
32
from dataclasses import dataclass
43
from typing import (
54
Any,
@@ -8,27 +7,16 @@
87
Iterable,
98
List,
109
Optional,
11-
Set,
1210
Tuple,
1311
Type,
1412
TypeVar,
1513
Union,
1614
)
1715

1816
import pydantic
19-
from pydantic import (
20-
BaseConfig,
21-
root_validator,
22-
validator,
23-
)
17+
from pydantic import BaseConfig
2418
from pydantic import BaseModel as PydanticBaseModel
25-
from pydantic.class_validators import (
26-
extract_root_validators,
27-
extract_validators,
28-
inherit_validators,
29-
)
30-
from pydantic.fields import FieldInfo, ModelField, Validator
31-
from pydantic.utils import unique_list
19+
from pydantic.fields import FieldInfo, ModelField
3220

3321
from fastapi_jsonapi.data_typing import TypeSchema
3422
from fastapi_jsonapi.schema import (
@@ -45,6 +33,10 @@
4533
)
4634
from fastapi_jsonapi.schema_base import BaseModel, Field, RelationshipInfo, registry
4735
from fastapi_jsonapi.splitter import SPLIT_REL
36+
from fastapi_jsonapi.validation_utils import (
37+
extract_field_validators,
38+
extract_validators,
39+
)
4840

4941
JSON_API_RESPONSE_TYPE = Dict[Union[int, str], Dict[str, Any]]
5042

@@ -301,7 +293,7 @@ def _get_info_from_schema_for_building(
301293
# works both for to-one and to-many
302294
included_schemas.append((name, field.type_, relationship.resource_type))
303295
elif name == "id":
304-
id_validators = self._extract_field_validators(
296+
id_validators = extract_field_validators(
305297
schema,
306298
include_for_field_names={"id"},
307299
)
@@ -323,7 +315,7 @@ class ConfigOrmMode(BaseConfig):
323315
f"{base_name}AttributesJSONAPI",
324316
**attributes_schema_fields,
325317
__config__=ConfigOrmMode,
326-
__validators__=self._extract_validators(schema, exclude_for_field_names={"id"}),
318+
__validators__=extract_validators(schema, exclude_for_field_names={"id"}),
327319
)
328320

329321
relationships_schema = pydantic.create_model(
@@ -391,110 +383,6 @@ def create_relationship_data_schema(
391383
self.relationship_schema_cache[cache_key] = relationship_data_schema
392384
return relationship_data_schema
393385

394-
def _extract_root_validators(self, model: Type[BaseModel]) -> Dict[str, Callable]:
395-
pre_rv_new, post_rv_new = extract_root_validators(model.__dict__)
396-
pre_root_validators = unique_list(
397-
model.__pre_root_validators__ + pre_rv_new,
398-
name_factory=lambda v: v.__name__,
399-
)
400-
post_root_validators = unique_list(
401-
model.__post_root_validators__ + post_rv_new,
402-
name_factory=lambda skip_on_failure_and_v: skip_on_failure_and_v[1].__name__,
403-
)
404-
405-
result_validators = {}
406-
407-
for validator_func in pre_root_validators:
408-
result_validators[validator_func.__name__] = root_validator(
409-
pre=True,
410-
allow_reuse=True,
411-
)(validator_func)
412-
413-
for skip_on_failure, validator_func in post_root_validators:
414-
result_validators[validator_func.__name__] = root_validator(
415-
allow_reuse=True,
416-
skip_on_failure=skip_on_failure,
417-
)(validator_func)
418-
419-
return result_validators
420-
421-
def _deduplicate_field_validators(self, validators: dict) -> dict:
422-
result_validators = {}
423-
424-
for field_name, field_validators in validators.items():
425-
result_validators[field_name] = list(
426-
{
427-
# override in definition order
428-
field_validator.func.__name__: field_validator
429-
for field_validator in field_validators
430-
}.values(),
431-
)
432-
433-
return result_validators
434-
435-
def _extract_field_validators(
436-
self,
437-
model: Type[BaseModel],
438-
*,
439-
include_for_field_names: Set[str] = None,
440-
exclude_for_field_names: Set[str] = None,
441-
):
442-
validators = inherit_validators(
443-
extract_validators(model.__dict__),
444-
deepcopy(model.__validators__),
445-
)
446-
validators = self._deduplicate_field_validators(validators)
447-
validator_origin_param_keys = (
448-
"pre",
449-
"each_item",
450-
"always",
451-
"check_fields",
452-
)
453-
454-
exclude_for_field_names = exclude_for_field_names or set()
455-
456-
if include_for_field_names and exclude_for_field_names:
457-
exclude_for_field_names = include_for_field_names.difference(
458-
exclude_for_field_names,
459-
)
460-
461-
result_validators = {}
462-
for field_name, field_validators in validators.items():
463-
if field_name in exclude_for_field_names:
464-
continue
465-
466-
if include_for_field_names and field_name not in include_for_field_names:
467-
continue
468-
469-
field_validator: Validator
470-
for field_validator in field_validators:
471-
validator_name = f"{field_name}_{field_validator.func.__name__}_validator"
472-
validator_params = {
473-
# copy validator params
474-
param_key: getattr(field_validator, param_key)
475-
for param_key in validator_origin_param_keys
476-
}
477-
result_validators[validator_name] = validator(
478-
field_name,
479-
**validator_params,
480-
allow_reuse=True,
481-
)(field_validator.func)
482-
483-
return result_validators
484-
485-
def _extract_validators(
486-
self,
487-
model: Type[BaseModel],
488-
exclude_for_field_names: Set[str] = None,
489-
) -> Dict[str, Callable]:
490-
return {
491-
**self._extract_field_validators(
492-
model,
493-
exclude_for_field_names=exclude_for_field_names,
494-
),
495-
**self._extract_root_validators(model),
496-
}
497-
498386
def _build_jsonapi_object(
499387
self,
500388
base_name: str,
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from copy import deepcopy
2+
from typing import (
3+
Callable,
4+
Dict,
5+
Set,
6+
Type,
7+
)
8+
9+
from pydantic import (
10+
class_validators,
11+
root_validator,
12+
validator,
13+
)
14+
from pydantic.fields import Validator
15+
from pydantic.utils import unique_list
16+
17+
from fastapi_jsonapi.schema_base import BaseModel
18+
19+
20+
def extract_root_validators(model: Type[BaseModel]) -> Dict[str, Callable]:
21+
pre_rv_new, post_rv_new = class_validators.extract_root_validators(model.__dict__)
22+
pre_root_validators = unique_list(
23+
model.__pre_root_validators__ + pre_rv_new,
24+
name_factory=lambda v: v.__name__,
25+
)
26+
post_root_validators = unique_list(
27+
model.__post_root_validators__ + post_rv_new,
28+
name_factory=lambda skip_on_failure_and_v: skip_on_failure_and_v[1].__name__,
29+
)
30+
31+
result_validators = {}
32+
33+
for validator_func in pre_root_validators:
34+
result_validators[validator_func.__name__] = root_validator(
35+
pre=True,
36+
allow_reuse=True,
37+
)(validator_func)
38+
39+
for skip_on_failure, validator_func in post_root_validators:
40+
result_validators[validator_func.__name__] = root_validator(
41+
allow_reuse=True,
42+
skip_on_failure=skip_on_failure,
43+
)(validator_func)
44+
45+
return result_validators
46+
47+
48+
def _deduplicate_field_validators(validators: Dict) -> Dict:
49+
result_validators = {}
50+
51+
for field_name, field_validators in validators.items():
52+
result_validators[field_name] = list(
53+
{
54+
# override in definition order
55+
field_validator.func.__name__: field_validator
56+
for field_validator in field_validators
57+
}.values(),
58+
)
59+
60+
return result_validators
61+
62+
63+
def extract_field_validators(
64+
model: Type[BaseModel],
65+
*,
66+
include_for_field_names: Set[str] = None,
67+
exclude_for_field_names: Set[str] = None,
68+
):
69+
validators = class_validators.inherit_validators(
70+
class_validators.extract_validators(model.__dict__),
71+
deepcopy(model.__validators__),
72+
)
73+
validators = _deduplicate_field_validators(validators)
74+
validator_origin_param_keys = (
75+
"pre",
76+
"each_item",
77+
"always",
78+
"check_fields",
79+
)
80+
81+
exclude_for_field_names = exclude_for_field_names or set()
82+
83+
if include_for_field_names and exclude_for_field_names:
84+
exclude_for_field_names = include_for_field_names.difference(
85+
exclude_for_field_names,
86+
)
87+
88+
result_validators = {}
89+
for field_name, field_validators in validators.items():
90+
if field_name in exclude_for_field_names:
91+
continue
92+
93+
if include_for_field_names and field_name not in include_for_field_names:
94+
continue
95+
96+
field_validator: Validator
97+
for field_validator in field_validators:
98+
validator_name = f"{field_name}_{field_validator.func.__name__}_validator"
99+
validator_params = {
100+
# copy validator params
101+
param_key: getattr(field_validator, param_key)
102+
for param_key in validator_origin_param_keys
103+
}
104+
result_validators[validator_name] = validator(
105+
field_name,
106+
**validator_params,
107+
allow_reuse=True,
108+
)(field_validator.func)
109+
110+
return result_validators
111+
112+
113+
def extract_validators(
114+
model: Type[BaseModel],
115+
exclude_for_field_names: Set[str] = None,
116+
) -> Dict[str, Callable]:
117+
return {
118+
**extract_field_validators(
119+
model,
120+
exclude_for_field_names=exclude_for_field_names,
121+
),
122+
**extract_root_validators(model),
123+
}

0 commit comments

Comments
 (0)