Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
pydantic-version: ["1.10.*", "2.*"]
exclude:
- python-version: "3.14"
pydantic-version: "1.10.*"

services:
postgres:
Expand Down
44 changes: 21 additions & 23 deletions django_pydantic_field/compat/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def unwrap(cls, value):
# This is a fallback for Python < 3.8, please be careful with that
return origin[unwrapped_args]
except TypeError:
return GenericAlias(origin, unwrapped_args)
return types.GenericAlias(origin, unwrapped_args)

def __eq__(self, other):
if isinstance(other, GenericTypes):
Expand Down Expand Up @@ -297,18 +297,14 @@ def serialize(self):

AnnotatedAlias = te._AnnotatedAlias

if sys.version_info >= (3, 9):
GenericAlias = types.GenericAlias
GenericTypes: ty.Tuple[ty.Any, ...] = (
GenericAlias,
if sys.version_info >= (3, 14):
GenericTypes: ty.Tuple[ty.Any, ...] = (types.GenericAlias, type(ty.List[int]), type(ty.List), ty.Union)
else:
GenericTypes = (
types.GenericAlias,
type(ty.List[int]),
type(ty.List),
)
else:
# types.GenericAlias is missing, meaning python version < 3.9,
# which has a different inheritance models for typed generics
GenericAlias = type(ty.List[int]) # noqa
GenericTypes = GenericAlias, type(ty.List) # noqa


# BaseContainerSerializer *must be* registered after all specialized container serializers
Expand All @@ -325,23 +321,25 @@ def serialize(self):

MigrationWriter.register_serializer(ty.ForwardRef, TypingSerializer)
MigrationWriter.register_serializer(type(ty.Union), TypingSerializer) # type: ignore
MigrationWriter.register_serializer(ty._SpecialForm, TypingSerializer) # type: ignore


UnionType = types.UnionType

if sys.version_info >= (3, 10):
UnionType = types.UnionType

class UnionTypeSerializer(BaseSerializer):
value: UnionType
class UnionTypeSerializer(BaseSerializer):
value: UnionType

def serialize(self):
imports = set()
if isinstance(self.value, (type(ty.Union), types.UnionType)): # type: ignore
imports.add("import typing")

def serialize(self):
imports = set()
if isinstance(self.value, (type(ty.Union), types.UnionType)): # type: ignore
imports.add("import typing")
for arg in get_args(self.value):
_, arg_imports = serializer_factory(arg).serialize()
imports.update(arg_imports)

for arg in get_args(self.value):
_, arg_imports = serializer_factory(arg).serialize()
imports.update(arg_imports)
return repr(self.value), imports

return repr(self.value), imports

MigrationWriter.register_serializer(UnionType, UnionTypeSerializer)
MigrationWriter.register_serializer(UnionType, UnionTypeSerializer)
28 changes: 15 additions & 13 deletions django_pydantic_field/v2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,22 @@
if ty.TYPE_CHECKING:
from collections.abc import Mapping

get_annotations: ty.Callable[[ty.Any], dict[str, ty.Any]]

def get_annotated_type(obj, field, default=None) -> ty.Any:
try:
try:
from annotationlib import get_annotations # Python >= 3.14
except ImportError:

def get_annotations(obj: ty.Any) -> dict[str, ty.Any]:
if isinstance(obj, type):
annotations = obj.__dict__["__annotations__"]
return obj.__dict__["__annotations__"]
else:
annotations = obj.__annotations__
return obj.__annotations__


def get_annotated_type(obj, field, default=None) -> ty.Any:
try:
annotations = get_annotations(obj)

return annotations[field]
except (AttributeError, KeyError):
Expand Down Expand Up @@ -48,12 +57,5 @@ def get_origin_type(cls: type):
return cls


if sys.version_info >= (3, 9):

def evaluate_forward_ref(ref: ty.ForwardRef, ns: Mapping[str, ty.Any]) -> ty.Any:
return ref._evaluate(dict(ns), {}, recursive_guard=frozenset())

else:

def evaluate_forward_ref(ref: ty.ForwardRef, ns: Mapping[str, ty.Any]) -> ty.Any:
return ref._evaluate(dict(ns), {})
def evaluate_forward_ref(ref: ty.ForwardRef, ns: Mapping[str, ty.Any]) -> ty.Any:
return ref._evaluate(dict(ns), {}, recursive_guard=frozenset())
12 changes: 5 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,14 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"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",
]

requires-python = ">=3.8"
requires-python = ">=3.10"
dependencies = [
"pydantic>=1.10,<3",
"django>=3.1,<6",
Expand All @@ -61,8 +60,8 @@ dev = [
"pre-commit",
"pytest~=7.4",
"djangorestframework>=3.11,<4",
"django-stubs[compatible-mypy]~=4.2",
"djangorestframework-stubs[compatible-mypy]~=3.14",
"django-stubs[compatible-mypy]~=5.2.3",
"djangorestframework-stubs[compatible-mypy]~=3.16.3",
"pytest-django>=4.5,<6",
]
test = [
Expand All @@ -73,8 +72,7 @@ test = [
"syrupy>=3,<5",
]
ci = [
'psycopg[binary]>=3.1,<4; python_version>="3.9"',
'psycopg2-binary>=2.7,<3; python_version<"3.9"',
'psycopg[binary]>=3.1,<4',
"mysqlclient>=2.1",
]

Expand Down
20 changes: 0 additions & 20 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ def test_field_serialization(field):
_test_field_serialization(field)


@pytest.mark.skipif(sys.version_info < (3, 9), reason="Built-in type subscription supports only in 3.9+")
@pytest.mark.parametrize(
"field_factory",
[
Expand All @@ -161,30 +160,11 @@ def test_field_builtin_annotations_serialization(field_factory):
_test_field_serialization(field_factory())


@pytest.mark.skipif(sys.version_info < (3, 10), reason="Union type syntax supported only in 3.10+")
def test_field_union_type_serialization():
field = fields.PydanticSchemaField(schema=(InnerSchema | None), null=True, default=None)
_test_field_serialization(field)


@pytest.mark.skipif(sys.version_info >= (3, 9), reason="Should test against builtin generic types")
@pytest.mark.parametrize(
"field",
[
fields.PydanticSchemaField(schema=ty.List[InnerSchema], default=list),
fields.PydanticSchemaField(schema=ty.Dict[str, InnerSchema], default=dict),
fields.PydanticSchemaField(schema=ty.Sequence[InnerSchema], default=list),
fields.PydanticSchemaField(schema=ty.Mapping[str, InnerSchema], default=dict),
],
)
def test_field_typing_annotations_serialization(field):
_test_field_serialization(field)


@pytest.mark.skipif(
sys.version_info < (3, 9),
reason="Typing-to-builtin migrations is reasonable only on py >= 3.9",
)
@pytest.mark.parametrize(
"old_field, new_field",
[
Expand Down
42 changes: 20 additions & 22 deletions tests/test_migration_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,27 @@
import pytest

import django_pydantic_field

try:
from django_pydantic_field.compat.django import GenericContainer
except ImportError:
from django_pydantic_field._migration_serializers import GenericContainer # noqa

if sys.version_info < (3, 9):
test_types = [
str,
list,
t.List[str],
t.Union[te.Literal["foo"], t.List[str]],
t.List[t.Union[int, bool]],
t.Tuple[t.List[te.Literal[1]], t.Union[str, te.Literal["foo"]]],
t.ForwardRef("str"),
]
else:
test_types = [
str,
list,
list[str],
t.Union[t.Literal["foo"], list[str]],
list[t.Union[int, bool]],
tuple[list[t.Literal[1]], t.Union[str, t.Literal["foo"]]],
t.ForwardRef("str"),
]
try:
import annotationlib
except ImportError:
annotationlib = None

test_types = [
str,
list,
list[str],
t.Literal["foo"],
t.Union[t.Literal["foo"], list[str]],
list[t.Union[int, bool]],
tuple[list[t.Literal[1]], t.Union[str, t.Literal["foo"]]],
t.ForwardRef("str"),
]


@pytest.mark.parametrize("raw_type", test_types)
Expand All @@ -42,6 +38,8 @@ def test_wrap_unwrap_idempotent(raw_type):
@pytest.mark.parametrize("raw_type", test_types)
def test_serialize_eval_idempotent(raw_type):
raw_type = GenericContainer.wrap(raw_type)
expression, _ = MigrationWriter.serialize(GenericContainer.wrap(raw_type))
imports = dict(typing=t, typing_extensions=te, django_pydantic_field=django_pydantic_field)
expression, _ = MigrationWriter.serialize(raw_type)
imports = dict(
typing=t, typing_extensions=te, django_pydantic_field=django_pydantic_field, annotationlib=annotationlib
)
assert eval(expression, imports) == raw_type
1 change: 0 additions & 1 deletion tests/v1/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ def test_concrete_types(type_, encoded, decoded):
assert decoder.decode(existing_encoded) == decoded


@pytest.mark.skipif(sys.version_info < (3, 9), reason="Should test against builtin generic types")
@pytest.mark.parametrize(
"type_factory, encoded, decoded",
[
Expand Down
13 changes: 4 additions & 9 deletions tests/v2/test_types.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import sys
import pydantic
import pytest
import typing as ty

from ..conftest import InnerSchema, SampleDataclass

types = pytest.importorskip("django_pydantic_field.v2.types")
skip_unsupported_builtin_subscription = pytest.mark.skipif(
sys.version_info < (3, 9),
reason="Built-in type subscription supports only in 3.9+",
)


# fmt: off
@pytest.mark.parametrize(
"ctor, args, kwargs",
[
pytest.param(types.SchemaAdapter, ["list[int]", None, None, None], {}, marks=skip_unsupported_builtin_subscription),
pytest.param(types.SchemaAdapter, ["list[int]", {"strict": True}, None, None], {}, marks=skip_unsupported_builtin_subscription),
pytest.param(types.SchemaAdapter, ["list[int]", None, None, None], {}),
pytest.param(types.SchemaAdapter, ["list[int]", {"strict": True}, None, None], {}),
(types.SchemaAdapter, [ty.List[int], None, None, None], {}),
(types.SchemaAdapter, [ty.List[int], {"strict": True}, None, None], {}),
(types.SchemaAdapter, [None, None, InnerSchema, "stub_int"], {}),
(types.SchemaAdapter, [None, None, SampleDataclass, "stub_int"], {}),
pytest.param(types.SchemaAdapter.from_type, ["list[int]"], {}, marks=skip_unsupported_builtin_subscription),
pytest.param(types.SchemaAdapter.from_type, ["list[int]", {"strict": True}], {}, marks=skip_unsupported_builtin_subscription),
pytest.param(types.SchemaAdapter.from_type, ["list[int]"], {}),
pytest.param(types.SchemaAdapter.from_type, ["list[int]", {"strict": True}], {}),
(types.SchemaAdapter.from_type, [ty.List[int]], {}),
(types.SchemaAdapter.from_type, [ty.List[int], {"strict": True}], {}),
(types.SchemaAdapter.from_annotation, [InnerSchema, "stub_int"], {}),
Expand Down