From 1f9b97f4d1b1f605852ee090340c939b23b623e0 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 29 Jan 2026 15:10:41 -0800 Subject: [PATCH 1/8] Add Cls. --- typemap/typing.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/typemap/typing.py b/typemap/typing.py index d8cbc10..833c8ed 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -183,6 +183,10 @@ class NewProtocol[*T]: pass +class Cls: + pass + + class RaiseError[S: str, *Ts]: """Raise a type error with the given message when evaluated. From a3e40ddf23060823efc3528661992d1cb413273a Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 29 Jan 2026 17:11:41 -0800 Subject: [PATCH 2/8] Add current_cls to context. --- typemap/type_eval/_eval_typing.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 29efd8b..98066a5 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -107,6 +107,10 @@ class EvalContext: # confused and wants to treat it as a MethodType. current_generic_alias: types.GenericAlias | typing.Any | None = None + # The current class being evaluated, used by Cls to resolve to the + # class whose attributes are being accessed + current_cls: type | None = None + # `eval_types()` calls can be nested, context must be preserved _current_context: contextvars.ContextVar[EvalContext | None] = ( @@ -170,6 +174,7 @@ def _child_context() -> typing.Iterator[EvalContext]: recursive_type_alias=ctx.recursive_type_alias, known_recursive_types=ctx.known_recursive_types.copy(), current_generic_alias=ctx.current_generic_alias, + current_cls=ctx.current_cls, ) _current_context.set(child_ctx) yield child_ctx @@ -315,6 +320,15 @@ def _get_class_type_hint_namespaces( @_eval_types_impl.register def _eval_type_type(obj: type, ctx: EvalContext): + from typemap.typing import Cls + + # Special handling for Cls + if obj is Cls: + # If we are evaluating Cls, return the current class + if ctx.current_cls is not None: + return ctx.current_cls + return Cls + # Ensure that any string annotations are resolved if ( hasattr(obj, '__annotations__') From 979726fdc8d36a99dbf1ddfae3253a98570a666b Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 29 Jan 2026 17:27:49 -0800 Subject: [PATCH 3/8] Evaluate Attrs, Members, GetAttr in child context. --- typemap/type_eval/_eval_operators.py | 57 +++++++++++++++++----------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 3bacffa..fcae962 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -12,6 +12,7 @@ from typemap import type_eval from typemap.type_eval import _apply_generic, _typing_inspect from typemap.type_eval._eval_typing import ( + _child_context, _eval_types, _get_class_type_hint_namespaces, ) @@ -680,18 +681,22 @@ def _hints_to_members(hints, ctx): @type_eval.register_evaluator(Attrs) @_lift_over_unions def _eval_Attrs(tp, *, ctx): - hints = get_annotated_type_hints(tp, include_extras=True) - return _hints_to_members(hints, ctx) + with _child_context() as child_ctx: + child_ctx.current_cls = tp + hints = get_annotated_type_hints(tp, include_extras=True) + return _hints_to_members(hints, child_ctx) @type_eval.register_evaluator(Members) @_lift_over_unions def _eval_Members(tp, *, ctx): - hints = { - **get_annotated_type_hints(tp, include_extras=True), - **get_annotated_method_hints(tp), - } - return _hints_to_members(hints, ctx) + with _child_context() as child_ctx: + child_ctx.current_cls = tp + hints = { + **get_annotated_type_hints(tp, include_extras=True), + **get_annotated_method_hints(tp), + } + return _hints_to_members(hints, child_ctx) @type_eval.register_evaluator(GetMember) @@ -699,14 +704,17 @@ def _eval_Members(tp, *, ctx): def _eval_GetMember(tp, prop, *, ctx): # XXX: extras? name = _from_literal(prop) - hints = { - **get_annotated_type_hints(tp, include_extras=True), - **get_annotated_method_hints(tp), - } - if name in hints: - return _hint_to_member(name, *hints[name], ctx=ctx) - else: - return typing.Never + + with _child_context() as child_ctx: + child_ctx.current_cls = tp + hints = { + **get_annotated_type_hints(tp, include_extras=True), + **get_annotated_method_hints(tp), + } + if name in hints: + return _hint_to_member(name, *hints[name], ctx=ctx) + else: + return typing.Never ################################################################## @@ -729,14 +737,17 @@ def _eval_FromUnion(tp, *, ctx): def _eval_GetMemberType(tp, prop, *, ctx): # XXX: extras? name = _from_literal(prop) - hints = { - **get_annotated_type_hints(tp, include_extras=True), - **get_annotated_method_hints(tp), - } - if name in hints: - return hints[name][0] - else: - return typing.Never + + with _child_context() as child_ctx: + child_ctx.current_cls = tp + hints = { + **get_annotated_type_hints(tp, include_extras=True), + **get_annotated_method_hints(tp), + } + if name in hints: + return _eval_types(hints[name][0], child_ctx) + else: + return typing.Never def _fix_callable_args(base, args): From afa402431994b0ca43bbaebd22baaaede52d720e Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 29 Jan 2026 15:10:36 -0800 Subject: [PATCH 4/8] Add tests. --- tests/test_type_eval.py | 257 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 5dfbf8c..fb25aa0 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -21,6 +21,7 @@ from typemap.typing import ( Attrs, Bool, + Cls, FromUnion, GenericCallable, GetArg, @@ -1612,6 +1613,262 @@ def static_method(x: int) -> int: ... """) +def test_type_eval_cls_01(): + res = eval_typing(Cls) + assert res == Cls + + +def test_type_eval_cls_02(): + class A: + child: Cls | None + + class B(A): + pass + + assert eval_typing(GetMemberType[A, Literal["child"]]) == A | None + assert eval_typing(GetMemberType[B, Literal["child"]]) == B | None + + assert ( + eval_typing(Attrs[A]) + == tuple[Member[Literal["child"], A | None, Never, Never, A]] + ) + assert ( + eval_typing(Attrs[B]) + == tuple[Member[Literal["child"], B | None, Never, Never, A]] + ) + + assert ( + eval_typing(Members[A]) + == tuple[Member[Literal["child"], A | None, Never, Never, A]] + ) + assert ( + eval_typing(Members[B]) + == tuple[Member[Literal["child"], B | None, Never, Never, A]] + ) + + fmt = format_helper.format_class(A) + assert fmt == textwrap.dedent("""\ + class A: + child: tests.test_type_eval.test_type_eval_cls_02..A | None + """) + fmt = format_helper.format_class(B) + assert fmt == textwrap.dedent("""\ + class B: + child: tests.test_type_eval.test_type_eval_cls_02..B | None + """) + + +type ValueOrNone = GetMemberType[Cls, Literal["value"]] | None + + +def test_type_eval_cls_03(): + class A: + value: int + maybe: ValueOrNone + + assert eval_typing(GetMemberType[A, Literal["maybe"]]) == int | None + + assert ( + eval_typing(Attrs[A]) + == tuple[ + Member[Literal["value"], int, Never, Never, A], + Member[Literal["maybe"], int | None, Never, Never, A], + ] + ) + assert ( + eval_typing(Members[A]) + == tuple[ + Member[Literal["value"], int, Never, Never, A], + Member[Literal["maybe"], int | None, Never, Never, A], + ] + ) + + fmt = format_helper.format_class(A) + assert fmt == textwrap.dedent("""\ + class A: + value: int + maybe: int | None + """) + + +def test_type_eval_cls_04(): + class A[T]: + value: T + maybe: ValueOrNone + + assert eval_typing(GetMemberType[A[int], Literal["maybe"]]) == int | None + assert ( + eval_typing(GetMemberType[A[list[int]], Literal["maybe"]]) + == list[int] | None + ) + + assert ( + eval_typing(Attrs[A[int]]) + == tuple[ + Member[Literal["value"], int, Never, Never, A[int]], + Member[Literal["maybe"], int | None, Never, Never, A[int]], + ] + ) + assert ( + eval_typing(Attrs[A[list[int]]]) + == tuple[ + Member[ + Literal["value"], + list[int], + Never, + Never, + A[list[int]], + ], + Member[ + Literal["maybe"], + list[int] | None, + Never, + Never, + A[list[int]], + ], + ] + ) + + assert ( + eval_typing(Members[A[int]]) + == tuple[ + Member[Literal["value"], int, Never, Never, A[int]], + Member[Literal["maybe"], int | None, Never, Never, A[int]], + ] + ) + assert ( + eval_typing(Members[A[list[int]]]) + == tuple[ + Member[ + Literal["value"], + list[int], + Never, + Never, + A[list[int]], + ], + Member[ + Literal["maybe"], + list[int] | None, + Never, + Never, + A[list[int]], + ], + ] + ) + + +def test_type_eval_cls_05(): + class A: + value: str + maybe: ValueOrNone + + class B: + value: int + maybe: GetMemberType[A, Literal["maybe"]] + + assert eval_typing(GetMemberType[B, Literal["maybe"]]) == str | None + + assert ( + eval_typing(Attrs[B]) + == tuple[ + Member[Literal["value"], int, Never, Never, B], + Member[Literal["maybe"], str | None, Never, Never, B], + ] + ) + + assert ( + eval_typing(Members[B]) + == tuple[ + Member[Literal["value"], int, Never, Never, B], + Member[Literal["maybe"], str | None, Never, Never, B], + ] + ) + + +def test_type_eval_cls_06(): + class A[T]: + value: T + maybe: ValueOrNone + + class B[T, U]: + value: T + maybe: GetMemberType[A[U], Literal["maybe"]] + + assert ( + eval_typing(GetMemberType[B[int, list[str]], Literal["maybe"]]) + == list[str] | None + ) + + assert ( + eval_typing(Attrs[B[int, list[str]]]) + == tuple[ + Member[ + Literal["value"], + int, + Never, + Never, + B[int, list[str]], + ], + Member[ + Literal["maybe"], + list[str] | None, + Never, + Never, + B[int, list[str]], + ], + ] + ) + + assert ( + eval_typing(Members[B[int, list[str]]]) + == tuple[ + Member[ + Literal["value"], + int, + Never, + Never, + B[int, list[str]], + ], + Member[ + Literal["maybe"], + list[str] | None, + Never, + Never, + B[int, list[str]], + ], + ] + ) + + +type AttrSetOfOnlyInt[T, N] = ( + set[GetMemberType[T, N]] + if IsSub[GetMemberType[T, N], int] + else GetMemberType[T, N] +) + + +def test_type_eval_cls_07(): + class A: + value: int + maybe: ValueOrNone + + class B: + value: str + maybe: ValueOrNone + + assert eval_typing(AttrSetOfOnlyInt[A, Literal["value"]]) == set[int] + assert eval_typing(AttrSetOfOnlyInt[B, Literal["value"]]) is str + + +def test_type_eval_cls_08(): + class A[T]: + value: T + maybe: ValueOrNone + + assert eval_typing(AttrSetOfOnlyInt[A[int], Literal["value"]]) == set[int] + assert eval_typing(AttrSetOfOnlyInt[A[str], Literal["value"]]) is str + + ############## type XTest[X] = Annotated[X, 'blah'] From 96c95317d6e3812f171c065f4c72de11f7489204 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Fri, 30 Jan 2026 17:14:39 -0800 Subject: [PATCH 5/8] Add PropName. --- typemap/typing.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/typemap/typing.py b/typemap/typing.py index 833c8ed..652b4a6 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -187,6 +187,10 @@ class Cls: pass +class PropName: + pass + + class RaiseError[S: str, *Ts]: """Raise a type error with the given message when evaluated. From 00769e4903e1832c8174e15e5ba79494c26dfbf3 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Sat, 31 Jan 2026 12:53:26 -0800 Subject: [PATCH 6/8] Add tests. --- tests/test_type_eval.py | 104 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index fb25aa0..8340a55 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -40,6 +40,7 @@ Members, NewProtocol, Param, + PropName, Slice, SpecialFormEllipsis, StrConcat, @@ -1869,6 +1870,109 @@ class A[T]: assert eval_typing(AttrSetOfOnlyInt[A[str], Literal["value"]]) is str +def test_type_eval_propname_01(): + t = eval_typing(PropName) + assert t == PropName + + +def test_type_eval_propname_02(): + class A: + x: int | PropName + y: str | PropName + z: list[int] | PropName + + t = eval_typing(GetMemberType[A, Literal["x"]]) + assert t == int | Literal["x"] + t = eval_typing(GetMemberType[A, Literal["y"]]) + assert t == str | Literal["y"] + t = eval_typing(GetMemberType[A, Literal["z"]]) + assert t == list[int] | Literal["z"] + + +type TheNameAsInt = int if Matches[PropName, Literal["the"]] else str + + +def test_type_eval_propname_03(): + class A: + the: TheNameAsInt + foo: TheNameAsInt + + class B(A): + bar: TheNameAsInt + + fmt = format_helper.format_class(A) + assert fmt == textwrap.dedent("""\ + class A: + the: int + foo: str + """) + + fmt = format_helper.format_class(B) + assert fmt == textwrap.dedent("""\ + class B: + the: int + foo: str + bar: str + """) + + +def test_type_eval_propname_04(): + class A: + the: TheNameAsInt + foo: TheNameAsInt + + class B: + the: GetMemberType[A, Literal["the"]] + foo: GetMemberType[A, Literal["foo"]] + + fmt = format_helper.format_class(B) + assert fmt == textwrap.dedent("""\ + class B: + the: int + foo: str + """) + + +def test_type_eval_propname_05(): + class A: + x: list[PropName] + y: dict[str, PropName] + + t1 = eval_typing(GetMemberType[A, Literal["x"]]) + assert t1 == list[Literal["x"]] + t2 = eval_typing(GetMemberType[A, Literal["y"]]) + assert t2 == dict[str, Literal["y"]] + + fmt = format_helper.format_class(A) + assert fmt == textwrap.dedent("""\ + class A: + x: list[typing.Literal['x']] + y: dict[str, typing.Literal['y']] + """) + + +def test_type_eval_propname_06(): + class A: + foo: list[PropName] + bar: GetMemberType[A, Literal["foo"]] + baz: GetType[GetMember[A, Literal["foo"]]] + + t1 = eval_typing(GetMemberType[A, Literal["foo"]]) + assert t1 == list[Literal["foo"]] + t2 = eval_typing(GetMemberType[A, Literal["bar"]]) + assert t2 == list[Literal["foo"]] + t3 = eval_typing(GetMemberType[A, Literal["baz"]]) + assert t3 == list[Literal["foo"]] + + fmt = format_helper.format_class(A) + assert fmt == textwrap.dedent("""\ + class A: + foo: list[typing.Literal['foo']] + bar: list[typing.Literal['foo']] + baz: list[typing.Literal['foo']] + """) + + ############## type XTest[X] = Annotated[X, 'blah'] From fd7d81aae695677e43863e4cf234294159763192 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Sat, 31 Jan 2026 12:54:41 -0800 Subject: [PATCH 7/8] First implementation of PropName. --- typemap/type_eval/_eval_operators.py | 13 ++++++++++++- typemap/type_eval/_eval_typing.py | 27 +++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index fcae962..74a72bc 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -662,9 +662,15 @@ def _get_function_hint_namespaces(func, receiver_type=None): def _hint_to_member(n, t, qs, init, d, *, ctx): + # Set current_propname so PropName can resolve to the property name + with _child_context() as child_ctx: + child_ctx.current_cls = ctx.current_cls + child_ctx.current_propname = n + evaluated_type = _eval_types(t, child_ctx) + return Member[ typing.Literal[n], - _eval_types(t, ctx), + evaluated_type, _mk_literal_union(*qs), init, d, @@ -683,6 +689,7 @@ def _hints_to_members(hints, ctx): def _eval_Attrs(tp, *, ctx): with _child_context() as child_ctx: child_ctx.current_cls = tp + child_ctx.current_propname = None hints = get_annotated_type_hints(tp, include_extras=True) return _hints_to_members(hints, child_ctx) @@ -692,6 +699,7 @@ def _eval_Attrs(tp, *, ctx): def _eval_Members(tp, *, ctx): with _child_context() as child_ctx: child_ctx.current_cls = tp + child_ctx.current_propname = None hints = { **get_annotated_type_hints(tp, include_extras=True), **get_annotated_method_hints(tp), @@ -707,6 +715,7 @@ def _eval_GetMember(tp, prop, *, ctx): with _child_context() as child_ctx: child_ctx.current_cls = tp + child_ctx.current_propname = None hints = { **get_annotated_type_hints(tp, include_extras=True), **get_annotated_method_hints(tp), @@ -740,11 +749,13 @@ def _eval_GetMemberType(tp, prop, *, ctx): with _child_context() as child_ctx: child_ctx.current_cls = tp + child_ctx.current_propname = None hints = { **get_annotated_type_hints(tp, include_extras=True), **get_annotated_method_hints(tp), } if name in hints: + child_ctx.current_propname = name return _eval_types(hints[name][0], child_ctx) else: return typing.Never diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 98066a5..4463650 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -111,6 +111,10 @@ class EvalContext: # class whose attributes are being accessed current_cls: type | None = None + # The current property name being evaluated, used by PropName to resolve + # to the name of the current property + current_propname: str | None = None + # `eval_types()` calls can be nested, context must be preserved _current_context: contextvars.ContextVar[EvalContext | None] = ( @@ -129,7 +133,8 @@ def _ensure_context() -> typing.Iterator[EvalContext]: _current_context.set(ctx) ctx_set = True evaluator_token = nt.special_form_evaluator.set( - lambda t: _eval_types(t, ctx) + # Ensure that special evaluators always use the latest context + lambda t: _eval_types(t, _current_context.get() or ctx) ) try: @@ -175,6 +180,7 @@ def _child_context() -> typing.Iterator[EvalContext]: known_recursive_types=ctx.known_recursive_types.copy(), current_generic_alias=ctx.current_generic_alias, current_cls=ctx.current_cls, + current_propname=ctx.current_propname, ) _current_context.set(child_ctx) yield child_ctx @@ -215,9 +221,12 @@ def _eval_types(obj: typing.Any, ctx: EvalContext): return obj # Already resolved or seen, return the result - if obj in ctx.resolved: + if ctx.current_propname is not None and _is_type_alias_type(obj): + # For now, just re-evaluate type aliases in property context + pass + elif obj in ctx.resolved: return ctx.resolved[obj] - if obj in ctx.seen: + elif obj in ctx.seen: return ctx.seen[obj] if _is_type_alias_type(obj): @@ -320,15 +329,21 @@ def _get_class_type_hint_namespaces( @_eval_types_impl.register def _eval_type_type(obj: type, ctx: EvalContext): - from typemap.typing import Cls + from typemap.typing import Cls, PropName - # Special handling for Cls + # Special handling for Cls and PropName if obj is Cls: - # If we are evaluating Cls, return the current class + # Cls returns the current class if ctx.current_cls is not None: return ctx.current_cls return Cls + elif obj is PropName: + # PropName returns the name of the current property + if ctx.current_propname is not None: + return typing.Literal[ctx.current_propname] + return PropName + # Ensure that any string annotations are resolved if ( hasattr(obj, '__annotations__') From 01277230d0caf55dd22719cce40a90b58314dff0 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Mon, 2 Feb 2026 10:09:04 -0800 Subject: [PATCH 8/8] Update test_qblike_3. --- tests/test_qblike_3.py | 137 ++++++++++++++++++++++++++++++++--------- 1 file changed, 109 insertions(+), 28 deletions(-) diff --git a/tests/test_qblike_3.py b/tests/test_qblike_3.py index 77b6221..d24ed88 100644 --- a/tests/test_qblike_3.py +++ b/tests/test_qblike_3.py @@ -16,6 +16,7 @@ from typemap.typing import ( Attrs, Bool, + Cls, Length, GetArg, GetMemberType, @@ -29,6 +30,7 @@ Matches, Member, NewProtocol, + PropName, Slice, ) @@ -137,13 +139,15 @@ class Table[name: str]: pass -class Field[Table, Name, PyType]: +class FieldType[Table, Name, PyType]: def __lt__(self, other: Any) -> Filter[Table]: ... -type FieldTable[T] = GetArg[T, Field, Literal[0]] -type FieldName[T] = GetArg[T, Field, Literal[1]] -type FieldPyType[T] = GetArg[T, Field, Literal[2]] +type Field[T] = FieldType[Cls, PropName, T] + +type FieldTable[T: FieldType] = GetArg[T, FieldType, Literal[0]] +type FieldName[T: FieldType] = GetArg[T, FieldType, Literal[1]] +type FieldPyType[T: FieldType] = GetArg[T, FieldType, Literal[2]] class ColumnArgs(TypedDict, total=False): @@ -230,7 +234,9 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): type MakeQueryEntryAllFields[T: Table] = QueryEntry[ T, - tuple[*[GetName[m] for m in Iter[Attrs[T]] if IsSub[GetType[m], Field]],], + tuple[ + *[GetName[m] for m in Iter[Attrs[T]] if IsSub[GetType[m], FieldType]], + ], ] type MakeQueryEntryNamedFields[ T: Table, @@ -241,7 +247,7 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): *[ GetName[m] for m in Iter[Attrs[T]] - if IsSub[GetType[m], Field] + if IsSub[GetType[m], FieldType] and any(IsSub[FieldName[GetType[m]], f] for f in Iter[FieldNames]) ], ], @@ -258,7 +264,7 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): else [MakeQueryEntryAllFields[New]] ), ] -type AddField[Entries, New: Field] = tuple[ +type AddField[Entries, New: FieldType] = tuple[ *[ # Existing entries ( e # Non-matching entry @@ -276,7 +282,7 @@ class DbLinkSource[Args: DbLinkSourceArgs](InitField[Args]): if not Bool[EntriesHasTable[Entries, FieldTable[New]]] ), ] -type AddEntries[Entries, News: tuple[Table | Field, ...]] = ( +type AddEntries[Entries, News: tuple[Table | FieldType, ...]] = ( Entries if IsSub[Length[News], Literal[0]] else AddEntries[ @@ -341,50 +347,44 @@ def execute[Es: tuple[type[Table], ...]]( class User(Table[Literal["users"]]): - id: Field[User, Literal["id"], int] = column( + id: Field[int] = column( db_type=DbInteger(), primary_key=True, autoincrement=True ) - name: Field[User, Literal["name"], str] = column( - db_type=DbString(length=150), nullable=False - ) - email: Field[User, Literal["email"], str] = column( + name: Field[str] = column(db_type=DbString(length=150), nullable=False) + email: Field[str] = column( db_type=DbString(length=100), unique=True, nullable=False ) - age: Field[User, Literal["age"], int | None] = column(db_type=DbInteger()) - active: Field[User, Literal["active"], bool] = column( + age: Field[int | None] = column(db_type=DbInteger()) + active: Field[bool] = column( db_type=DbBoolean(), default=True, nullable=False ) - posts: Field[User, Literal["posts"], list[Post]] = column( + posts: Field[list[Post]] = column( db_type=DbLinkSource(source="Post", cardinality=Cardinality.MANY) ) class Post(Table[Literal["posts"]]): - id: Field[Post, Literal["id"], int] = column( + id: Field[int] = column( db_type=DbInteger(), primary_key=True, autoincrement=True ) - content: Field[Post, Literal["content"], str] = column( - db_type=DbString(length=1000), nullable=False - ) - author: Field[Post, Literal["author"], User] = column( + content: Field[str] = column(db_type=DbString(length=1000), nullable=False) + author: Field[User] = column( db_type=DbLinkTarget(target=User), nullable=False ) - comments: Field[Post, Literal["comments"], list[Comment]] = column( + comments: Field[list[Comment]] = column( db_type=DbLinkSource(source="Comment", cardinality=Cardinality.MANY) ) class Comment(Table[Literal["comments"]]): - id: Field[Comment, Literal["id"], int] = column( + id: Field[int] = column( db_type=DbInteger(), primary_key=True, autoincrement=True ) - content: Field[Comment, Literal["content"], str] = column( - db_type=DbString(length=1000), nullable=False - ) - author: Field[Comment, Literal["author"], User] = column( + content: Field[str] = column(db_type=DbString(length=1000), nullable=False) + author: Field[User] = column( db_type=DbLinkTarget(target=User), nullable=False ) - post: Field[Comment, Literal["post"], Post] = column( + post: Field[Post] = column( db_type=DbLinkTarget(target=Post), nullable=False ) @@ -573,3 +573,84 @@ class Select[tests.test_qblike_3.User, tuple[typing.Literal['name']]]: class Select[tests.test_qblike_3.Post, tuple[typing.Literal['content']]]: content: str """) + + +def test_qblike_3_select_08(): + class UserAlias(User): + pass + + query = eval_call_with_types(select, UserAlias) + fmt = format_helper.format_class(query) + assert fmt == textwrap.dedent("""\ + class Query[tuple[tuple[tests.test_qblike_3.test_qblike_3_select_08..UserAlias, tuple[typing.Literal['id'], typing.Literal['name'], typing.Literal['email'], typing.Literal['age'], typing.Literal['active'], typing.Literal['posts']]]]]: + """) + + results = eval_call_with_types(Session.execute, Session, query) + result = eval_typing(GetArg[results, list, Literal[0]]) + + fmt = format_helper.format_class(result) + assert fmt == textwrap.dedent("""\ + class Select[tests.test_qblike_3.test_qblike_3_select_08..UserAlias, tuple[typing.Literal['id'], typing.Literal['name'], typing.Literal['email'], typing.Literal['age'], typing.Literal['active'], typing.Literal['posts']]]: + id: int + name: str + email: str + age: int | None + active: bool + posts: list[tests.test_qblike_3.Post] + """) + + +def test_qblike_3_select_09(): + class UserAlias(User): + pass + + user_alias_name = eval_typing(GetMemberType[UserAlias, Literal["name"]]) + user_alias_email = eval_typing(GetMemberType[UserAlias, Literal["email"]]) + query = eval_call_with_types(select, user_alias_name, user_alias_email) + fmt = format_helper.format_class(query) + assert fmt == textwrap.dedent("""\ + class Query[tuple[tuple[tests.test_qblike_3.test_qblike_3_select_09..UserAlias, tuple[typing.Literal['name'], typing.Literal['email']]]]]: + """) + + results = eval_call_with_types(Session.execute, Session, query) + result = eval_typing(GetArg[results, list, Literal[0]]) + + fmt = format_helper.format_class(result) + assert fmt == textwrap.dedent("""\ + class Select[tests.test_qblike_3.test_qblike_3_select_09..UserAlias, tuple[typing.Literal['name'], typing.Literal['email']]]: + name: str + email: str + """) + + +def test_qblike_3_select_10(): + class UserAlias(User): + pass + + user_name = eval_typing(GetMemberType[User, Literal["name"]]) + user_alias_name = eval_typing(GetMemberType[UserAlias, Literal["name"]]) + query = eval_call_with_types(select, user_name, user_alias_name) + fmt = format_helper.format_class(query) + assert fmt == textwrap.dedent("""\ + class Query[tuple[tuple[tests.test_qblike_3.User, tuple[typing.Literal['name']]], tuple[tests.test_qblike_3.test_qblike_3_select_10..UserAlias, tuple[typing.Literal['name']]]]]: + """) + + results = eval_call_with_types(Session.execute, Session, query) + result = eval_typing(GetArg[results, list, Literal[0]]) + + result_names = eval_typing(AttrNames[result]) + assert result_names == tuple[Literal["User"], Literal["UserAlias"]] + + result_user = eval_typing(GetMemberType[result, Literal["User"]]) + fmt = format_helper.format_class(result_user) + assert fmt == textwrap.dedent("""\ + class Select[tests.test_qblike_3.User, tuple[typing.Literal['name']]]: + name: str + """) + + result_user_alias = eval_typing(GetMemberType[result, Literal["UserAlias"]]) + fmt = format_helper.format_class(result_user_alias) + assert fmt == textwrap.dedent("""\ + class Select[tests.test_qblike_3.test_qblike_3_select_10..UserAlias, tuple[typing.Literal['name']]]: + name: str + """)