From f7d86eca0c260ccada9f569ae816148958573985 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:14:14 +0100 Subject: [PATCH 01/19] =?UTF-8?q?=F0=9F=93=9D=20Fix=20type=20alias=20handl?= =?UTF-8?q?ing=20in=20=5Fcompat.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 230f8cc362..5b024cc9ca 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -81,6 +81,7 @@ def partial_init() -> Generator[None, None, None]: from pydantic._internal._repr import Representation as Representation from pydantic_core import PydanticUndefined as Undefined from pydantic_core import PydanticUndefinedType as UndefinedType + from typing import TypeAliasType # Dummy for types, to make it importable class ModelField: @@ -203,6 +204,8 @@ def get_sa_type_from_type_annotation(annotation: Any) -> Any: # Resolve Optional fields if annotation is None: raise ValueError("Missing field type") + if isinstance(annotation, TypeAliasType): + annotation = annotation.__value__ origin = get_origin(annotation) if origin is None: return annotation From a7615de8db797b2f3fbc314fd7b2000b9982c1b8 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:14:22 +0100 Subject: [PATCH 02/19] =?UTF-8?q?=F0=9F=93=9D=20Add=20tests=20for=20type?= =?UTF-8?q?=20alias=20handling=20in=20test=5Ffield=5Fsa=5Ftype.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_field_sa_type.py | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_field_sa_type.py diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py new file mode 100644 index 0000000000..d8f3aae7ab --- /dev/null +++ b/tests/test_field_sa_type.py @@ -0,0 +1,54 @@ +from sys import version_info +from typing import Annotated, TypeAlias + +import pytest +from sqlmodel import Field, SQLModel + +Type5: TypeAlias = str +Type6: TypeAlias = Annotated[str, "Just a comment"] + + +@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +def test_sa_type_1() -> None: + Type1 = str + + class Hero1(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type1 = 'sword' + +@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +def test_sa_type_2() -> None: + Type2 = Annotated[str, "Just a comment"] + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type2 = 'sword' + +@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +def test_sa_type_3() -> None: + type Type3 = str + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type3 = 'sword' + +@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +def test_sa_type_4() -> None: + type Type4 = Annotated[str, "Just a comment"] + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type4 = 'sword' + +@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +def test_sa_type_5() -> None: + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type5 = 'sword' + +@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +def test_sa_type_6() -> None: + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type6 = 'sword' From 105a8e554ddb13ccb18854497cafcdaab0b91441 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 01:32:04 +0100 Subject: [PATCH 03/19] =?UTF-8?q?=F0=9F=93=9D=20Update=20type=20alias=20de?= =?UTF-8?q?finitions=20in=20tests=20and=20compatibility=20module=20for=20P?= =?UTF-8?q?ython=203.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 2 +- tests/test_field_sa_type.py | 33 ++++++++++++++++++++------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 5b024cc9ca..7631316f89 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -15,6 +15,7 @@ Optional, Set, Type, + TypeAliasType, TypeVar, Union, ) @@ -81,7 +82,6 @@ def partial_init() -> Generator[None, None, None]: from pydantic._internal._repr import Representation as Representation from pydantic_core import PydanticUndefined as Undefined from pydantic_core import PydanticUndefinedType as UndefinedType - from typing import TypeAliasType # Dummy for types, to make it importable class ModelField: diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index d8f3aae7ab..1e17ce222e 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -4,9 +4,6 @@ import pytest from sqlmodel import Field, SQLModel -Type5: TypeAlias = str -Type6: TypeAlias = Annotated[str, "Just a comment"] - @pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") def test_sa_type_1() -> None: @@ -14,7 +11,8 @@ def test_sa_type_1() -> None: class Hero1(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type1 = 'sword' + weapon: Type1 = "sword" + @pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") def test_sa_type_2() -> None: @@ -22,33 +20,42 @@ def test_sa_type_2() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type2 = 'sword' + weapon: Type2 = "sword" + + +Type3: TypeAlias = str + @pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") def test_sa_type_3() -> None: - type Type3 = str - class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type3 = 'sword' + weapon: Type3 = "sword" + + +Type4: TypeAlias = Annotated[str, "Just a comment"] + @pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") def test_sa_type_4() -> None: - type Type4 = Annotated[str, "Just a comment"] - class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type4 = 'sword' + weapon: Type4 = "sword" + @pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") def test_sa_type_5() -> None: + type Type5 = str class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type5 = 'sword' + weapon: Type5 = "sword" + @pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") def test_sa_type_6() -> None: + type Type6 = Annotated[str, "Just a comment"] + class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type6 = 'sword' + weapon: Type6 = "sword" From 9d636578391715e32820cb2994b25352544df1dd Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 01:41:28 +0100 Subject: [PATCH 04/19] =?UTF-8?q?=F0=9F=93=9D=20Update=20type=20alias=20ha?= =?UTF-8?q?ndling=20for=20Python=203.12=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 6 ++++-- tests/conftest.py | 3 +++ tests/test_field_sa_type.py | 16 ++++++++-------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 7631316f89..8ef279d8ce 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -15,7 +15,6 @@ Optional, Set, Type, - TypeAliasType, TypeVar, Union, ) @@ -75,6 +74,9 @@ def partial_init() -> Generator[None, None, None]: if IS_PYDANTIC_V2: + if sys.version_info >= (3, 12): + from typing import TypeAliasType + from annotated_types import MaxLen from pydantic import ConfigDict as BaseConfig from pydantic._internal._fields import PydanticMetadata @@ -204,7 +206,7 @@ def get_sa_type_from_type_annotation(annotation: Any) -> Any: # Resolve Optional fields if annotation is None: raise ValueError("Missing field type") - if isinstance(annotation, TypeAliasType): + if sys.version_info >= (3, 12) and isinstance(annotation, TypeAliasType): annotation = annotation.__value__ origin = get_origin(annotation) if origin is None: diff --git a/tests/conftest.py b/tests/conftest.py index 98a4d2b7e6..204c28b9ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,3 +93,6 @@ def print_mock_fixture() -> Generator[PrintMock, None, None]: needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) +needs_py312 = pytest.mark.skipif( + sys.version_info < (3, 12), reason="requires python3.12+" +) diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index 1e17ce222e..f8176017a0 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -1,11 +1,11 @@ -from sys import version_info from typing import Annotated, TypeAlias -import pytest from sqlmodel import Field, SQLModel +from tests.conftest import needs_py312 -@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") + +@needs_py312 def test_sa_type_1() -> None: Type1 = str @@ -14,7 +14,7 @@ class Hero1(SQLModel, table=True): weapon: Type1 = "sword" -@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +@needs_py312 def test_sa_type_2() -> None: Type2 = Annotated[str, "Just a comment"] @@ -26,7 +26,7 @@ class Hero(SQLModel, table=True): Type3: TypeAlias = str -@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +@needs_py312 def test_sa_type_3() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) @@ -36,14 +36,14 @@ class Hero(SQLModel, table=True): Type4: TypeAlias = Annotated[str, "Just a comment"] -@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +@needs_py312 def test_sa_type_4() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type4 = "sword" -@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +@needs_py312 def test_sa_type_5() -> None: type Type5 = str @@ -52,7 +52,7 @@ class Hero(SQLModel, table=True): weapon: Type5 = "sword" -@pytest.mark.skipif(version_info[1] < 12, reason="Language feature of Python 3.12+") +@needs_py312 def test_sa_type_6() -> None: type Type6 = Annotated[str, "Just a comment"] From 5954463ebbb6be48e2dfe4d243805b5ae3633f36 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 01:44:08 +0100 Subject: [PATCH 05/19] =?UTF-8?q?=F0=9F=93=9D=20Refactor=20tests=20in=20te?= =?UTF-8?q?st=5Ffield=5Fsa=5Ftype.py=20for=20Python=203.12=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_field_sa_type.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index f8176017a0..7c873b2fce 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -1,3 +1,4 @@ +import sys from typing import Annotated, TypeAlias from sqlmodel import Field, SQLModel @@ -43,19 +44,20 @@ class Hero(SQLModel, table=True): weapon: Type4 = "sword" -@needs_py312 -def test_sa_type_5() -> None: - type Type5 = str +if sys.version_info >= (3, 12): - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type5 = "sword" + @needs_py312 + def test_sa_type_5() -> None: + type Type5 = str + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type5 = "sword" -@needs_py312 -def test_sa_type_6() -> None: - type Type6 = Annotated[str, "Just a comment"] + @needs_py312 + def test_sa_type_6() -> None: + type Type6 = Annotated[str, "Just a comment"] - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type6 = "sword" + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type6 = "sword" From 69a2b21edca183712d92bee8cb48777ad85cd486 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:22:41 +0100 Subject: [PATCH 06/19] =?UTF-8?q?=F0=9F=93=9D=20Enhance=20type=20alias=20h?= =?UTF-8?q?andling=20in=20compatibility=20module=20and=20tests=20for=20Pyt?= =?UTF-8?q?hon=203.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 20 +++++-- tests/test_field_sa_type.py | 109 +++++++++++++++++++++++++++--------- 2 files changed, 98 insertions(+), 31 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 8ef279d8ce..6c1c602d28 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -1,5 +1,6 @@ import sys import types +import typing from contextlib import contextmanager from contextvars import ContextVar from dataclasses import dataclass @@ -19,6 +20,7 @@ Union, ) +import typing_extensions from pydantic import VERSION as P_VERSION from pydantic import BaseModel from pydantic.fields import FieldInfo @@ -74,9 +76,6 @@ def partial_init() -> Generator[None, None, None]: if IS_PYDANTIC_V2: - if sys.version_info >= (3, 12): - from typing import TypeAliasType - from annotated_types import MaxLen from pydantic import ConfigDict as BaseConfig from pydantic._internal._fields import PydanticMetadata @@ -202,11 +201,24 @@ def is_field_noneable(field: "FieldInfo") -> bool: return False return False + def _is_type_alias_type_instance(annotation: Any) -> bool: + type_to_check = "TypeAliasType" + in_typing = hasattr(typing, type_to_check) + in_typing_extensions = hasattr(typing_extensions, type_to_check) + + check_type = [] + if in_typing: + check_type.append(typing.TypeAliasType) + if in_typing_extensions: + check_type.append(typing_extensions.TypeAliasType) + + return check_type and isinstance(annotation, tuple(check_type)) + def get_sa_type_from_type_annotation(annotation: Any) -> Any: # Resolve Optional fields if annotation is None: raise ValueError("Missing field type") - if sys.version_info >= (3, 12) and isinstance(annotation, TypeAliasType): + if _is_type_alias_type_instance(annotation): annotation = annotation.__value__ origin = get_origin(annotation) if origin is None: diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index 7c873b2fce..56d8978048 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -1,63 +1,118 @@ -import sys -from typing import Annotated, TypeAlias +import typing as t +import typing_extensions as te from sqlmodel import Field, SQLModel from tests.conftest import needs_py312 @needs_py312 -def test_sa_type_1() -> None: - Type1 = str +def test_sa_type_typing_1() -> None: + Type1_t = str class Hero1(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type1 = "sword" + weapon: Type1_t = "sword" @needs_py312 -def test_sa_type_2() -> None: - Type2 = Annotated[str, "Just a comment"] +def test_sa_type_typing_2() -> None: + Type2_t = t.Annotated[str, "Just a comment"] class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type2 = "sword" + weapon: Type2_t = "sword" -Type3: TypeAlias = str +Type3_t: t.TypeAlias = str @needs_py312 -def test_sa_type_3() -> None: +def test_sa_type_typing_3() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type3 = "sword" + weapon: Type3_t = "sword" -Type4: TypeAlias = Annotated[str, "Just a comment"] +Type4_t: t.TypeAlias = t.Annotated[str, "Just a comment"] @needs_py312 -def test_sa_type_4() -> None: +def test_sa_type_typing_4() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) - weapon: Type4 = "sword" + weapon: Type4_t = "sword" -if sys.version_info >= (3, 12): +@needs_py312 +def test_sa_type_typing_5() -> None: + type Type5_t = str + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type5_t = "sword" + + +@needs_py312 +def test_sa_type_typing_6() -> None: + type Type6_t = t.Annotated[str, "Just a comment"] + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type6_t = "sword" + + +@needs_py312 +def test_sa_type_typing_extensions_1() -> None: + Type1_te = str + + class Hero1(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type1_te = "sword" + + +@needs_py312 +def test_sa_type_typing_extensions_2() -> None: + Type2_te = te.Annotated[str, "Just a comment"] + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type2_te = "sword" - @needs_py312 - def test_sa_type_5() -> None: - type Type5 = str - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type5 = "sword" +Type3_te: te.TypeAlias = str - @needs_py312 - def test_sa_type_6() -> None: - type Type6 = Annotated[str, "Just a comment"] - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type6 = "sword" +@needs_py312 +def test_sa_type_typing_extensions_3() -> None: + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type3_te = "sword" + + +Type4_te: te.TypeAlias = te.Annotated[str, "Just a comment"] + + +@needs_py312 +def test_sa_type_typing_extensions_4() -> None: + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type4_te = "sword" + + +@needs_py312 +def test_sa_type_typing_extensions_5() -> None: + type Type5_te = str + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type5_te = "sword" + + +@needs_py312 +def test_sa_type_typing_extensions_6() -> None: + type Type6_te = te.Annotated[str, "Just a comment"] + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type6_te = "sword" From 05d32daa8fb601676cf6f3b8e617e4152cb82b5e Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:24:09 +0100 Subject: [PATCH 07/19] =?UTF-8?q?=F0=9F=93=9D=20Remove=20Python=203.12=20c?= =?UTF-8?q?ompatibility=20marker=20from=20tests=20in=20test=5Ffield=5Fsa?= =?UTF-8?q?=5Ftype.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 3 --- tests/test_field_sa_type.py | 14 -------------- 2 files changed, 17 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 204c28b9ea..98a4d2b7e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,6 +93,3 @@ def print_mock_fixture() -> Generator[PrintMock, None, None]: needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) -needs_py312 = pytest.mark.skipif( - sys.version_info < (3, 12), reason="requires python3.12+" -) diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index 56d8978048..2df19f6c08 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -3,10 +3,7 @@ import typing_extensions as te from sqlmodel import Field, SQLModel -from tests.conftest import needs_py312 - -@needs_py312 def test_sa_type_typing_1() -> None: Type1_t = str @@ -15,7 +12,6 @@ class Hero1(SQLModel, table=True): weapon: Type1_t = "sword" -@needs_py312 def test_sa_type_typing_2() -> None: Type2_t = t.Annotated[str, "Just a comment"] @@ -27,7 +23,6 @@ class Hero(SQLModel, table=True): Type3_t: t.TypeAlias = str -@needs_py312 def test_sa_type_typing_3() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) @@ -37,14 +32,12 @@ class Hero(SQLModel, table=True): Type4_t: t.TypeAlias = t.Annotated[str, "Just a comment"] -@needs_py312 def test_sa_type_typing_4() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type4_t = "sword" -@needs_py312 def test_sa_type_typing_5() -> None: type Type5_t = str @@ -53,7 +46,6 @@ class Hero(SQLModel, table=True): weapon: Type5_t = "sword" -@needs_py312 def test_sa_type_typing_6() -> None: type Type6_t = t.Annotated[str, "Just a comment"] @@ -62,7 +54,6 @@ class Hero(SQLModel, table=True): weapon: Type6_t = "sword" -@needs_py312 def test_sa_type_typing_extensions_1() -> None: Type1_te = str @@ -71,7 +62,6 @@ class Hero1(SQLModel, table=True): weapon: Type1_te = "sword" -@needs_py312 def test_sa_type_typing_extensions_2() -> None: Type2_te = te.Annotated[str, "Just a comment"] @@ -83,7 +73,6 @@ class Hero(SQLModel, table=True): Type3_te: te.TypeAlias = str -@needs_py312 def test_sa_type_typing_extensions_3() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) @@ -93,14 +82,12 @@ class Hero(SQLModel, table=True): Type4_te: te.TypeAlias = te.Annotated[str, "Just a comment"] -@needs_py312 def test_sa_type_typing_extensions_4() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type4_te = "sword" -@needs_py312 def test_sa_type_typing_extensions_5() -> None: type Type5_te = str @@ -109,7 +96,6 @@ class Hero(SQLModel, table=True): weapon: Type5_te = "sword" -@needs_py312 def test_sa_type_typing_extensions_6() -> None: type Type6_te = te.Annotated[str, "Just a comment"] From a05a68565e33c791f4d921c55297d72630131be6 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:30:46 +0100 Subject: [PATCH 08/19] =?UTF-8?q?=F0=9F=93=9D=20Add=20handling=20for=20Gen?= =?UTF-8?q?ericAlias=20in=20type=20alias=20checks=20for=20Python=203.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 6c1c602d28..5cac4b7e25 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -212,6 +212,11 @@ def _is_type_alias_type_instance(annotation: Any) -> bool: if in_typing_extensions: check_type.append(typing_extensions.TypeAliasType) + if sys.version_info[:2] == (3, 10): + if type(annotation) is types.GenericAlias: + # In Python 3.10, TypeAliasType instances are of type GenericAlias + return False + return check_type and isinstance(annotation, tuple(check_type)) def get_sa_type_from_type_annotation(annotation: Any) -> Any: From fc9a2e67f1e21288348fb445e0200fd3b357e543 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:45:35 +0100 Subject: [PATCH 09/19] =?UTF-8?q?=F0=9F=93=9D=20Refactor=20type=20alias=20?= =?UTF-8?q?handling=20and=20add=20tests=20for=20NewType=20and=20TypeVar=20?= =?UTF-8?q?support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 39 +++++++++++++++++++---------- tests/test_field_sa_type.py | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 5cac4b7e25..a3dc7021af 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -31,7 +31,6 @@ PYDANTIC_MINOR_VERSION = tuple(int(i) for i in P_VERSION.split(".")[:2]) IS_PYDANTIC_V2 = PYDANTIC_MINOR_VERSION[0] == 2 - if TYPE_CHECKING: from .main import RelationshipInfo, SQLModel @@ -201,29 +200,43 @@ def is_field_noneable(field: "FieldInfo") -> bool: return False return False - def _is_type_alias_type_instance(annotation: Any) -> bool: - type_to_check = "TypeAliasType" - in_typing = hasattr(typing, type_to_check) - in_typing_extensions = hasattr(typing_extensions, type_to_check) - + def _is_typing_type_instance(annotation: Any, type_name: str) -> bool: check_type = [] - if in_typing: - check_type.append(typing.TypeAliasType) - if in_typing_extensions: - check_type.append(typing_extensions.TypeAliasType) + if hasattr(typing, type_name): + check_type.append(getattr(typing, type_name)) + if hasattr(typing_extensions, type_name): + check_type.append(getattr(typing_extensions, type_name)) + + return check_type and isinstance(annotation, tuple(check_type)) + + def _is_new_type_instance(annotation: Any) -> bool: + return _is_typing_type_instance(annotation, "NewType") + def _is_type_var_instance(annotation: Any) -> bool: + return _is_typing_type_instance(annotation, "TypeVar") + + def _is_type_alias_type_instance(annotation: Any) -> bool: if sys.version_info[:2] == (3, 10): if type(annotation) is types.GenericAlias: - # In Python 3.10, TypeAliasType instances are of type GenericAlias + # In Python 3.10, GenericAlias instances are of type TypeAliasType return False - return check_type and isinstance(annotation, tuple(check_type)) + return _is_typing_type_instance(annotation, "TypeAliasType") def get_sa_type_from_type_annotation(annotation: Any) -> Any: # Resolve Optional fields if annotation is None: raise ValueError("Missing field type") - if _is_type_alias_type_instance(annotation): + if _is_type_var_instance(annotation): + annotation = annotation.__bound__ + if not annotation: + raise ValueError( + "TypeVars without a bound type cannot be converted to SQLAlchemy types" + ) + # annotations.__constraints__ could be used and defined Union[*constraints], but ORM does not support it + elif _is_new_type_instance(annotation): + annotation = annotation.__supertype__ + elif _is_type_alias_type_instance(annotation): annotation = annotation.__value__ origin = get_origin(annotation) if origin is None: diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index 2df19f6c08..39f3a343de 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -1,5 +1,6 @@ import typing as t +import pytest import typing_extensions as te from sqlmodel import Field, SQLModel @@ -54,6 +55,29 @@ class Hero(SQLModel, table=True): weapon: Type6_t = "sword" +def test_sa_type_typing_7() -> None: + Type7_t = t.NewType("Type7_t", str) + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type7_t = "sword" + + +def test_sa_type_typing_8() -> None: + Type8_t = t.TypeVar("Type8_t", bound=str) + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type8_t = "sword" + +def test_sa_type_typing_9() -> None: + Type9_t = t.TypeVar("Type9_t", str, bytes) + + with pytest.raises(ValueError): + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type9_t = "sword" + def test_sa_type_typing_extensions_1() -> None: Type1_te = str @@ -102,3 +126,28 @@ def test_sa_type_typing_extensions_6() -> None: class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type6_te = "sword" + + +def test_sa_type_typing_extensions_7() -> None: + Type7_te = te.NewType("Type7_te", str) + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type7_te = "sword" + + +def test_sa_type_typing_extensions_8() -> None: + Type8_te = te.TypeVar("Type8_te", bound=str) + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type8_te = "sword" + + +def test_sa_type_typing_extensions_9() -> None: + Type9_te = te.TypeVar("Type9_te", str, bytes) + + with pytest.raises(ValueError): + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type9_te = "sword" From 506e5c11b4a0b2865570cc3002e9fc3a3137acf8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:45:46 +0000 Subject: [PATCH 10/19] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_field_sa_type.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index 39f3a343de..7d171b6ac4 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -70,14 +70,17 @@ class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type8_t = "sword" + def test_sa_type_typing_9() -> None: Type9_t = t.TypeVar("Type9_t", str, bytes) with pytest.raises(ValueError): + class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type9_t = "sword" + def test_sa_type_typing_extensions_1() -> None: Type1_te = str @@ -148,6 +151,7 @@ def test_sa_type_typing_extensions_9() -> None: Type9_te = te.TypeVar("Type9_te", str, bytes) with pytest.raises(ValueError): + class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type9_te = "sword" From 80f7a091502532c4201666015f45d013e6bc79a8 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:48:57 +0100 Subject: [PATCH 11/19] =?UTF-8?q?=F0=9F=93=9D=20Fix=20type=20alias=20check?= =?UTF-8?q?=20to=20ensure=20check=5Ftype=20is=20not=20empty=20before=20val?= =?UTF-8?q?idation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index a3dc7021af..f746ebc54f 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -207,7 +207,7 @@ def _is_typing_type_instance(annotation: Any, type_name: str) -> bool: if hasattr(typing_extensions, type_name): check_type.append(getattr(typing_extensions, type_name)) - return check_type and isinstance(annotation, tuple(check_type)) + return bool(check_type) and isinstance(annotation, tuple(check_type)) def _is_new_type_instance(annotation: Any) -> bool: return _is_typing_type_instance(annotation, "NewType") From 8fe139b1864d2c520357e66915079f306aa31ee9 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:58:54 +0100 Subject: [PATCH 12/19] =?UTF-8?q?=F0=9F=93=9D=20Fix=20type=20alias=20check?= =?UTF-8?q?=20to=20ensure=20check=5Ftype=20is=20not=20empty=20before=20val?= =?UTF-8?q?idation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 3 +++ tests/test_field_sa_type.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 98a4d2b7e6..204c28b9ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,3 +93,6 @@ def print_mock_fixture() -> Generator[PrintMock, None, None]: needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) +needs_py312 = pytest.mark.skipif( + sys.version_info < (3, 12), reason="requires python3.12+" +) diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index 7d171b6ac4..40708c4ba3 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -1,9 +1,12 @@ import typing as t +from textwrap import dedent import pytest import typing_extensions as te from sqlmodel import Field, SQLModel +from tests.conftest import needs_py312 + def test_sa_type_typing_1() -> None: Type1_t = str @@ -39,20 +42,28 @@ class Hero(SQLModel, table=True): weapon: Type4_t = "sword" +@needs_py312 def test_sa_type_typing_5() -> None: + test_code = dedent(""" type Type5_t = str class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type5_t = "sword" + """) + exec(test_code, globals()) +@needs_py312 def test_sa_type_typing_6() -> None: + test_code = dedent(""" type Type6_t = t.Annotated[str, "Just a comment"] class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type6_t = "sword" + """) + exec(test_code, globals()) def test_sa_type_typing_7() -> None: @@ -115,20 +126,28 @@ class Hero(SQLModel, table=True): weapon: Type4_te = "sword" +@needs_py312 def test_sa_type_typing_extensions_5() -> None: + test_code = dedent(""" type Type5_te = str class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type5_te = "sword" + """) + exec(test_code, globals()) +@needs_py312 def test_sa_type_typing_extensions_6() -> None: + test_code = dedent(""" type Type6_te = te.Annotated[str, "Just a comment"] class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type6_te = "sword" + """) + exec(test_code, globals()) def test_sa_type_typing_extensions_7() -> None: From 563fa927b46df1b133a18de2d7b6d0c900544678 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:10:13 +0100 Subject: [PATCH 13/19] =?UTF-8?q?=F0=9F=93=9D=20Update=20type=20alias=20de?= =?UTF-8?q?finitions=20to=20support=20conditional=20assignment=20for=20Pyt?= =?UTF-8?q?hon=203.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_field_sa_type.py | 48 +++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index 40708c4ba3..56431ebe5a 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -24,22 +24,20 @@ class Hero(SQLModel, table=True): weapon: Type2_t = "sword" -Type3_t: t.TypeAlias = str +if hasattr(t, "TypeAlias"): + Type3_t: t.TypeAlias = str + def test_sa_type_typing_3() -> None: + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type3_t = "sword" -def test_sa_type_typing_3() -> None: - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type3_t = "sword" - - -Type4_t: t.TypeAlias = t.Annotated[str, "Just a comment"] - + Type4_t: t.TypeAlias = t.Annotated[str, "Just a comment"] -def test_sa_type_typing_4() -> None: - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type4_t = "sword" + def test_sa_type_typing_4() -> None: + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type4_t = "sword" @needs_py312 @@ -108,22 +106,20 @@ class Hero(SQLModel, table=True): weapon: Type2_te = "sword" -Type3_te: te.TypeAlias = str +if hasattr(te, "TypeAlias"): + Type3_te: te.TypeAlias = str + def test_sa_type_typing_extensions_3() -> None: + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type3_te = "sword" -def test_sa_type_typing_extensions_3() -> None: - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type3_te = "sword" - - -Type4_te: te.TypeAlias = te.Annotated[str, "Just a comment"] - + Type4_te: te.TypeAlias = te.Annotated[str, "Just a comment"] -def test_sa_type_typing_extensions_4() -> None: - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type4_te = "sword" + def test_sa_type_typing_extensions_4() -> None: + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type4_te = "sword" @needs_py312 From a86418c83b67ad50cc2e007ca297214ce8d30687 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:30:57 +0100 Subject: [PATCH 14/19] =?UTF-8?q?=F0=9F=93=9D=20Update=20tests=20to=20cond?= =?UTF-8?q?itionally=20define=20type=20aliases=20based=20on=20availability?= =?UTF-8?q?=20of=20Annotated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_field_sa_type.py | 46 +++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index 56431ebe5a..8f3bc46b85 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -16,12 +16,14 @@ class Hero1(SQLModel, table=True): weapon: Type1_t = "sword" -def test_sa_type_typing_2() -> None: - Type2_t = t.Annotated[str, "Just a comment"] +if hasattr(t, "Annotated"): - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type2_t = "sword" + def test_sa_type_typing_2() -> None: + Type2_t = t.Annotated[str, "Just a comment"] + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type2_t = "sword" if hasattr(t, "TypeAlias"): @@ -32,12 +34,13 @@ class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type3_t = "sword" - Type4_t: t.TypeAlias = t.Annotated[str, "Just a comment"] + if hasattr(t, "Annotated"): + Type4_t: t.TypeAlias = t.Annotated[str, "Just a comment"] - def test_sa_type_typing_4() -> None: - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type4_t = "sword" + def test_sa_type_typing_4() -> None: + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type4_t = "sword" @needs_py312 @@ -98,12 +101,14 @@ class Hero1(SQLModel, table=True): weapon: Type1_te = "sword" -def test_sa_type_typing_extensions_2() -> None: - Type2_te = te.Annotated[str, "Just a comment"] +if hasattr(te, "Annotated"): - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type2_te = "sword" + def test_sa_type_typing_extensions_2() -> None: + Type2_te = te.Annotated[str, "Just a comment"] + + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type2_te = "sword" if hasattr(te, "TypeAlias"): @@ -114,12 +119,13 @@ class Hero(SQLModel, table=True): pk: int = Field(primary_key=True) weapon: Type3_te = "sword" - Type4_te: te.TypeAlias = te.Annotated[str, "Just a comment"] + if hasattr(te, "Annotated"): + Type4_te: te.TypeAlias = te.Annotated[str, "Just a comment"] - def test_sa_type_typing_extensions_4() -> None: - class Hero(SQLModel, table=True): - pk: int = Field(primary_key=True) - weapon: Type4_te = "sword" + def test_sa_type_typing_extensions_4() -> None: + class Hero(SQLModel, table=True): + pk: int = Field(primary_key=True) + weapon: Type4_te = "sword" @needs_py312 From daadf432288c25dc3d0c6978986aa4442eb62428 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:36:29 +0100 Subject: [PATCH 15/19] =?UTF-8?q?=F0=9F=93=9D=20Update=20=5Fis=5Fnew=5Ftyp?= =?UTF-8?q?e=5Finstance=20to=20handle=20Python=203.10=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index f746ebc54f..972b5debb3 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -210,7 +210,10 @@ def _is_typing_type_instance(annotation: Any, type_name: str) -> bool: return bool(check_type) and isinstance(annotation, tuple(check_type)) def _is_new_type_instance(annotation: Any) -> bool: - return _is_typing_type_instance(annotation, "NewType") + if sys.version_info >= (3, 10): + return _is_typing_type_instance(annotation, "NewType") + else: + return hasattr(annotation, "__supertype__") def _is_type_var_instance(annotation: Any) -> bool: return _is_typing_type_instance(annotation, "TypeVar") From d128318acf5c54387471863a2b4d9a825a8cefb3 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:12:35 +0100 Subject: [PATCH 16/19] =?UTF-8?q?=F0=9F=93=9D=20Add=20needs=5Fpydanticv2?= =?UTF-8?q?=20decorator=20to=20tests=20for=20Pydantic=20v2=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_field_sa_type.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index 8f3bc46b85..e7318df4f0 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -5,7 +5,7 @@ import typing_extensions as te from sqlmodel import Field, SQLModel -from tests.conftest import needs_py312 +from tests.conftest import needs_py312, needs_pydanticv2 def test_sa_type_typing_1() -> None: @@ -44,6 +44,7 @@ class Hero(SQLModel, table=True): @needs_py312 +@needs_pydanticv2 def test_sa_type_typing_5() -> None: test_code = dedent(""" type Type5_t = str @@ -56,6 +57,7 @@ class Hero(SQLModel, table=True): @needs_py312 +@needs_pydanticv2 def test_sa_type_typing_6() -> None: test_code = dedent(""" type Type6_t = t.Annotated[str, "Just a comment"] @@ -129,6 +131,7 @@ class Hero(SQLModel, table=True): @needs_py312 +@needs_pydanticv2 def test_sa_type_typing_extensions_5() -> None: test_code = dedent(""" type Type5_te = str @@ -141,6 +144,7 @@ class Hero(SQLModel, table=True): @needs_py312 +@needs_pydanticv2 def test_sa_type_typing_extensions_6() -> None: test_code = dedent(""" type Type6_te = te.Annotated[str, "Just a comment"] @@ -152,6 +156,7 @@ class Hero(SQLModel, table=True): exec(test_code, globals()) +@needs_pydanticv2 def test_sa_type_typing_extensions_7() -> None: Type7_te = te.NewType("Type7_te", str) From 1e9cf12752bf0e2170b9befe9226c46602968d6c Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:03:58 +0100 Subject: [PATCH 17/19] =?UTF-8?q?=F0=9F=93=9D=20Refactor=20type=20alias=20?= =?UTF-8?q?handling=20to=20improve=20compatibility=20with=20Python=203.10?= =?UTF-8?q?=20and=20enhance=20type=20resolution=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 39 --------------------------- sqlmodel/main.py | 54 ++++++++++++++++++++++++++++++++++++- tests/test_field_sa_type.py | 7 +---- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 972b5debb3..5b1055edfa 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -1,6 +1,5 @@ import sys import types -import typing from contextlib import contextmanager from contextvars import ContextVar from dataclasses import dataclass @@ -20,7 +19,6 @@ Union, ) -import typing_extensions from pydantic import VERSION as P_VERSION from pydantic import BaseModel from pydantic.fields import FieldInfo @@ -200,47 +198,10 @@ def is_field_noneable(field: "FieldInfo") -> bool: return False return False - def _is_typing_type_instance(annotation: Any, type_name: str) -> bool: - check_type = [] - if hasattr(typing, type_name): - check_type.append(getattr(typing, type_name)) - if hasattr(typing_extensions, type_name): - check_type.append(getattr(typing_extensions, type_name)) - - return bool(check_type) and isinstance(annotation, tuple(check_type)) - - def _is_new_type_instance(annotation: Any) -> bool: - if sys.version_info >= (3, 10): - return _is_typing_type_instance(annotation, "NewType") - else: - return hasattr(annotation, "__supertype__") - - def _is_type_var_instance(annotation: Any) -> bool: - return _is_typing_type_instance(annotation, "TypeVar") - - def _is_type_alias_type_instance(annotation: Any) -> bool: - if sys.version_info[:2] == (3, 10): - if type(annotation) is types.GenericAlias: - # In Python 3.10, GenericAlias instances are of type TypeAliasType - return False - - return _is_typing_type_instance(annotation, "TypeAliasType") - def get_sa_type_from_type_annotation(annotation: Any) -> Any: # Resolve Optional fields if annotation is None: raise ValueError("Missing field type") - if _is_type_var_instance(annotation): - annotation = annotation.__bound__ - if not annotation: - raise ValueError( - "TypeVars without a bound type cannot be converted to SQLAlchemy types" - ) - # annotations.__constraints__ could be used and defined Union[*constraints], but ORM does not support it - elif _is_new_type_instance(annotation): - annotation = annotation.__supertype__ - elif _is_type_alias_type_instance(annotation): - annotation = annotation.__value__ origin = get_origin(annotation) if origin is None: return annotation diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 7c916f79af..271bf5ddeb 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -1,6 +1,9 @@ from __future__ import annotations import ipaddress +import sys +import types +import typing import uuid import weakref from datetime import date, datetime, time, timedelta @@ -27,6 +30,7 @@ overload, ) +import typing_extensions from pydantic import BaseModel, EmailStr from pydantic.fields import FieldInfo as PydanticFieldInfo from sqlalchemy import ( @@ -519,7 +523,7 @@ def __new__( if k in relationships: relationship_annotations[k] = v else: - pydantic_annotations[k] = v + pydantic_annotations[k] = resolve_type_alias(v) dict_used = { **dict_for_pydantic, "__weakref__": None, @@ -763,6 +767,54 @@ def get_column_from_field(field: Any) -> Column: # type: ignore return Column(sa_type, *args, **kwargs) # type: ignore +def _is_typing_type_instance(annotation: Any, type_name: str) -> bool: + check_type = [] + if hasattr(typing, type_name): + check_type.append(getattr(typing, type_name)) + if hasattr(typing_extensions, type_name): + check_type.append(getattr(typing_extensions, type_name)) + + return bool(check_type) and isinstance(annotation, tuple(check_type)) + + +def _is_new_type_instance(annotation: Any) -> bool: + if sys.version_info >= (3, 10): + return _is_typing_type_instance(annotation, "NewType") + else: + return hasattr(annotation, "__supertype__") + + +def _is_type_var_instance(annotation: Any) -> bool: + return _is_typing_type_instance(annotation, "TypeVar") + + +def _is_type_alias_type_instance(annotation: Any) -> bool: + if sys.version_info[:2] == (3, 10): + if type(annotation) is types.GenericAlias: + # In Python 3.10, GenericAlias instances are of type TypeAliasType + return False + + return _is_typing_type_instance(annotation, "TypeAliasType") + + +def resolve_type_alias(annotation: Any) -> Any: + if _is_type_var_instance(annotation): + resolution = annotation.__bound__ + if not annotation: + raise ValueError( + "TypeVars without a bound type cannot be converted to SQLAlchemy types" + ) + # annotations.__constraints__ could be used and defined Union[*constraints], but ORM does not support it + elif _is_new_type_instance(annotation): + resolution = annotation.__supertype__ + elif _is_type_alias_type_instance(annotation): + resolution = annotation.__value__ + else: + resolution = annotation + + return resolution + + class_registry = weakref.WeakValueDictionary() # type: ignore default_registry = registry() diff --git a/tests/test_field_sa_type.py b/tests/test_field_sa_type.py index e7318df4f0..8f3bc46b85 100644 --- a/tests/test_field_sa_type.py +++ b/tests/test_field_sa_type.py @@ -5,7 +5,7 @@ import typing_extensions as te from sqlmodel import Field, SQLModel -from tests.conftest import needs_py312, needs_pydanticv2 +from tests.conftest import needs_py312 def test_sa_type_typing_1() -> None: @@ -44,7 +44,6 @@ class Hero(SQLModel, table=True): @needs_py312 -@needs_pydanticv2 def test_sa_type_typing_5() -> None: test_code = dedent(""" type Type5_t = str @@ -57,7 +56,6 @@ class Hero(SQLModel, table=True): @needs_py312 -@needs_pydanticv2 def test_sa_type_typing_6() -> None: test_code = dedent(""" type Type6_t = t.Annotated[str, "Just a comment"] @@ -131,7 +129,6 @@ class Hero(SQLModel, table=True): @needs_py312 -@needs_pydanticv2 def test_sa_type_typing_extensions_5() -> None: test_code = dedent(""" type Type5_te = str @@ -144,7 +141,6 @@ class Hero(SQLModel, table=True): @needs_py312 -@needs_pydanticv2 def test_sa_type_typing_extensions_6() -> None: test_code = dedent(""" type Type6_te = te.Annotated[str, "Just a comment"] @@ -156,7 +152,6 @@ class Hero(SQLModel, table=True): exec(test_code, globals()) -@needs_pydanticv2 def test_sa_type_typing_extensions_7() -> None: Type7_te = te.NewType("Type7_te", str) From 2cc8cd08c07ef5777382470e196afd36e71c97b8 Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:03:40 +0100 Subject: [PATCH 18/19] Update _compat.py --- sqlmodel/_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 5b1055edfa..732366c4a0 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -72,6 +72,7 @@ def partial_init() -> Generator[None, None, None]: finish_init.reset(token) + if IS_PYDANTIC_V2: from annotated_types import MaxLen from pydantic import ConfigDict as BaseConfig From 702ac3883aef8d88d48c1ce23bcb9aa3865437ab Mon Sep 17 00:00:00 2001 From: "Julio C. Galindo" <54072664+stickM4N@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:04:16 +0100 Subject: [PATCH 19/19] Update _compat.py --- sqlmodel/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 732366c4a0..230f8cc362 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -29,6 +29,7 @@ PYDANTIC_MINOR_VERSION = tuple(int(i) for i in P_VERSION.split(".")[:2]) IS_PYDANTIC_V2 = PYDANTIC_MINOR_VERSION[0] == 2 + if TYPE_CHECKING: from .main import RelationshipInfo, SQLModel @@ -72,7 +73,6 @@ def partial_init() -> Generator[None, None, None]: finish_init.reset(token) - if IS_PYDANTIC_V2: from annotated_types import MaxLen from pydantic import ConfigDict as BaseConfig