Skip to content

Commit ecd7c3a

Browse files
committed
updated validators and fixed ci bug
* fixed validators compatibility problems between python versions * removed duplicated key in pyproject.toml
1 parent b0696a7 commit ecd7c3a

File tree

5 files changed

+124
-12
lines changed

5 files changed

+124
-12
lines changed

fastapi_jsonapi/schema_builder.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import logging
44
from typing import Annotated, Any, Callable, Literal, Optional, Type, TypeVar, Union
55

6-
from pydantic import AfterValidator, BeforeValidator, ConfigDict, create_model
6+
from pydantic import AfterValidator, BeforeValidator, ConfigDict, PlainValidator, WrapValidator, create_model
77
from pydantic import BaseModel as PydanticBaseModel
88

99
# noinspection PyProtectedMember
1010
from pydantic.fields import FieldInfo
11-
from typing_extensions import Unpack
1211

1312
from fastapi_jsonapi.common import get_relationship_info_from_field_metadata, search_client_can_set_id
1413
from fastapi_jsonapi.schema import (
@@ -190,11 +189,13 @@ def _annotation_with_validators(cls, field: FieldInfo) -> type:
190189
annotation = field.annotation
191190
validators = []
192191
for val in field.metadata:
193-
if isinstance(val, (AfterValidator, BeforeValidator)):
192+
if isinstance(val, (AfterValidator, BeforeValidator, WrapValidator, PlainValidator)):
194193
validators.append(val)
195194

196195
if validators:
197-
annotation = Annotated[annotation, Unpack[validators]]
196+
# TODO: change to Annotation[annotation, *validators] when drop support of python 3.10
197+
annotation_validators = ",".join(f"validators[{i}]" for i in range(len(validators)))
198+
annotation = eval(f"Annotated[annotation, {annotation_validators}]")
198199

199200
return annotation
200201

fastapi_jsonapi/validation_utils.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,17 @@ def extract_validators(
3535
if include_for_field_names and field_name not in include_for_field_names:
3636
continue
3737
validator_config = field_validator(field_name, mode=validator.info.mode)
38-
field_validators[name] = validator_config(validator.func)
38+
39+
func = validator.func.__func__ if hasattr(validator.func, "__func__") else validator.func
40+
41+
field_validators[name] = validator_config(func)
3942

4043
# model validators
4144
for name, validator in validators.model_validators.items():
4245
validator_config = model_validator(mode=validator.info.mode)
43-
model_validators[name] = validator_config(validator.func)
46+
47+
func = validator.func.__func__ if hasattr(validator.func, "__func__") else validator.func
48+
49+
model_validators[name] = validator_config(func)
4450

4551
return field_validators, model_validators

fastapi_jsonapi/views/view_base.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,10 @@ def _prepare_item_data(
261261
resource_type: str,
262262
include_fields: Optional[dict[str, dict[str, Type[TypeSchema]]]] = None,
263263
) -> dict:
264+
attrs_schema = schemas_storage.get_attrs_schema(resource_type, operation_type="get")
265+
264266
if include_fields is None or not (field_schemas := include_fields.get(resource_type)):
265-
attrs_schema = schemas_storage.get_attrs_schema(resource_type, operation_type="get")
267+
266268
data_schema = schemas_storage.get_data_schema(resource_type, operation_type="get")
267269
return data_schema(
268270
id=f"{db_item.id}",
@@ -282,13 +284,21 @@ def _prepare_item_data(
282284
)
283285
if before_validators:
284286
for validator_name, validator in before_validators.items():
287+
if hasattr(validator.wrapped, "__func__"):
288+
pre_values = validator.wrapped.__func__(attrs_schema, pre_values)
289+
continue
290+
285291
pre_values = validator.wrapped(pre_values)
286292

287293
for field_name, field_schema in field_schemas.items():
288294
validated_model = field_schema(**{field_name: pre_values[field_name]})
289295

290296
if after_validators:
291297
for validator_name, validator in after_validators.items():
298+
if hasattr(validator.wrapped, "__func__"):
299+
validated_model = validator.wrapped.__func__(attrs_schema, validated_model)
300+
continue
301+
292302
validated_model = validator.wrapped(validated_model)
293303

294304
result_attributes[field_name] = getattr(validated_model, field_name)

pyproject.toml

-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ mypy = "^1.14.1"
101101
pre-commit = "^4.1.0"
102102
ruff = "^0.9.4"
103103
sqlalchemy-stubs = "^0.4"
104-
pre-commit = "^3.3.3"
105104

106105
[tool.poetry.group.docs.dependencies]
107106
sphinx = "^7.0.1"

tests/test_api/test_validators.py

+100-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@
55
from fastapi import FastAPI, status
66
from fastapi.datastructures import QueryParams
77
from httpx import AsyncClient
8-
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
8+
from pydantic import (
9+
AfterValidator,
10+
BaseModel,
11+
BeforeValidator,
12+
ConfigDict,
13+
PlainValidator,
14+
ValidatorFunctionWrapHandler,
15+
WrapValidator,
16+
field_validator,
17+
model_validator,
18+
)
919
from pytest_asyncio import fixture
1020
from sqlalchemy.ext.asyncio import AsyncSession
1121

@@ -723,12 +733,23 @@ def validator_post_2(cls, values):
723733
)
724734

725735
async def test_validator_calls_for_field_requests(self, user_1: User):
736+
def annotation_pre_validator(value: str) -> str:
737+
return f"{value} (annotation_pre_field)"
738+
739+
def annotation_post_validator(value: str) -> str:
740+
return f"{value} (annotation_post_field)"
741+
726742
class UserSchemaWithValidator(BaseModel):
727743
model_config = ConfigDict(
728744
from_attributes=True,
729745
)
730746

731-
name: str
747+
name: Annotated[
748+
str,
749+
BeforeValidator(annotation_pre_validator),
750+
AfterValidator(annotation_post_validator),
751+
# WrapValidator(wrapp_validator),
752+
]
732753

733754
@field_validator("name", mode="before")
734755
@classmethod
@@ -749,7 +770,7 @@ def pre_model_validator(cls, data: dict):
749770

750771
@model_validator(mode="after")
751772
@classmethod
752-
def post_model_validator(self, value):
773+
def post_model_validator(cls, value):
753774
value.name = f"{value.name} (post_model)"
754775
return value
755776

@@ -773,14 +794,89 @@ def post_model_validator(self, value):
773794
"data": {
774795
"attributes": {
775796
# check validators call order
776-
"name": f"{user_1.name} (pre_model) (pre_field) (post_field) (post_model)",
797+
"name": (
798+
f"{user_1.name} (pre_model) (pre_field) (annotation_pre_field) "
799+
"(annotation_post_field) (post_field) (post_model)"
800+
),
777801
},
778802
"type": self.resource_type,
779803
},
780804
"jsonapi": {"version": "1.0"},
781805
"meta": None,
782806
}
783807

808+
async def test_wrapp_validator_for_field_requests(self, user_1: User):
809+
def wrapp_validator(value: str, handler: ValidatorFunctionWrapHandler) -> str:
810+
return f"{value} (wrapp_field)"
811+
812+
class UserSchemaWithValidator(BaseModel):
813+
model_config = ConfigDict(
814+
from_attributes=True,
815+
)
816+
817+
name: Annotated[str, WrapValidator(wrapp_validator)]
818+
819+
params = QueryParams(
820+
[
821+
(f"fields[{self.resource_type}]", "name"),
822+
],
823+
)
824+
825+
app = self.build_app(UserSchemaWithValidator)
826+
827+
async with AsyncClient(app=app, base_url="http://test") as client:
828+
url = app.url_path_for(f"get_{self.resource_type}_detail", obj_id=user_1.id)
829+
res = await client.get(url, params=params)
830+
assert res.status_code == status.HTTP_200_OK, res.text
831+
res_json = res.json()
832+
833+
assert res_json["data"]
834+
assert res_json["data"].pop("id")
835+
assert res_json == {
836+
"data": {
837+
"attributes": {"name": (f"{user_1.name} (wrapp_field)")},
838+
"type": self.resource_type,
839+
},
840+
"jsonapi": {"version": "1.0"},
841+
"meta": None,
842+
}
843+
844+
async def test_plain_validator_for_field_requests(self, user_1: User):
845+
def plain_validator(value: str, handler: ValidatorFunctionWrapHandler) -> str:
846+
return f"{value} (plain_field)"
847+
848+
class UserSchemaWithValidator(BaseModel):
849+
model_config = ConfigDict(
850+
from_attributes=True,
851+
)
852+
853+
name: Annotated[int, PlainValidator(plain_validator)]
854+
855+
params = QueryParams(
856+
[
857+
(f"fields[{self.resource_type}]", "name"),
858+
],
859+
)
860+
861+
app = self.build_app(UserSchemaWithValidator)
862+
863+
async with AsyncClient(app=app, base_url="http://test") as client:
864+
url = app.url_path_for(f"get_{self.resource_type}_detail", obj_id=user_1.id)
865+
res = await client.get(url, params=params)
866+
assert res.status_code == status.HTTP_200_OK, res.text
867+
res_json = res.json()
868+
869+
assert res_json["data"]
870+
assert res_json["data"].pop("id")
871+
assert res_json == {
872+
"data": {
873+
"attributes": {"name": (f"{user_1.name} (plain_field)")},
874+
"type": self.resource_type,
875+
},
876+
"jsonapi": {"version": "1.0"},
877+
"meta": None,
878+
}
879+
784880

785881
class TestValidationUtils:
786882
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)