From dbb8b8f9aae782b6937c03f88f40c7cbf225410b Mon Sep 17 00:00:00 2001 From: Nse-Abasi Etim Date: Thu, 7 May 2026 08:21:25 +0100 Subject: [PATCH 1/2] fix pydantic v2 wrap validators and silence deprecation warning Return validated instances from DANJAResource/DANJAResourceList wrap validators to comply with Pydantic v2 expectations and avoid the "return self" warning. Add regression tests for single/list resources with included payloads, and update resource id resolution to access model_fields from the model class to remove v2.11 deprecation warnings. --- src/pydanja/__init__.py | 43 +++++++++++++++---------- tests/test_resource_creation.py | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/pydanja/__init__.py b/src/pydanja/__init__.py index 84e381e..19aa60d 100644 --- a/src/pydanja/__init__.py +++ b/src/pydanja/__init__.py @@ -20,6 +20,29 @@ ResourceType = TypeVar("ResourceType") +def _validate_ignoring_included(data: Any, handler: ModelWrapValidatorHandler[Self]) -> Self: + """ + Validate a resource container while bypassing validation for `included`. + """ + data_copy = deepcopy(data) + included = None + + # dict payloads (e.g. DANJAResource(...)) + if isinstance(data_copy, dict): + included = data_copy.pop("included", None) + # model payloads (e.g. DANJAResource.model_validate(existing_model)) + elif hasattr(data_copy, "included"): + included = getattr(data_copy, "included") + delattr(data_copy, "included") + + validated = handler(data_copy) + + if included is not None: + validated.included = included + + return validated + + class DANJALink(BaseModel): """JSON:API Link""" @@ -105,7 +128,7 @@ def resolve_resource_name(cls, resource) -> str: @classmethod def resolve_resource_id(cls, resource) -> Optional[str]: - for field_name, field in resource.model_fields.items(): + for field_name, field in resource.__class__.model_fields.items(): if ( hasattr(field, "primary_key") and isinstance(field.primary_key, bool) and field.primary_key ): # Latest SQLMode @@ -181,14 +204,7 @@ def ignore_included(cls, data: Any, handler: ModelWrapValidatorHandler[Self]) -> resource type as the top level data block. So in the meantime, we exclude `included` resources from the validation process. """ - data_copy = deepcopy(data) - - # Exclude included resource types - if hasattr(data_copy, "included"): - delattr(data_copy, "included") - - handler(data_copy) - return data + return _validate_ignoring_included(data, handler) class DANJAResourceList(BaseModel, ResourceResolver, Generic[ResourceType]): @@ -257,11 +273,4 @@ def ignore_included(cls, data: Any, handler: ModelWrapValidatorHandler[Self]) -> resource type as the top level data block. So in the meantime, we exclude `included` resources from the validation process. """ - data_copy = deepcopy(data) - - # Exclude included resource types - if hasattr(data_copy, "included"): - delattr(data_copy, "included") - - handler(data_copy) - return data + return _validate_ignoring_included(data, handler) diff --git a/tests/test_resource_creation.py b/tests/test_resource_creation.py index 5b2d006..1e371b4 100644 --- a/tests/test_resource_creation.py +++ b/tests/test_resource_creation.py @@ -1,5 +1,6 @@ import pytest import json +import warnings from typing import Optional from pathlib import Path from src.pydanja import DANJAResource, DANJAResourceList @@ -137,3 +138,58 @@ def test_it_creates_a_container_list_from_base_model_without_id(resource): # Check resource data assert(len(new_resources.resources) == len(basemodel_instances)) + + +def test_it_does_not_warn_for_single_resource_with_included(): + payload = { + "data": { + "id": "1", + "type": "fixturetesttype", + "attributes": {"id": 1, "name": "Stuff!", "description": "This is desc!"}, + }, + "included": [ + { + "id": "200", + "type": "other", + "attributes": {"name": "Included model"}, + } + ], + } + + with warnings.catch_warnings(record=True) as warning_records: + warnings.simplefilter("always") + resource = DANJAResource[FixtureTestType](**payload) + + assert resource.included == payload["included"] + assert not any("Returning anything other than `self`" in str(w.message) for w in warning_records) + + +def test_it_does_not_warn_for_resource_list_with_included(): + payload = { + "data": [ + { + "id": "1", + "type": "fixturetesttype", + "attributes": {"id": 1, "name": "Stuff!", "description": "This is desc!"}, + }, + { + "id": "2", + "type": "fixturetesttype", + "attributes": {"id": 2, "name": "More Stuff!", "description": "This is more desc!"}, + }, + ], + "included": [ + { + "id": "200", + "type": "other", + "attributes": {"name": "Included model"}, + } + ], + } + + with warnings.catch_warnings(record=True) as warning_records: + warnings.simplefilter("always") + resource_list = DANJAResourceList[FixtureTestType](**payload) + + assert resource_list.included == payload["included"] + assert not any("Returning anything other than `self`" in str(w.message) for w in warning_records) From ee9d9952df5b7592060e3b02d0aceef015ce91ee Mon Sep 17 00:00:00 2001 From: Nse-Abasi Etim Date: Thu, 7 May 2026 10:14:00 +0100 Subject: [PATCH 2/2] fix typecheck issues --- src/pydanja/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pydanja/__init__.py b/src/pydanja/__init__.py index 19aa60d..d900690 100644 --- a/src/pydanja/__init__.py +++ b/src/pydanja/__init__.py @@ -18,9 +18,10 @@ ] ResourceType = TypeVar("ResourceType") +ModelType = TypeVar("ModelType", bound=BaseModel) -def _validate_ignoring_included(data: Any, handler: ModelWrapValidatorHandler[Self]) -> Self: +def _validate_ignoring_included(data: Any, handler: ModelWrapValidatorHandler[ModelType]) -> ModelType: """ Validate a resource container while bypassing validation for `included`. """ @@ -38,7 +39,7 @@ def _validate_ignoring_included(data: Any, handler: ModelWrapValidatorHandler[Se validated = handler(data_copy) if included is not None: - validated.included = included + setattr(validated, "included", included) return validated @@ -193,7 +194,7 @@ def include_from_basemodels(self, includes: list[Any]) -> None: self.included = [] for include in includes: # Convert these to resource types - self.included.append(DANJASingleResource(**include)) # ty: ignore + self.included.append(DANJASingleResource(**include)) @model_validator(mode="wrap") @classmethod @@ -262,7 +263,7 @@ def include_from_basemodels(self, includes: list[Any]) -> None: self.included = [] for include in includes: # Convert these to resource types - self.included.append(DANJASingleResource(**include)) # ty: ignore + self.included.append(DANJASingleResource(**include)) @model_validator(mode="wrap") @classmethod