From 3a30ba4fff37a7ba9e8c4df486bb7de68cb9e0ae Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 28 Jan 2026 11:30:38 -0800 Subject: [PATCH 1/3] Add Slice. --- tests/test_type_eval.py | 17 +++++++++++++++++ typemap/type_eval/_eval_operators.py | 13 +++++++++++++ typemap/typing.py | 4 ++++ 3 files changed, 34 insertions(+) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 9eee8ca..1c68ef9 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -35,6 +35,7 @@ Members, NewProtocol, Param, + Slice, SpecialFormEllipsis, StrConcat, StrSlice, @@ -1043,6 +1044,22 @@ def test_eval_length_01(): assert d == Literal[None] +def test_eval_slice_01(): + t = tuple[Literal[0], Literal[1], Literal[2], Literal[3], Literal[4]] + d = eval_typing(Slice[t, Literal[1], Literal[3]]) + assert d == tuple[Literal[1], Literal[2]] + d = eval_typing(Slice[t, Literal[1], Literal[None]]) + assert d == tuple[Literal[1], Literal[2], Literal[3], Literal[4]] + d = eval_typing(Slice[t, Literal[None], Literal[3]]) + assert d == tuple[Literal[0], Literal[1], Literal[2]] + d = eval_typing(Slice[t, Literal[None], Literal[None]]) + assert ( + d == tuple[Literal[0], Literal[1], Literal[2], Literal[3], Literal[4]] + ) + d = eval_typing(Slice[t, Literal[1], Literal[1]]) + assert d == tuple[()] + + def test_eval_literal_idempotent_01(): t = Literal[int] for _ in range(5): diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 49a60ab..a829ef1 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -35,6 +35,7 @@ Members, NewProtocol, Param, + Slice, SpecialFormEllipsis, StrConcat, StrSlice, @@ -896,6 +897,18 @@ def _eval_Length(tp, *, ctx) -> typing.Any: raise TypeError(f"Invalid type argument to Length: {tp} is not a tuple") +@type_eval.register_evaluator(Slice) +@_lift_over_unions +def _eval_Slice(tp, start, end, *, ctx): + tp = _eval_types(tp, ctx) + start = _eval_literal(start, ctx) + end = _eval_literal(end, ctx) + if _typing_inspect.is_generic_alias(tp) and tp.__origin__ is tuple: + return tp.__origin__[tp.__args__[start:end]] + else: + raise TypeError(f"Invalid type argument to Slice: {tp} is not a tuple") + + # String literals diff --git a/typemap/typing.py b/typemap/typing.py index 021483d..f8f7e54 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -147,6 +147,10 @@ class Length[S: tuple]: pass +class Slice[S: str | tuple, Start: int | None, End: int | None]: + pass + + class Uppercase[S: str]: pass From 02b8f4462407ad26e4d79d5bd565f311c0af86e9 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 28 Jan 2026 11:32:52 -0800 Subject: [PATCH 2/3] Handle str with Slice. --- tests/test_type_eval.py | 19 ++++++++++++++++--- typemap/type_eval/_eval_operators.py | 8 ++++++-- typemap/typing.py | 4 ---- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 1c68ef9..d63819f 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -38,7 +38,6 @@ Slice, SpecialFormEllipsis, StrConcat, - StrSlice, Uppercase, ) @@ -241,12 +240,12 @@ def test_type_strings_4(): def test_type_strings_5(): - d = eval_typing(StrSlice[Literal["abcd"], Literal[0], Literal[1]]) + d = eval_typing(Slice[Literal["abcd"], Literal[0], Literal[1]]) assert d == Literal["a"] def test_type_strings_6(): - d = eval_typing(StrSlice[Literal["abcd"], Literal[1], Literal[None]]) + d = eval_typing(Slice[Literal["abcd"], Literal[1], Literal[None]]) assert d == Literal["bcd"] @@ -1060,6 +1059,20 @@ def test_eval_slice_01(): assert d == tuple[()] +def test_eval_slice_02(): + t = Literal["abcde"] + d = eval_typing(Slice[t, Literal[1], Literal[3]]) + assert d == Literal["bc"] + d = eval_typing(Slice[t, Literal[1], Literal[None]]) + assert d == Literal["bcde"] + d = eval_typing(Slice[t, Literal[None], Literal[3]]) + assert d == Literal["abc"] + d = eval_typing(Slice[t, Literal[None], Literal[None]]) + assert d == Literal["abcde"] + d = eval_typing(Slice[t, Literal[1], Literal[1]]) + assert d == Literal[""] + + def test_eval_literal_idempotent_01(): t = Literal[int] for _ in range(5): diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index a829ef1..41e4255 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -38,7 +38,6 @@ Slice, SpecialFormEllipsis, StrConcat, - StrSlice, Uncapitalize, Uppercase, ) @@ -905,6 +904,12 @@ def _eval_Slice(tp, start, end, *, ctx): end = _eval_literal(end, ctx) if _typing_inspect.is_generic_alias(tp) and tp.__origin__ is tuple: return tp.__origin__[tp.__args__[start:end]] + elif ( + _typing_inspect.is_generic_alias(tp) + and tp.__origin__ is typing.Literal + and isinstance(tp.__args__[0], str) + ): + return tp.__origin__[tp.__args__[0][start:end]] else: raise TypeError(f"Invalid type argument to Slice: {tp} is not a tuple") @@ -925,7 +930,6 @@ def func(*args, ctx): _string_literal_op(Capitalize, op=str.capitalize) _string_literal_op(Uncapitalize, op=lambda s: s[0:1].lower() + s[1:]) _string_literal_op(StrConcat, op=lambda s, t: s + t) -_string_literal_op(StrSlice, op=lambda s, start, end: s[start:end]) ################################################################## diff --git a/typemap/typing.py b/typemap/typing.py index f8f7e54..1c5d283 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -171,10 +171,6 @@ class StrConcat[S: str, T: str]: pass -class StrSlice[S: str, Start: int | None, End: int | None]: - pass - - class NewProtocol[*T]: pass From a7135eb8f17ed4c1375398e57745af8ffbb07d93 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 29 Jan 2026 08:25:24 -0800 Subject: [PATCH 3/3] Return Never for invalid inputs. --- tests/test_type_eval.py | 7 +++++++ typemap/type_eval/_eval_operators.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index d63819f..28b4057 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -1073,6 +1073,13 @@ def test_eval_slice_02(): assert d == Literal[""] +def test_eval_slice_03(): + d = eval_typing(Slice[int, Literal[1], Literal[2]]) + assert d == Never + d = eval_typing(Slice[dict[int, str], Literal[1], Literal[2]]) + assert d == Never + + def test_eval_literal_idempotent_01(): t = Literal[int] for _ in range(5): diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 41e4255..4b642b4 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -911,7 +911,7 @@ def _eval_Slice(tp, start, end, *, ctx): ): return tp.__origin__[tp.__args__[0][start:end]] else: - raise TypeError(f"Invalid type argument to Slice: {tp} is not a tuple") + return typing.Never # String literals