diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe04d22..5049910 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: - name: Run tests (std) run: PYTHONPATH="$(pwd):$PYTHONPATH" FORCE_STD_XML=true poetry run py.test --cov=pydantic_xml --cov-report=xml tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2ed56b9..d59f799 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,13 @@ Changelog ========= +2.17.3 (2025-07-13) +------------------- + +- fix: xml_field_validator/serializer type annotations fixed. See https://github.com/dapper91/pydantic-xml/pull/277 + + + 2.17.2 (2025-06-21) ------------------- diff --git a/examples/xml-serialization-decorator/model.py b/examples/xml-serialization-decorator/model.py index f8ff322..f7dde11 100644 --- a/examples/xml-serialization-decorator/model.py +++ b/examples/xml-serialization-decorator/model.py @@ -11,9 +11,12 @@ class Plot(BaseXmlModel): y: List[float] = element() @xml_field_validator('x', 'y') + @classmethod def validate_space_separated_list(cls, element: XmlElementReader, field_name: str) -> List[float]: - if element := element.pop_element(field_name, search_mode=cls.__xml_search_mode__): - return list(map(float, element.pop_text().split())) + if (sub_element := element.pop_element(field_name, search_mode=cls.__xml_search_mode__)) and ( + text := sub_element.pop_text() + ): + return list(map(float, text.split())) return [] diff --git a/pydantic_xml/fields.py b/pydantic_xml/fields.py index 7c18e2b..8880172 100644 --- a/pydantic_xml/fields.py +++ b/pydantic_xml/fields.py @@ -1,6 +1,6 @@ import dataclasses as dc import typing -from typing import Any, Callable, Optional, TypeVar, Union +from typing import Any, Callable, Optional, Union import pydantic as pd import pydantic_core as pdc @@ -292,10 +292,9 @@ def computed_element( return computed_entity(EntityLocation.ELEMENT, prop, path=tag, ns=ns, nsmap=nsmap, nillable=nillable, **kwargs) -ValidatorFuncT = TypeVar('ValidatorFuncT', bound='model.SerializerFunc') - - -def xml_field_validator(field: str, /, *fields: str) -> Callable[[ValidatorFuncT], ValidatorFuncT]: +def xml_field_validator( + field: str, /, *fields: str +) -> 'Callable[[model.ValidatorFuncT[model.ModelT]], model.ValidatorFuncT[model.ModelT]]': """ Marks the method as a field xml validator. @@ -303,17 +302,18 @@ def xml_field_validator(field: str, /, *fields: str) -> Callable[[ValidatorFuncT :param fields: fields to be validated """ - def wrapper(func: ValidatorFuncT) -> ValidatorFuncT: + def wrapper(func: model.ValidatorFuncT[model.ModelT]) -> model.ValidatorFuncT[model.ModelT]: + if isinstance(func, (classmethod, staticmethod)): + func = func.__func__ setattr(func, '__xml_field_validator__', (field, *fields)) return func return wrapper -SerializerFuncT = TypeVar('SerializerFuncT', bound='model.SerializerFunc') - - -def xml_field_serializer(field: str, /, *fields: str) -> Callable[[SerializerFuncT], SerializerFuncT]: +def xml_field_serializer( + field: str, /, *fields: str +) -> 'Callable[[model.SerializerFuncT[model.ModelT]], model.SerializerFuncT[model.ModelT]]': """ Marks the method as a field xml serializer. @@ -321,7 +321,7 @@ def xml_field_serializer(field: str, /, *fields: str) -> Callable[[SerializerFun :param fields: fields to be serialized """ - def wrapper(func: SerializerFuncT) -> SerializerFuncT: + def wrapper(func: model.SerializerFuncT[model.ModelT]) -> model.SerializerFuncT[model.ModelT]: setattr(func, '__xml_field_serializer__', (field, *fields)) return func diff --git a/pydantic_xml/model.py b/pydantic_xml/model.py index 940d477..312efd2 100644 --- a/pydantic_xml/model.py +++ b/pydantic_xml/model.py @@ -20,9 +20,12 @@ __all__ = ( 'BaseXmlModel', 'create_model', + 'ModelT', 'RootXmlModel', 'SerializerFunc', + 'SerializerFuncT', 'ValidatorFunc', + 'ValidatorFuncT', 'XmlModelMeta', ) @@ -137,9 +140,11 @@ def _collect_xml_field_serializers_validators(mcls, cls: Type['BaseXmlModel']) - cls.__xml_field_validators__[field] = func -ValidatorFunc = Callable[[Type['BaseXmlModel'], XmlElementReader, str], Any] -SerializerFunc = Callable[['BaseXmlModel', XmlElementWriter, Any, str], Any] ModelT = TypeVar('ModelT', bound='BaseXmlModel') +ValidatorFuncT = Callable[[Type[ModelT], XmlElementReader, str], Any] +ValidatorFunc = ValidatorFuncT['BaseXmlModel'] +SerializerFuncT = Callable[[ModelT, XmlElementWriter, Any, str], Any] +SerializerFunc = SerializerFuncT['BaseXmlModel'] class BaseXmlModel(BaseModel, __xml_abstract__=True, metaclass=XmlModelMeta): diff --git a/pyproject.toml b/pyproject.toml index 22e3416..1c6d117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pydantic-xml" -version = "2.17.2" +version = "2.17.3" description = "pydantic xml extension" authors = ["Dmitry Pershin "] license = "Unlicense" diff --git a/tests/test_misc.py b/tests/test_misc.py index 02bc8a0..826bbd8 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -406,3 +406,13 @@ class TestModel(BaseXmlModel, tag='root'): with pytest.raises(pd.ValidationError): TestModel.from_xml("-1") + + +def test_get_type_hints(): + from typing import get_type_hints + + class TestModel(BaseXmlModel, tag="model"): + int_val: int = element() + + hints = get_type_hints(TestModel) + assert isinstance(hints, dict) diff --git a/tests/test_preprocessors.py b/tests/test_preprocessors.py index efd13bf..52ed990 100644 --- a/tests/test_preprocessors.py +++ b/tests/test_preprocessors.py @@ -13,9 +13,12 @@ class TestModel(BaseXmlModel, tag='model1'): element1: List[int] = element() @xml_field_validator('element1') + @classmethod def validate_element(cls, element: XmlElementReader, field_name: str) -> List[int]: - if element := element.pop_element(field_name, search_mode=cls.__xml_search_mode__): - return list(map(int, element.pop_text().split())) + if (sub_element := element.pop_element(field_name, search_mode=cls.__xml_search_mode__)) and ( + text := sub_element.pop_text() + ): + return list(map(float, text.split())) return []