diff --git a/src/adaptix/_internal/type_tools/fundamentals.py b/src/adaptix/_internal/type_tools/fundamentals.py index b9889595..a699bf5c 100644 --- a/src/adaptix/_internal/type_tools/fundamentals.py +++ b/src/adaptix/_internal/type_tools/fundamentals.py @@ -1,3 +1,4 @@ +import sys import types from typing import TypeVar, get_args, get_origin, get_type_hints @@ -41,5 +42,22 @@ def get_generic_args(tp: TypeHint) -> VarTuple[TypeHint]: return get_args(tp) -def get_all_type_hints(obj, globalns=None, localns=None): - return get_type_hints(obj, globalns, localns, include_extras=True) +if sys.version_info >= (3, 14): + import annotationlib + + def get_all_type_hints(obj, globalns=None, localns=None): + return get_type_hints( + obj, + globalns, + localns, + include_extras=True, + format=annotationlib.Format.FORWARDREF, + ) +else: + def get_all_type_hints(obj, globalns=None, localns=None): + return get_type_hints( + obj, + globalns, + localns, + include_extras=True, + ) diff --git a/tests/unit/integrations/sqlalchemy/forward_ref/__init__.py b/tests/unit/integrations/sqlalchemy/forward_ref/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/integrations/sqlalchemy/forward_ref/base.py b/tests/unit/integrations/sqlalchemy/forward_ref/base.py new file mode 100644 index 00000000..fa2b68a5 --- /dev/null +++ b/tests/unit/integrations/sqlalchemy/forward_ref/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass diff --git a/tests/unit/integrations/sqlalchemy/forward_ref/product.py b/tests/unit/integrations/sqlalchemy/forward_ref/product.py new file mode 100644 index 00000000..f49b0d0d --- /dev/null +++ b/tests/unit/integrations/sqlalchemy/forward_ref/product.py @@ -0,0 +1,13 @@ +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from .base import Base +from .tag import Tag + + +class Product(Base): + __tablename__ = "products" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] + + tags: Mapped[list[Tag]] = relationship(back_populates="product") diff --git a/tests/unit/integrations/sqlalchemy/forward_ref/tag.py b/tests/unit/integrations/sqlalchemy/forward_ref/tag.py new file mode 100644 index 00000000..a7b68a56 --- /dev/null +++ b/tests/unit/integrations/sqlalchemy/forward_ref/tag.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from .base import Base + +if TYPE_CHECKING: + from .product import Product + + +class Tag(Base): + __tablename__ = "tags" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] + product_id: Mapped[int] = mapped_column(ForeignKey("products.id")) + + product: Mapped["Product"] = relationship(back_populates="tags") diff --git a/tests/unit/integrations/sqlalchemy/test_cyclic_forward_ref.py b/tests/unit/integrations/sqlalchemy/test_cyclic_forward_ref.py new file mode 100644 index 00000000..1e3b5e2e --- /dev/null +++ b/tests/unit/integrations/sqlalchemy/test_cyclic_forward_ref.py @@ -0,0 +1,37 @@ +from dataclasses import asdict, dataclass + +from adaptix.conversion import get_converter + +from .forward_ref.product import Product +from .forward_ref.tag import Tag + + +@dataclass +class TagView: + id: int + name: str + + +@dataclass +class ProductView: + id: int + name: str + tags: list[TagView] + + +def test_cyclic_forward_ref(): + product = Product( + id=1, + name="prod_name", + tags=[Tag(id=1, name="tag1"), Tag(id=2, name="tag2")], + ) + converter = get_converter( + Product, + ProductView, + ) + result = converter(product) + assert asdict(result) == { + "id": 1, + "name": "prod_name", + "tags": [{"id": 1, "name": "tag1"}, {"id": 2, "name": "tag2"}], + }