From b89ce45f08c3aede3006e084d282848dbee63118 Mon Sep 17 00:00:00 2001 From: Dmitry Pershin Date: Sun, 18 Oct 2026 18:56:32 +0500 Subject: [PATCH 1/2] fix: added named-tuple core schema support (added in pydantic 2.14 release). --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- pydantic_xml/serializers/factories/call.py | 10 +--------- .../serializers/factories/homogeneous.py | 2 ++ .../serializers/factories/named_tuple.py | 17 ++++++++--------- pydantic_xml/serializers/serializer.py | 6 ++++++ pyproject.toml | 7 +++---- 7 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1706e7..52f9d19 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.15'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad47610..dfb0c3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,5 +72,5 @@ repos: pass_filenames: false args: ["--package", "pydantic_xml"] additional_dependencies: - - pydantic>=2.6.0 + - pydantic>=2.14.0 - lxml-stubs>=0.4.0 diff --git a/pydantic_xml/serializers/factories/call.py b/pydantic_xml/serializers/factories/call.py index a5f70be..9900712 100644 --- a/pydantic_xml/serializers/factories/call.py +++ b/pydantic_xml/serializers/factories/call.py @@ -1,16 +1,8 @@ -import inspect - from pydantic_core import core_schema as pcs from pydantic_xml import errors -from pydantic_xml.serializers.factories import named_tuple from pydantic_xml.serializers.serializer import Serializer def from_core_schema(schema: pcs.CallSchema, ctx: Serializer.Context) -> Serializer: - func = schema['function'] - - if inspect.isclass(func) and issubclass(func, tuple): - return named_tuple.from_core_schema(schema, ctx) - else: - raise errors.ModelError("type call is not supported") + raise errors.ModelError("type call is not supported") diff --git a/pydantic_xml/serializers/factories/homogeneous.py b/pydantic_xml/serializers/factories/homogeneous.py index 3d134ed..59c773d 100644 --- a/pydantic_xml/serializers/factories/homogeneous.py +++ b/pydantic_xml/serializers/factories/homogeneous.py @@ -123,6 +123,7 @@ def from_core_schema(schema: HomogeneousCollectionTypeSchema, ctx: Serializer.Co SchemaTypeFamily.IS_INSTANCE, SchemaTypeFamily.CALL, SchemaTypeFamily.TUPLE, + SchemaTypeFamily.NAMED_TUPLE, ): raise errors.ModelFieldError( ctx.model_name, ctx.field_name, "collection item must be of primitive, model, mapping, union or tuple type", @@ -133,6 +134,7 @@ def from_core_schema(schema: HomogeneousCollectionTypeSchema, ctx: Serializer.Co SchemaTypeFamily.UNION, SchemaTypeFamily.TAGGED_UNION, SchemaTypeFamily.TUPLE, + SchemaTypeFamily.NAMED_TUPLE, SchemaTypeFamily.CALL, ) and ctx.entity_location is None: raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided") diff --git a/pydantic_xml/serializers/factories/named_tuple.py b/pydantic_xml/serializers/factories/named_tuple.py index 426a094..bb3a611 100644 --- a/pydantic_xml/serializers/factories/named_tuple.py +++ b/pydantic_xml/serializers/factories/named_tuple.py @@ -1,4 +1,3 @@ -import typing from typing import Any, Dict, List, Optional, Tuple from pydantic_core import core_schema as pcs @@ -12,11 +11,11 @@ class ElementSerializer(Serializer): @classmethod - def from_core_schema(cls, schema: pcs.ArgumentsSchema, ctx: Serializer.Context) -> 'ElementSerializer': + def from_core_schema(cls, schema: pcs.NamedTupleSchema, ctx: Serializer.Context) -> 'ElementSerializer': model_name = ctx.model_name computed = ctx.field_computed inner_serializers: List[Serializer] = [] - for argument_schema in schema['arguments_schema']: + for argument_schema in schema['fields']: param_schema = argument_schema['schema'] inner_serializers.append(Serializer.parse_core_schema(param_schema, ctx)) @@ -61,10 +60,10 @@ def deserialize( ) -def from_core_schema(schema: pcs.CallSchema, ctx: Serializer.Context) -> Serializer: - arguments_schema = typing.cast(pcs.ArgumentsSchema, schema['arguments_schema']) - for argument_schema in arguments_schema['arguments_schema']: - param_schema = argument_schema['schema'] +def from_core_schema(schema: pcs.NamedTupleSchema, ctx: Serializer.Context) -> Serializer: + print(schema) + for field_schema in schema['fields']: + param_schema = field_schema['schema'] param_schema, ctx = Serializer.preprocess_schema(param_schema, ctx) param_type_family = TYPE_FAMILY.get(param_schema['type']) @@ -86,9 +85,9 @@ def from_core_schema(schema: pcs.CallSchema, ctx: Serializer.Context) -> Seriali raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided") if ctx.entity_location is EntityLocation.ELEMENT: - return ElementSerializer.from_core_schema(arguments_schema, ctx) + return ElementSerializer.from_core_schema(schema, ctx) elif ctx.entity_location is None: - return ElementSerializer.from_core_schema(arguments_schema, ctx) + return ElementSerializer.from_core_schema(schema, ctx) elif ctx.entity_location is EntityLocation.ATTRIBUTE: raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "attributes of tuple types are not supported") else: diff --git a/pydantic_xml/serializers/serializer.py b/pydantic_xml/serializers/serializer.py index 40a4e53..8b08cc7 100644 --- a/pydantic_xml/serializers/serializer.py +++ b/pydantic_xml/serializers/serializer.py @@ -41,6 +41,7 @@ class SchemaTypeFamily(IntEnum): JSON_OR_PYTHON = 11 IS_INSTANCE = 12 CALL = 13 + NAMED_TUPLE = 14 TYPE_FAMILY = { @@ -68,6 +69,7 @@ class SchemaTypeFamily(IntEnum): 'model': SchemaTypeFamily.MODEL, 'tuple': SchemaTypeFamily.TUPLE, + 'named-tuple': SchemaTypeFamily.NAMED_TUPLE, 'list': SchemaTypeFamily.HOMOGENEOUS_COLLECTION, 'set': SchemaTypeFamily.HOMOGENEOUS_COLLECTION, 'frozenset': SchemaTypeFamily.HOMOGENEOUS_COLLECTION, @@ -246,6 +248,10 @@ def select_serializer(cls, schema: pcs.CoreSchema, ctx: Context) -> 'Serializer' schema = typing.cast(pcs.TupleSchema, schema) return factories.tuple.from_core_schema(schema, ctx) + elif type_family is SchemaTypeFamily.NAMED_TUPLE: + schema = typing.cast(pcs.NamedTupleSchema, schema) + return factories.named_tuple.from_core_schema(schema, ctx) + elif type_family is SchemaTypeFamily.MAPPING: schema = typing.cast(pcs.DictSchema, schema) return factories.mapping.from_core_schema(schema, ctx) diff --git a/pyproject.toml b/pyproject.toml index 52153c0..2ea6f3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,20 +23,19 @@ classifiers = [ "Framework :: Pydantic :: 1", "Framework :: Pydantic :: 2", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Typing :: Typed", ] [tool.poetry.dependencies] -python = ">=3.8" +python = ">=3.10" lxml = {version = ">=4.9.0", optional = true} -pydantic = ">=2.6.0, !=2.10.0b1" +pydantic = ">=2.14.0" pydantic-core = ">=2.15.0" furo = {version = "^2022.12.7", optional = true} From e6abbb49f8bc265f8ca8e30a817b902d5ce0f7cd Mon Sep 17 00:00:00 2001 From: Dmitry Pershin Date: Sun, 18 Oct 2026 19:29:17 +0500 Subject: [PATCH 2/2] tests: importlib replaced by runpy for testing examples. --- tests/test_examples.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 81edd9e..ab1cf05 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,4 +1,4 @@ -import importlib.machinery +import runpy import sys from pathlib import Path @@ -12,22 +12,19 @@ @pytest.mark.parametrize('snippet', list((EXAMPLES_PATH / 'snippets').glob('*.py')), ids=lambda p: p.name) def test_snippets(snippet: Path): - loader = importlib.machinery.SourceFileLoader('snippet', str(snippet)) - loader.load_module('snippet') + runpy.run_path(str(snippet), run_name='snippet') @pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 and above") @pytest.mark.parametrize('snippet', list((EXAMPLES_PATH / 'snippets' / 'py3.9').glob('*.py')), ids=lambda p: p.name) def test_snippets_py39(snippet: Path): - loader = importlib.machinery.SourceFileLoader('snippet', str(snippet)) - loader.load_module('snippet') + runpy.run_path(str(snippet), run_name='snippet') @pytest.mark.skipif(not is_lxml_native(), reason='not lxml used') @pytest.mark.parametrize('snippet', list((EXAMPLES_PATH / 'snippets' / 'lxml').glob('*.py')), ids=lambda p: p.name) def test_snippets_py39(snippet: Path): - loader = importlib.machinery.SourceFileLoader('snippet', str(snippet)) - loader.load_module('snippet') + runpy.run_path(str(snippet), run_name='snippet') @pytest.fixture( @@ -54,5 +51,4 @@ def example_dir(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) def test_example(example_dir): - loader = importlib.machinery.SourceFileLoader('example', str(example_dir / 'model.py')) - loader.load_module('example') + runpy.run_path(str(example_dir / 'model.py'), run_name='example')