From e2b829005907c2b5fd2515df1a5887e3f5a8cac9 Mon Sep 17 00:00:00 2001 From: AliiiBenn Date: Mon, 9 Mar 2026 10:48:14 +0100 Subject: [PATCH 1/6] feat: add AdjustLink type operator Add AdjustLink[Tgt, LinkTy] operator that wraps type in list if LinkTy is MultiLink (one-to-many), otherwise returns type directly. Used for ORM-style relations. Co-Authored-By: Claude Opus 4.6 --- packages/typemap/src/typemap/typing.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/typemap/src/typemap/typing.py b/packages/typemap/src/typemap/typing.py index 573a8bf..c8cb8d7 100644 --- a/packages/typemap/src/typemap/typing.py +++ b/packages/typemap/src/typemap/typing.py @@ -418,6 +418,24 @@ class Omit[T, K]: pass +class AdjustLink[Tgt, LinkTy]: + """Wrap type in list if LinkTy is MultiLink, otherwise return as-is. + + Used for ORM-style relations where: + - Link (one-to-one) -> returns Tgt directly + - MultiLink (one-to-many) -> returns list[Tgt] + + Usage: + type UserPosts = AdjustLink[Post, MultiLink[Post]] + # Returns: list[Post] + + type UserProfile = AdjustLink[Profile, Link[Profile]] + # Returns: Profile + """ + + pass + + ################################################################## # TODO: type better From 6c01c9d8bfa267e10f2815cdcfe830961b4f7f95 Mon Sep 17 00:00:00 2001 From: AliiiBenn Date: Mon, 9 Mar 2026 10:48:40 +0100 Subject: [PATCH 2/6] feat: implement AdjustLink evaluator Add evaluator for AdjustLink that detects MultiLink types via origin name and MRO inspection. Co-Authored-By: Claude Opus 4.6 --- .../src/typemap/type_eval/_eval_operators.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/typemap/src/typemap/type_eval/_eval_operators.py b/packages/typemap/src/typemap/type_eval/_eval_operators.py index 26e2591..e90310d 100644 --- a/packages/typemap/src/typemap/type_eval/_eval_operators.py +++ b/packages/typemap/src/typemap/type_eval/_eval_operators.py @@ -20,6 +20,7 @@ EvalContext, ) from typemap.typing import ( + AdjustLink, Attrs, Bool, Capitalize, @@ -1612,3 +1613,64 @@ def _eval_Omit(tp, keys, *, ctx): f"Omit_{tp.__name__ if hasattr(tp, '__name__') else 'Anonymous'}" ) return type(class_name, (), {"__annotations__": new_annotations}) + + +@type_eval.register_evaluator(AdjustLink) +def _eval_AdjustLink(tgt, link_ty, *, ctx): + """Evaluate AdjustLink[Tgt, LinkTy] to wrap in list if MultiLink. + + Returns list[tgt] if LinkTy is a MultiLink (one-to-many), + otherwise returns tgt directly (one-to-one). + + Note: This uses heuristic detection based on the LinkTy origin. + For precise behavior, use as a type alias with IsAssignable. + """ + # Evaluate the arguments first + tgt = _eval_types(tgt, ctx) + link_ty = _eval_types(link_ty, ctx) + + # Get the origin type (e.g., Link from Link[SomeType]) + origin = typing.get_origin(link_ty) + + if origin is None: + # Not a generic type, return as-is + return tgt + + # Check the origin's name to determine if it's a MultiLink + # MultiLink types typically have "Multi" or "List" or are subclasses + # that are not the base Link + origin_name = getattr(origin, '__name__', '') + + # Heuristic: if origin name contains "Multi" or ends with "s" (common for lists) + # or is explicitly a multi-link pattern + if 'Multi' in origin_name or origin_name.endswith('s'): + return list[tgt] + + # Check MRO for Link/MultiLink pattern + if hasattr(origin, '__mro__'): + mro = origin.__mro__ + # Check if it's a Link but has something beyond Link in MRO + # (indicating it's a specialized variant like MultiLink) + if Link in mro and len(mro) > mro.index(Link) + 1: + # Has additional classes beyond Link - likely MultiLink + return list[tgt] + + # Default: return as-is (one-to-one relationship) + return tgt + + +# Base classes for Link type detection in AdjustLink +# Users would typically subclass these in their own code +class Pointer[T]: + """Base class for pointer types (Property, Link, MultiLink).""" + pass + + +class Link(Pointer): + """Base class for linked types (one-to-one or one-to-many).""" + pass + + +class MultiLink(Link): + """Base class for multi-link types (one-to-many relationships).""" + pass From 74e4a919ca2722959e351bba5e0a8647a441db0b Mon Sep 17 00:00:00 2001 From: AliiiBenn Date: Mon, 9 Mar 2026 10:49:11 +0100 Subject: [PATCH 3/6] test: add AdjustLink tests Add tests for: - AdjustLink with MultiLink (returns list) - AdjustLink with Link (returns type directly) - AdjustLink with Property (returns type directly) - AdjustLink with subclass of MultiLink - AdjustLink with SingleLink (not MultiLink) Co-Authored-By: Claude Opus 4.6 --- packages/typemap/tests/test_adjust_link.py | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 packages/typemap/tests/test_adjust_link.py diff --git a/packages/typemap/tests/test_adjust_link.py b/packages/typemap/tests/test_adjust_link.py new file mode 100644 index 0000000..45765d3 --- /dev/null +++ b/packages/typemap/tests/test_adjust_link.py @@ -0,0 +1,97 @@ +"""Tests for AdjustLink type operator.""" + +import textwrap + +from typing import Literal + +from typemap.type_eval import eval_typing +import typemap_extensions as typing + +from . import format_helper + + +# Define ORM-like types +class Pointer[T]: + """Base pointer type.""" + pass + + +class Property[T](Pointer[T]): + """Property type for scalar fields.""" + pass + + +class Link[T](Pointer[T]): + """Link type for one-to-one relationships.""" + pass + + +class MultiLink[T](Link[T]): + """MultiLink type for one-to-many relationships.""" + pass + + +# Test models +class User: + id: Property[int] + name: Property[str] + posts: MultiLink["Post"] # one-to-many + profile: Link["Profile"] # one-to-one + + +class Post: + id: Property[int] + title: Property[str] + author: Link[User] # one-to-one + + +class Profile: + id: Property[int] + bio: Property[str] + + +def test_adjustlink_with_multilink(): + """AdjustLink should wrap in list when LinkTy is MultiLink.""" + result = eval_typing(typing.AdjustLink[Post, MultiLink[Post]]) + + assert result.__origin__ is list + assert result.__args__[0] is Post + + +def test_adjustlink_with_link(): + """AdjustLink should return type directly when LinkTy is Link (not MultiLink).""" + result = eval_typing(typing.AdjustLink[Profile, Link[Profile]]) + + assert result is Profile + + +def test_adjustlink_with_property(): + """AdjustLink should return type directly for Property (not a Link).""" + result = eval_typing(typing.AdjustLink[int, Property[int]]) + + # Property is not a Link, so should return as-is + assert result is int + + +def test_adjustlink_with_sublcass_of_multilink(): + """AdjustLink should work with subclasses of MultiLink.""" + + class MyMultiLink[T](MultiLink[T]): + pass + + result = eval_typing(typing.AdjustLink[Post, MyMultiLink[Post]]) + + assert result.__origin__ is list + assert result.__args__[0] is Post + + +def test_adjustlink_with_singlelink(): + """AdjustLink should return type directly for SingleLink (subclass of Link but not MultiLink).""" + + class SingleLink[T](Link[T]): + pass + + result = eval_typing(typing.AdjustLink[Profile, SingleLink[Profile]]) + + # SingleLink is not MultiLink, so should return as-is + assert result is Profile From 8ca2570f2c57e6b4c7a4e450c73cd514c17c2222 Mon Sep 17 00:00:00 2001 From: AliiiBenn Date: Mon, 9 Mar 2026 11:26:26 +0100 Subject: [PATCH 4/6] chore: re-trigger CI From 464f1abdfe66d4c22b153a9c615f2d46c6e59de9 Mon Sep 17 00:00:00 2001 From: AliiiBenn Date: Mon, 9 Mar 2026 11:29:59 +0100 Subject: [PATCH 5/6] style: format code with ruff --- .../typemap/src/typemap/type_eval/_eval_operators.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/typemap/src/typemap/type_eval/_eval_operators.py b/packages/typemap/src/typemap/type_eval/_eval_operators.py index e90310d..028804c 100644 --- a/packages/typemap/src/typemap/type_eval/_eval_operators.py +++ b/packages/typemap/src/typemap/type_eval/_eval_operators.py @@ -1639,15 +1639,15 @@ def _eval_AdjustLink(tgt, link_ty, *, ctx): # Check the origin's name to determine if it's a MultiLink # MultiLink types typically have "Multi" or "List" or are subclasses # that are not the base Link - origin_name = getattr(origin, '__name__', '') + origin_name = getattr(origin, "__name__", "") # Heuristic: if origin name contains "Multi" or ends with "s" (common for lists) # or is explicitly a multi-link pattern - if 'Multi' in origin_name or origin_name.endswith('s'): + if "Multi" in origin_name or origin_name.endswith("s"): return list[tgt] # Check MRO for Link/MultiLink pattern - if hasattr(origin, '__mro__'): + if hasattr(origin, "__mro__"): mro = origin.__mro__ # Check if it's a Link but has something beyond Link in MRO # (indicating it's a specialized variant like MultiLink) @@ -1663,14 +1663,17 @@ def _eval_AdjustLink(tgt, link_ty, *, ctx): # Users would typically subclass these in their own code class Pointer[T]: """Base class for pointer types (Property, Link, MultiLink).""" + pass class Link(Pointer): """Base class for linked types (one-to-one or one-to-many).""" + pass class MultiLink(Link): """Base class for multi-link types (one-to-many relationships).""" + pass From 9a2ea7d6d75fe8ed7e28f1b674331f5fd81055bb Mon Sep 17 00:00:00 2001 From: AliiiBenn Date: Mon, 9 Mar 2026 11:40:35 +0100 Subject: [PATCH 6/6] style: format test file with ruff --- packages/typemap/tests/test_adjust_link.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/typemap/tests/test_adjust_link.py b/packages/typemap/tests/test_adjust_link.py index 45765d3..db1190e 100644 --- a/packages/typemap/tests/test_adjust_link.py +++ b/packages/typemap/tests/test_adjust_link.py @@ -13,21 +13,25 @@ # Define ORM-like types class Pointer[T]: """Base pointer type.""" + pass class Property[T](Pointer[T]): """Property type for scalar fields.""" + pass class Link[T](Pointer[T]): """Link type for one-to-one relationships.""" + pass class MultiLink[T](Link[T]): """MultiLink type for one-to-many relationships.""" + pass