From 2dd30a608e9bdcf57deb619713f38ece376f282f Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 18:30:30 -0800 Subject: [PATCH 01/15] Add _LiteralGeneric. --- typemap/typing.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/typemap/typing.py b/typemap/typing.py index 021483d..9365be6 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -217,3 +217,7 @@ def IsSubSimilar(self, tps): IsSub = IsSubSimilar + + +class _LiteralGeneric[T: bool]: + pass From 2aaf422f42308045f90b111379c23c65c0003bb3 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 17:06:50 -0800 Subject: [PATCH 02/15] Add tests. --- tests/test_type_eval.py | 98 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 9eee8ca..8331a05 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -39,6 +39,7 @@ StrConcat, StrSlice, Uppercase, + _LiteralGeneric, ) from . import format_helper @@ -1026,6 +1027,103 @@ def test_eval_iter_02(): assert d == tuple[int, str, int, str] +type NotLiteralGeneric[T] = not T +type AndLiteralGeneric[L, R] = L and R +type OrLiteralGeneric[L, R] = L or R +type LiteralGenericToLiteral[T] = Literal[True] if T else Literal[False] +type NotLiteralGenericToLiteral[T] = Literal[True] if not T else Literal[False] + + +def test_eval_literal_generic_01(): + d = eval_typing(_LiteralGeneric[True]) + assert d == _LiteralGeneric[True] + + d = eval_typing(_LiteralGeneric[False]) + assert d == _LiteralGeneric[False] + + d = eval_typing(_LiteralGeneric[1]) + assert d == _LiteralGeneric[True] + + d = eval_typing(_LiteralGeneric[0]) + assert d == _LiteralGeneric[False] + + +def test_eval_literal_generic_02(): + d = eval_typing(not _LiteralGeneric[True]) + assert d == _LiteralGeneric[False] + + d = eval_typing(NotLiteralGeneric[_LiteralGeneric[True]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(NotLiteralGeneric[_LiteralGeneric[False]]) + assert d == _LiteralGeneric[True] + + +def test_eval_literal_generic_03(): + d = eval_typing( + AndLiteralGeneric[_LiteralGeneric[True], _LiteralGeneric[True]] + ) + assert d == _LiteralGeneric[True] + + d = eval_typing( + AndLiteralGeneric[_LiteralGeneric[True], _LiteralGeneric[False]] + ) + assert d == _LiteralGeneric[False] + + d = eval_typing( + AndLiteralGeneric[_LiteralGeneric[False], _LiteralGeneric[True]] + ) + assert d == _LiteralGeneric[False] + + d = eval_typing( + AndLiteralGeneric[_LiteralGeneric[False], _LiteralGeneric[False]] + ) + assert d == _LiteralGeneric[False] + + +def test_eval_literal_generic_04(): + d = eval_typing( + OrLiteralGeneric[_LiteralGeneric[True], _LiteralGeneric[True]] + ) + assert d == _LiteralGeneric[True] + + d = eval_typing( + OrLiteralGeneric[_LiteralGeneric[True], _LiteralGeneric[False]] + ) + assert d == _LiteralGeneric[True] + + d = eval_typing( + OrLiteralGeneric[_LiteralGeneric[False], _LiteralGeneric[True]] + ) + assert d == _LiteralGeneric[True] + + d = eval_typing( + OrLiteralGeneric[_LiteralGeneric[False], _LiteralGeneric[False]] + ) + assert d == _LiteralGeneric[False] + + +def test_eval_literal_generic_05(): + d = eval_typing(LiteralGenericToLiteral[_LiteralGeneric[True]]) + assert d == Literal[True] + + d = eval_typing(LiteralGenericToLiteral[_LiteralGeneric[False]]) + assert d == Literal[False] + + +def test_eval_literal_generic_06(): + d = eval_typing(NotLiteralGenericToLiteral[_LiteralGeneric[True]]) + assert d == Literal[False] + + d = eval_typing(NotLiteralGenericToLiteral[_LiteralGeneric[False]]) + assert d == Literal[True] + + +def test_eval_literal_generic_error_01(): + with pytest.raises(TypeError, match="Expected literal type, got 'int'"): + eval_typing(_LiteralGeneric[int]) + + def test_eval_length_01(): d = eval_typing(Length[tuple[int, str]]) assert d == Literal[2] From 1d7c5fbe3440338f7232f444926671b172ea36e0 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 18:36:22 -0800 Subject: [PATCH 03/15] Wrap result booleans. --- typemap/type_eval/_eval_typing.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index 2db3fd7..bc6976d 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -182,6 +182,16 @@ def eval_typing(obj: typing.Any): result = _eval_types(obj, ctx) if not isinstance(result, list) and result in ctx.known_recursive_types: result = ctx.known_recursive_types[result] + + if isinstance(result, bool): + # Wrap a boolean result with _LiteralGeneric + # This is because `not` calls `__bool__` first so a boolean + # expression like `not _LiteralGeneric[True]` will result `False`, + # not `_LiteralGeneric[False]` as we want. + from typemap.typing import _LiteralGeneric + + result = _LiteralGeneric[result] # type: ignore[valid-type] + return result From bed4a5729d129a471876c42f37a78417f559df8d Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 19:01:44 -0800 Subject: [PATCH 04/15] Add _BoolValue. --- typemap/type_eval/_wrapped_value.py | 30 +++++++++++++++++++++++++++++ typemap/typing.py | 6 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 typemap/type_eval/_wrapped_value.py diff --git a/typemap/type_eval/_wrapped_value.py b/typemap/type_eval/_wrapped_value.py new file mode 100644 index 0000000..eb3f46d --- /dev/null +++ b/typemap/type_eval/_wrapped_value.py @@ -0,0 +1,30 @@ +class _BoolValue: + class _WrappedInstance: + def __init__(self, value: bool, type_name: str): + self._value = value + self._type_name = type_name + + def __bool__(self): + return self._value + + def __repr__(self): + return f"typemap.typing.{self._type_name}[{self._value!r}]" + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) + and self._value == other._value + ) + + def __hash__(self): + return hash((self._type_name, self._value)) + + def __init_subclass__(cls): + cls.__true_instance = cls._WrappedInstance(True, cls.__name__) + cls.__false_instance = cls._WrappedInstance(False, cls.__name__) + + def __class_getitem__(cls, item): + if isinstance(item, type): + raise TypeError(f"Expected literal type, got '{item.__name__}'") + + return cls.__true_instance if bool(item) else cls.__false_instance diff --git a/typemap/typing.py b/typemap/typing.py index 9365be6..8f2d93b 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -3,6 +3,10 @@ from typing import Literal from typing import _GenericAlias # type: ignore + +from .type_eval._wrapped_value import _BoolValue + + _SpecialForm: typing.Any = typing._SpecialForm # Not type-level computation but related @@ -219,5 +223,5 @@ def IsSubSimilar(self, tps): IsSub = IsSubSimilar -class _LiteralGeneric[T: bool]: +class _LiteralGeneric[B: bool](_BoolValue): pass From 0292f8a78de3b71ac94bbb7b76c1dcbf71dcfe4f Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 17:07:12 -0800 Subject: [PATCH 05/15] Fix other tests. --- tests/test_type_eval.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 8331a05..e5b1cb1 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -994,7 +994,7 @@ def test_uppercase_never(): def test_never_is(): d = eval_typing(IsSub[Never, Never]) - assert d is True + assert d == _LiteralGeneric[True] def test_eval_iter_01(): @@ -1150,7 +1150,9 @@ def test_eval_literal_idempotent_01(): def test_is_literal_true_vs_one(): - assert eval_typing(IsSub[Literal[True], Literal[1]]) is False + assert ( + eval_typing(IsSub[Literal[True], Literal[1]]) == _LiteralGeneric[False] + ) def test_callable_to_signature_01(): @@ -1318,7 +1320,7 @@ class AnnoTest: def test_type_eval_annotated_02(): res = eval_typing(IsSub[GetAttr[AnnoTest, Literal["a"]], int]) - assert res is True + assert res == _LiteralGeneric[True] def test_type_eval_annotated_03(): From 767f4e5ffe95e8ff361accce26118f97997c6d87 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 19:21:22 -0800 Subject: [PATCH 06/15] Add Bool. --- tests/test_type_eval.py | 65 ++++++++++++++++++++++++++++ typemap/type_eval/_eval_operators.py | 17 ++++++++ typemap/typing.py | 4 ++ 3 files changed, 86 insertions(+) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index e5b1cb1..65eae2d 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -20,6 +20,7 @@ from typemap.type_eval import eval_typing from typemap.typing import ( Attrs, + Bool, FromUnion, GenericCallable, GetArg, @@ -1034,6 +1035,70 @@ def test_eval_iter_02(): type NotLiteralGenericToLiteral[T] = Literal[True] if not T else Literal[False] +def test_eval_bool_01(): + d = eval_typing(Bool[Literal[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(Bool[Literal[False]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(Bool[Literal[1]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(Bool[Literal[0]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(Bool[Literal["true"]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(Bool[Literal["false"]]) + assert d == _LiteralGeneric[True] + + +def test_eval_bool_02(): + d = eval_typing(Bool[_LiteralGeneric[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(Bool[_LiteralGeneric[False]]) + assert d == _LiteralGeneric[False] + + +def test_eval_bool_03(): + d = eval_typing(NotLiteralGeneric[Bool[Literal[True]]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(NotLiteralGeneric[Bool[Literal[False]]]) + assert d == _LiteralGeneric[True] + + +type NestedBool0[T] = Bool[T] +type NestedBool1[T] = NestedBool0[Bool[T]] +type NestedBool2[T] = NestedBool1[Bool[T]] +type NestedBool3[T] = NestedBool2[Bool[T]] +type NestedBool4[T] = NestedBool3[Bool[T]] +type NestedBool5[T] = NestedBool4[Bool[T]] + + +def test_eval_bool_04(): + d = eval_typing(NestedBool5[Literal[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(NestedBool5[Literal[False]]) + assert d == _LiteralGeneric[False] + + +type IsIntBool[T] = Bool[IsSub[T, int]] +type IsIntLiteral[T] = Literal[True] if IsIntBool[T] else Literal[False] + + +def test_eval_bool_05(): + d = eval_typing(IsIntLiteral[int]) + assert d == Literal[True] + + d = eval_typing(IsIntLiteral[str]) + assert d == Literal[False] + + def test_eval_literal_generic_01(): d = eval_typing(_LiteralGeneric[True]) assert d == _LiteralGeneric[True] diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 49a60ab..ab530c4 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -17,6 +17,7 @@ ) from typemap.typing import ( Attrs, + Bool, Capitalize, DropAnnotations, FromUnion, @@ -40,7 +41,9 @@ StrSlice, Uncapitalize, Uppercase, + _LiteralGeneric, ) +from typemap.type_eval._wrapped_value import _BoolValue ################################################################## @@ -242,6 +245,20 @@ def _eval_IsSubSimilar(lhs, rhs, *, ctx): return type_eval.issubsimilar(lhs, rhs) +@type_eval.register_evaluator(Bool) +@_lift_evaluated +def _eval_Bool(tp, *, ctx): + return _eval_bool_tp(tp) + + +def _eval_bool_tp(tp): + if typing.get_origin(tp) is typing.Literal: + return _LiteralGeneric[tp.__args__[0]] + elif isinstance(tp, _BoolValue._WrappedInstance): + return _LiteralGeneric[tp._value] + raise TypeError(f"Expected Literal type, got {tp}") + + ################################################################## diff --git a/typemap/typing.py b/typemap/typing.py index 8f2d93b..c065e11 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -223,5 +223,9 @@ def IsSubSimilar(self, tps): IsSub = IsSubSimilar +class Bool[T: typing.Literal[True, False]]: + pass + + class _LiteralGeneric[B: bool](_BoolValue): pass From 68b886a765f4aed564cd002ab83621085fa3c22f Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 20:02:31 -0800 Subject: [PATCH 07/15] Immediately execute type aliases in type aliases. --- typemap/type_eval/_eval_typing.py | 52 ++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index bc6976d..a8632a9 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -410,7 +410,13 @@ def _eval_applied_type_alias(obj: types.GenericAlias, ctx: EvalContext): # Type alias types are already added in _eval_types child_ctx.alias_stack.add(new_obj) - ff = types.FunctionType(func.__code__, mod.__dict__, None, None, args) + ff = types.FunctionType( + func.__code__, + _GlobalsWrapper(mod.__dict__, child_ctx), + None, + None, + args, + ) unpacked = ff(annotationlib.Format.VALUE) child_ctx.seen[obj] = unpacked @@ -422,6 +428,50 @@ def _eval_applied_type_alias(obj: types.GenericAlias, ctx: EvalContext): return evaled +class _GlobalsWrapper(dict): + """Wraps module dict to make type aliases in type aliases evaluate + immediately. + + This allows us to ensure that generic aliases which resolve to + _LiteralGeneric are evaluated *before* they are used as booleans. + + For example, suppose we have: + + type IsIntBool[T] = Bool[IsSub[T, int]] + type IsIntLiteral[T] = Literal[True] if IsIntBool[T] else Literal[False] + + Though Bool is a special form, IsIntBool is not, and so when used in a + boolean context, it will always evaluate to true. + """ + + def __init__(self, base_dict, ctx): + super().__init__(base_dict) + self._ctx = ctx + + def __getitem__(self, key): + value = super().__getitem__(key) + if isinstance(value, typing.TypeAliasType): + return _TypeAliasWrapper(value, self._ctx) + return value + + +class _TypeAliasWrapper: + def __init__(self, type_alias, ctx): + self._type_alias = type_alias + self._ctx = ctx + + def __getitem__(self, item): + result = self._type_alias[item] + # If the result of a type alias is a generic alias, immediately + # evaluate it. + if isinstance(result, types.GenericAlias): + return _eval_types(result, self._ctx) + return result + + def __getattr__(self, name): + return getattr(self._type_alias, name) + + @_eval_types_impl.register def _eval_applied_class(obj: typing_GenericAlias, ctx: EvalContext): """Eval a typing._GenericAlias -- an applied user-defined class""" From b4619849196a845577c46cd20e6faefe8559ec10 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 19:32:39 -0800 Subject: [PATCH 08/15] Add AnyOf. --- tests/test_type_eval.py | 84 ++++++++++++++++++++++++++++ typemap/type_eval/_eval_operators.py | 7 +++ typemap/typing.py | 4 ++ 3 files changed, 95 insertions(+) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 65eae2d..d05f4e8 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -19,6 +19,7 @@ from typemap.type_eval import eval_typing from typemap.typing import ( + AnyOf, Attrs, Bool, FromUnion, @@ -1099,6 +1100,89 @@ def test_eval_bool_05(): assert d == Literal[False] +def test_eval_any_01(): + d = eval_typing(AnyOf[()]) + assert d == _LiteralGeneric[False] + + d = eval_typing(AnyOf[_LiteralGeneric[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AnyOf[_LiteralGeneric[False]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(AnyOf[_LiteralGeneric[True], _LiteralGeneric[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AnyOf[_LiteralGeneric[True], _LiteralGeneric[False]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AnyOf[_LiteralGeneric[False], _LiteralGeneric[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AnyOf[_LiteralGeneric[False], _LiteralGeneric[False]]) + assert d == _LiteralGeneric[False] + + +def test_eval_any_02(): + d = eval_typing(AnyOf[()]) + assert d == _LiteralGeneric[False] + + d = eval_typing(AnyOf[Literal[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AnyOf[Literal[False]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(AnyOf[Literal[True], Literal[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AnyOf[Literal[True], Literal[False]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AnyOf[Literal[False], Literal[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AnyOf[Literal[False], Literal[False]]) + assert d == _LiteralGeneric[False] + + +type ContainsAnyInt[Ts] = AnyOf[*[IsSub[t, int] for t in Iter[Ts]]] +type ContainsAnyIntToLiteral[Ts] = ( + Literal[True] if ContainsAnyInt[Ts] else Literal[False] +) + + +def test_eval_any_03(): + d = eval_typing(ContainsAnyInt[tuple[()]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(ContainsAnyInt[tuple[int]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(ContainsAnyInt[tuple[str]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(ContainsAnyInt[tuple[int, int]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(ContainsAnyInt[tuple[int, str]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(ContainsAnyInt[tuple[str, str]]) + assert d == _LiteralGeneric[False] + + +def test_eval_any_04(): + d = eval_typing(ContainsAnyIntToLiteral[tuple[()]]) + assert d == Literal[False] + + d = eval_typing(ContainsAnyIntToLiteral[tuple[int]]) + assert d == Literal[True] + + d = eval_typing(ContainsAnyIntToLiteral[tuple[str]]) + assert d == Literal[False] + + def test_eval_literal_generic_01(): d = eval_typing(_LiteralGeneric[True]) assert d == _LiteralGeneric[True] diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index ab530c4..5c7e027 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -16,6 +16,7 @@ _get_class_type_hint_namespaces, ) from typemap.typing import ( + AnyOf, Attrs, Bool, Capitalize, @@ -245,6 +246,12 @@ def _eval_IsSubSimilar(lhs, rhs, *, ctx): return type_eval.issubsimilar(lhs, rhs) +@type_eval.register_evaluator(AnyOf) +@_lift_evaluated +def _eval_AnyOf(*tps, ctx): + return _LiteralGeneric[any(_eval_bool_tp(tp) for tp in tps)] + + @type_eval.register_evaluator(Bool) @_lift_evaluated def _eval_Bool(tp, *, ctx): diff --git a/typemap/typing.py b/typemap/typing.py index c065e11..eb7de45 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -223,6 +223,10 @@ def IsSubSimilar(self, tps): IsSub = IsSubSimilar +class AnyOf[*Ts]: + pass + + class Bool[T: typing.Literal[True, False]]: pass From 1f4ce6e56cd566035e2eae5dbc756bbd7fd3e338 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 21:05:49 -0800 Subject: [PATCH 09/15] Add AllOf. --- tests/test_type_eval.py | 72 ++++++++++++++++++++++++++++ typemap/type_eval/_eval_operators.py | 7 +++ typemap/typing.py | 4 ++ 3 files changed, 83 insertions(+) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index d05f4e8..aa9fe2a 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -19,6 +19,7 @@ from typemap.type_eval import eval_typing from typemap.typing import ( + AllOf, AnyOf, Attrs, Bool, @@ -1100,6 +1101,77 @@ def test_eval_bool_05(): assert d == Literal[False] +def test_eval_all_01(): + d = eval_typing(AllOf[()]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AllOf[_LiteralGeneric[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AllOf[_LiteralGeneric[False]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(AllOf[_LiteralGeneric[True], _LiteralGeneric[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AllOf[_LiteralGeneric[True], _LiteralGeneric[False]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(AllOf[_LiteralGeneric[False], _LiteralGeneric[True]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(AllOf[_LiteralGeneric[False], _LiteralGeneric[False]]) + assert d == _LiteralGeneric[False] + + +def test_eval_all_02(): + d = eval_typing(AllOf[Literal[True], Literal[True]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(AllOf[Literal[True], Literal[False]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(AllOf[Literal[False], Literal[True]]) + assert d == _LiteralGeneric[False] + + +type ContainsAllInt[Ts] = AllOf[*[IsSub[t, int] for t in Iter[Ts]]] +type ContainsAllIntToLiteral[Ts] = ( + Literal[True] if ContainsAllInt[Ts] else Literal[False] +) + + +def test_eval_all_03(): + d = eval_typing(ContainsAllInt[tuple[()]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(ContainsAllInt[tuple[int]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(ContainsAllInt[tuple[str]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(ContainsAllInt[tuple[int, int]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(ContainsAllInt[tuple[int, str]]) + assert d == _LiteralGeneric[False] + + d = eval_typing(ContainsAllInt[tuple[str, str]]) + assert d == _LiteralGeneric[False] + + +def test_eval_all_04(): + d = eval_typing(ContainsAllIntToLiteral[tuple[()]]) + assert d == Literal[True] + + d = eval_typing(ContainsAllIntToLiteral[tuple[int]]) + assert d == Literal[True] + + d = eval_typing(ContainsAllIntToLiteral[tuple[str]]) + assert d == Literal[False] + + def test_eval_any_01(): d = eval_typing(AnyOf[()]) assert d == _LiteralGeneric[False] diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 5c7e027..ad1199d 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -16,6 +16,7 @@ _get_class_type_hint_namespaces, ) from typemap.typing import ( + AllOf, AnyOf, Attrs, Bool, @@ -246,6 +247,12 @@ def _eval_IsSubSimilar(lhs, rhs, *, ctx): return type_eval.issubsimilar(lhs, rhs) +@type_eval.register_evaluator(AllOf) +@_lift_evaluated +def _eval_AllOf(*tps, ctx): + return _LiteralGeneric[all(_eval_bool_tp(tp) for tp in tps)] + + @type_eval.register_evaluator(AnyOf) @_lift_evaluated def _eval_AnyOf(*tps, ctx): diff --git a/typemap/typing.py b/typemap/typing.py index eb7de45..ec90593 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -223,6 +223,10 @@ def IsSubSimilar(self, tps): IsSub = IsSubSimilar +class AllOf[*Ts]: + pass + + class AnyOf[*Ts]: pass From 829e9cabbdae02c56952907b3f99241cbb02819d Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 28 Jan 2026 09:26:31 -0800 Subject: [PATCH 10/15] Add Matches. --- tests/test_type_eval.py | 94 ++++++++++++++++++++++++++++ typemap/type_eval/_eval_operators.py | 9 +++ typemap/typing.py | 4 ++ 3 files changed, 107 insertions(+) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index aa9fe2a..847394f 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -34,6 +34,7 @@ IsSub, Iter, Length, + Matches, Member, Members, NewProtocol, @@ -1000,6 +1001,99 @@ def test_never_is(): assert d == _LiteralGeneric[True] +def test_matches_01(): + d = eval_typing(Matches[int, int]) + assert d == _LiteralGeneric[True] + + d = eval_typing(Matches[int, str]) + assert d == _LiteralGeneric[False] + + d = eval_typing(Matches[str, int]) + assert d == _LiteralGeneric[False] + + +def test_matches_02(): + class A: + pass + + class B(A): + pass + + class C(B): + pass + + class D(A): + pass + + class X: + pass + + d = eval_typing(Matches[A, A]) + assert d == _LiteralGeneric[True] + + d = eval_typing(Matches[A, B]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[B, A]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[B, C]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[C, B]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[C, D]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[D, C]) + assert d == _LiteralGeneric[False] + + d = eval_typing(Matches[A, X]) + assert d == _LiteralGeneric[False] + + +def test_matches_03(): + class A[T]: + pass + + class B[T](A[T]): + pass + + class C(B[int]): + pass + + class D(A[str]): + pass + + class X: + pass + + d = eval_typing(Matches[A[int], A[int]]) + assert d == _LiteralGeneric[True] + d = eval_typing(Matches[A[int], A[str]]) + assert d == _LiteralGeneric[True] + + d = eval_typing(Matches[A[int], B[int]]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[B[int], A[int]]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[A[int], B[str]]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[B[str], A[int]]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[B[int], C]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[C, B[int]]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[B[str], C]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[C, B[str]]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[C, D]) + assert d == _LiteralGeneric[False] + d = eval_typing(Matches[D, C]) + assert d == _LiteralGeneric[False] + + d = eval_typing(Matches[A[int], X]) + assert d == _LiteralGeneric[False] + + def test_eval_iter_01(): d = eval_typing(Iter[tuple[int, str]]) assert tuple(d) == (int, str) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index ad1199d..7f5a20d 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -34,6 +34,7 @@ Iter, Length, Lowercase, + Matches, Member, Members, NewProtocol, @@ -247,6 +248,14 @@ def _eval_IsSubSimilar(lhs, rhs, *, ctx): return type_eval.issubsimilar(lhs, rhs) +@type_eval.register_evaluator(Matches) +@_lift_evaluated +def _eval_Matches(lhs, rhs, *, ctx): + return _LiteralGeneric[ + type_eval.issubsimilar(lhs, rhs) and type_eval.issubsimilar(rhs, lhs) + ] + + @type_eval.register_evaluator(AllOf) @_lift_evaluated def _eval_AllOf(*tps, ctx): diff --git a/typemap/typing.py b/typemap/typing.py index ec90593..32aadcc 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -220,6 +220,10 @@ def IsSubSimilar(self, tps): return _IsGenericAlias(self, tps) +class Matches[Lhs, Rhs]: + pass + + IsSub = IsSubSimilar From e47400a63cc9cfe2b81b9cb168ae4fa1a5927672 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Tue, 27 Jan 2026 21:09:23 -0800 Subject: [PATCH 11/15] Use _LiteralGeneric in IsSubtype and IsSubSimilar. --- typemap/type_eval/_eval_operators.py | 4 ++-- typemap/typing.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 7f5a20d..eff8c51 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -239,13 +239,13 @@ def _eval_Iter(tp, *, ctx): @type_eval.register_evaluator(IsSubtype) @_lift_evaluated def _eval_IsSubtype(lhs, rhs, *, ctx): - return type_eval.issubtype(lhs, rhs) + return _LiteralGeneric[type_eval.issubtype(lhs, rhs)] @type_eval.register_evaluator(IsSubSimilar) @_lift_evaluated def _eval_IsSubSimilar(lhs, rhs, *, ctx): - return type_eval.issubsimilar(lhs, rhs) + return _LiteralGeneric[type_eval.issubsimilar(lhs, rhs)] @type_eval.register_evaluator(Matches) diff --git a/typemap/typing.py b/typemap/typing.py index 32aadcc..42e5a35 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -205,7 +205,7 @@ class _IsGenericAlias(_GenericAlias, _root=True): # type: ignore[call-arg] def __bool__(self): evaluator = special_form_evaluator.get() if evaluator: - return evaluator(self) + return bool(evaluator(self)) else: return False From 737aa0d137adc1a9fabb24fc0af48456d682e0d2 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 28 Jan 2026 09:50:22 -0800 Subject: [PATCH 12/15] Remove special form for IsSubtype and IsSubSimilar.. --- typemap/typing.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/typemap/typing.py b/typemap/typing.py index 42e5a35..e0921ac 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -210,14 +210,12 @@ def __bool__(self): return False -@_SpecialForm -def IsSubtype(self, tps): - return _IsGenericAlias(self, tps) +class IsSubtype[Lhs, Rhs]: + pass -@_SpecialForm -def IsSubSimilar(self, tps): - return _IsGenericAlias(self, tps) +class IsSubSimilar[Lhs, Rhs]: + pass class Matches[Lhs, Rhs]: From 60b2f08edb497c499baf0b60d15fac2657142944 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 28 Jan 2026 10:19:01 -0800 Subject: [PATCH 13/15] Wrap _BoolExpr for immediate execution. --- typemap/type_eval/_eval_typing.py | 29 +++++++++++++++++++++++++---- typemap/type_eval/_wrapped_value.py | 4 ++++ typemap/typing.py | 14 +++++++------- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index a8632a9..be525c8 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -21,7 +21,7 @@ if typing.TYPE_CHECKING: from typing import Any -from . import _apply_generic, _typing_inspect +from . import _apply_generic, _typing_inspect, _wrapped_value __all__ = ("eval_typing",) @@ -437,11 +437,12 @@ class _GlobalsWrapper(dict): For example, suppose we have: - type IsIntBool[T] = Bool[IsSub[T, int]] + type IsIntBool[T] = IsSub[T, int] type IsIntLiteral[T] = Literal[True] if IsIntBool[T] else Literal[False] - Though Bool is a special form, IsIntBool is not, and so when used in a - boolean context, it will always evaluate to true. + Though `IsSub` results in a `_LiteralGeneric`, `IsIntBool` is not itself + a `_LiteralGeneric`. So when used in `IsIntLiteral`, it will always evaluate + to true. """ def __init__(self, base_dict, ctx): @@ -452,6 +453,10 @@ def __getitem__(self, key): value = super().__getitem__(key) if isinstance(value, typing.TypeAliasType): return _TypeAliasWrapper(value, self._ctx) + elif isinstance(value, type) and issubclass( + value, _wrapped_value._BoolExpr + ): + return _GenericClassWrapper(value, self._ctx) return value @@ -472,6 +477,22 @@ def __getattr__(self, name): return getattr(self._type_alias, name) +class _GenericClassWrapper: + def __init__(self, generic_class, ctx): + self._generic_class = generic_class + self._ctx = ctx + + def __getitem__(self, item): + result = self._generic_class[item] + # Immediately evaluate the generic alias + if isinstance(result, (types.GenericAlias, typing._GenericAlias)): + return _eval_types(result, self._ctx) + return result + + def __getattr__(self, name): + return getattr(self._generic_class, name) + + @_eval_types_impl.register def _eval_applied_class(obj: typing_GenericAlias, ctx: EvalContext): """Eval a typing._GenericAlias -- an applied user-defined class""" diff --git a/typemap/type_eval/_wrapped_value.py b/typemap/type_eval/_wrapped_value.py index eb3f46d..0b6bbc8 100644 --- a/typemap/type_eval/_wrapped_value.py +++ b/typemap/type_eval/_wrapped_value.py @@ -1,3 +1,7 @@ +class _BoolExpr: + pass + + class _BoolValue: class _WrappedInstance: def __init__(self, value: bool, type_name: str): diff --git a/typemap/typing.py b/typemap/typing.py index e0921ac..eada354 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -4,7 +4,7 @@ from typing import _GenericAlias # type: ignore -from .type_eval._wrapped_value import _BoolValue +from .type_eval._wrapped_value import _BoolExpr, _BoolValue _SpecialForm: typing.Any = typing._SpecialForm @@ -210,30 +210,30 @@ def __bool__(self): return False -class IsSubtype[Lhs, Rhs]: +class IsSubtype[Lhs, Rhs](_BoolExpr): pass -class IsSubSimilar[Lhs, Rhs]: +class IsSubSimilar[Lhs, Rhs](_BoolExpr): pass -class Matches[Lhs, Rhs]: +class Matches[Lhs, Rhs](_BoolExpr): pass IsSub = IsSubSimilar -class AllOf[*Ts]: +class AllOf[*Ts](_BoolExpr): pass -class AnyOf[*Ts]: +class AnyOf[*Ts](_BoolExpr): pass -class Bool[T: typing.Literal[True, False]]: +class Bool[T: typing.Literal[True, False]](_BoolExpr): pass From 275b46882e5d6f131322b2d9aef0a17d607f8495 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 28 Jan 2026 13:06:24 -0800 Subject: [PATCH 14/15] Remove immediate execution for type aliases. --- typemap/type_eval/_eval_typing.py | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/typemap/type_eval/_eval_typing.py b/typemap/type_eval/_eval_typing.py index be525c8..dae8291 100644 --- a/typemap/type_eval/_eval_typing.py +++ b/typemap/type_eval/_eval_typing.py @@ -437,12 +437,10 @@ class _GlobalsWrapper(dict): For example, suppose we have: - type IsIntBool[T] = IsSub[T, int] - type IsIntLiteral[T] = Literal[True] if IsIntBool[T] else Literal[False] + type BoolToLiteral[T] = Literal[True] if Bool[T] else Literal[False] - Though `IsSub` results in a `_LiteralGeneric`, `IsIntBool` is not itself - a `_LiteralGeneric`. So when used in `IsIntLiteral`, it will always evaluate - to true. + Though `Bool` results in a `_LiteralGeneric`, it is not one itself. So when + used in `BoolToLiteral`, it will always evaluate to true. """ def __init__(self, base_dict, ctx): @@ -451,32 +449,13 @@ def __init__(self, base_dict, ctx): def __getitem__(self, key): value = super().__getitem__(key) - if isinstance(value, typing.TypeAliasType): - return _TypeAliasWrapper(value, self._ctx) - elif isinstance(value, type) and issubclass( + if isinstance(value, type) and issubclass( value, _wrapped_value._BoolExpr ): return _GenericClassWrapper(value, self._ctx) return value -class _TypeAliasWrapper: - def __init__(self, type_alias, ctx): - self._type_alias = type_alias - self._ctx = ctx - - def __getitem__(self, item): - result = self._type_alias[item] - # If the result of a type alias is a generic alias, immediately - # evaluate it. - if isinstance(result, types.GenericAlias): - return _eval_types(result, self._ctx) - return result - - def __getattr__(self, name): - return getattr(self._type_alias, name) - - class _GenericClassWrapper: def __init__(self, generic_class, ctx): self._generic_class = generic_class From 33ee98d09e51b79d2c610af197f0394eb91a329c Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 28 Jan 2026 13:00:23 -0800 Subject: [PATCH 15/15] Fix test to use Bool when calling type-bool type aliases. --- tests/test_type_eval.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 847394f..4a13b36 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1184,7 +1184,7 @@ def test_eval_bool_04(): type IsIntBool[T] = Bool[IsSub[T, int]] -type IsIntLiteral[T] = Literal[True] if IsIntBool[T] else Literal[False] +type IsIntLiteral[T] = Literal[True] if Bool[IsIntBool[T]] else Literal[False] def test_eval_bool_05(): @@ -1231,7 +1231,7 @@ def test_eval_all_02(): type ContainsAllInt[Ts] = AllOf[*[IsSub[t, int] for t in Iter[Ts]]] type ContainsAllIntToLiteral[Ts] = ( - Literal[True] if ContainsAllInt[Ts] else Literal[False] + Literal[True] if Bool[ContainsAllInt[Ts]] else Literal[False] ) @@ -1314,7 +1314,7 @@ def test_eval_any_02(): type ContainsAnyInt[Ts] = AnyOf[*[IsSub[t, int] for t in Iter[Ts]]] type ContainsAnyIntToLiteral[Ts] = ( - Literal[True] if ContainsAnyInt[Ts] else Literal[False] + Literal[True] if Bool[ContainsAnyInt[Ts]] else Literal[False] )