From 507088c3b2517ae809fbfa743f770e7fafbff1c3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 30 Sep 2024 19:50:36 +0200 Subject: [PATCH 01/17] type_params must be a tuple --- src/test_typing_extensions.py | 5 +++++ src/typing_extensions.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 8c2726f8..fa109c35 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7312,6 +7312,11 @@ def test_no_instance_subclassing(self): class MyAlias(TypeAliasType): pass + def test_type_params_possibilities(self): + T = TypeVar('T') + # Test not a tuple + with self.assertRaisesRegex(TypeError, "type_params must be a tuple"): + TypeAliasType("InvalidTypeParams", List[T], type_params=[T]) class DocTests(BaseTestCase): def test_annotation(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 5bf4f2dc..9949e6d2 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3531,6 +3531,8 @@ class TypeAliasType: def __init__(self, name: str, value, *, type_params=()): if not isinstance(name, str): raise TypeError("TypeAliasType name must be a string") + if not isinstance(type_params, tuple): + raise TypeError("type_params must be a tuple") self.__value__ = value self.__type_params__ = type_params From c00494a469e6ad23cda0a291fdd51b9ac93a8eed Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 30 Sep 2024 19:53:50 +0200 Subject: [PATCH 02/17] no non default value after default value Also draft for others --- src/test_typing_extensions.py | 59 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 7 +++++ 2 files changed, 66 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index fa109c35..eb12a625 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7318,6 +7318,65 @@ def test_type_params_possibilities(self): with self.assertRaisesRegex(TypeError, "type_params must be a tuple"): TypeAliasType("InvalidTypeParams", List[T], type_params=[T]) + # Regression test assure compatibility with typing.TypeVar + typing_T = typing.TypeVar('T') + with self.subTest(type_params="typing.TypeVar"): + TypeAliasType("TypingTypeParams", List[typing_T], type_params=(typing_T,)) + + # Test default order + T_default = TypeVar('T_default', default=int) + Ts = TypeVarTuple('Ts') + Ts_default = TypeVarTuple('Ts_default', default=Unpack[Tuple[str, int]]) + P = ParamSpec('P') + P_default = ParamSpec('P_default', default=[str, int]) + P_default2 = ParamSpec('P_default2', default=...) + + ok_cases = [ + (T, T_default), + (T, Ts_default), + (T, T_default, Ts_default), + (T, P, Ts), + (T, P, Ts_default), + (T, P_default), + (T, P_default, T_default), + (T, P_default, Ts_default), + (T, Ts_default, P_default), + (T, P_default, P_default2), + ] + invalid_cases = [ + (T_default, T), + (Ts_default, T), + + # TypeVar after TypeVarTuple + # "TypeVars with defaults cannot immediately follow TypeVarTuples" + # (T, Ts, T_default), + # (T, Ts_default, T_default), + + # Two TypeVarTuples in a row, should the reverse be also invalid? + (T, Ts_default, Ts), + + (P_default, T), + (P_default, Ts), + + # Double defintion + # (T, T) + # (Ts, *Ts) + # (P, **P) + + # Potentially add invalid inputs, e.g. literals or classes + # depends on upstream + # (1,) + # (str,) + ] + + for case in ok_cases: + with self.subTest(type_params=case): + TypeAliasType("OkCase", List[T], type_params=case) + for case in invalid_cases: + with self.subTest(type_params=case): + with self.assertRaises(TypeError): + TypeAliasType("InvalidCase", List[T], type_params=case) + class DocTests(BaseTestCase): def test_annotation(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9949e6d2..40a55f5d 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3536,8 +3536,15 @@ def __init__(self, name: str, value, *, type_params=()): self.__value__ = value self.__type_params__ = type_params + default_value_encountered = False parameters = [] for type_param in type_params: + has_default = getattr(type_param, '__default__', NoDefault) is not NoDefault + if default_value_encountered and not has_default: + raise TypeError(f'Type parameter {type_param!r} without a default' + ' follows type parameter with a default') + if has_default: + default_value_encountered = True if isinstance(type_param, TypeVarTuple): parameters.extend(type_param) else: From 4e2f041cb4920e6a209666aea2f88abffc73845a Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 30 Sep 2024 20:09:47 +0200 Subject: [PATCH 03/17] Consider Type Error from python/cpython/pull/124795 --- src/typing_extensions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 40a55f5d..6d10506a 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3539,6 +3539,8 @@ def __init__(self, name: str, value, *, type_params=()): default_value_encountered = False parameters = [] for type_param in type_params: + if not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)): + raise TypeError(f"Expected a type param, got {type_param!r}") has_default = getattr(type_param, '__default__', NoDefault) is not NoDefault if default_value_encountered and not has_default: raise TypeError(f'Type parameter {type_param!r} without a default' From b2412b028fd54a14152e6ed92d32f25e68636fc9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 1 Oct 2024 11:23:31 +0200 Subject: [PATCH 04/17] Adjusted error messages to match cpython --- src/test_typing_extensions.py | 43 ++++++++++++++++------------------- src/typing_extensions.py | 8 ++++--- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index eb12a625..dce876b0 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7318,25 +7318,25 @@ def test_type_params_possibilities(self): with self.assertRaisesRegex(TypeError, "type_params must be a tuple"): TypeAliasType("InvalidTypeParams", List[T], type_params=[T]) - # Regression test assure compatibility with typing.TypeVar + # Regression test to assure compatibility with typing.TypeVar typing_T = typing.TypeVar('T') with self.subTest(type_params="typing.TypeVar"): TypeAliasType("TypingTypeParams", List[typing_T], type_params=(typing_T,)) - # Test default order + # Test default order and other invalid inputs T_default = TypeVar('T_default', default=int) Ts = TypeVarTuple('Ts') Ts_default = TypeVarTuple('Ts_default', default=Unpack[Tuple[str, int]]) P = ParamSpec('P') P_default = ParamSpec('P_default', default=[str, int]) - P_default2 = ParamSpec('P_default2', default=...) + P_default2 = ParamSpec('P_default2', default=P_default) ok_cases = [ (T, T_default), (T, Ts_default), - (T, T_default, Ts_default), (T, P, Ts), - (T, P, Ts_default), + (P, T_default, Ts_default), + (Ts, P, Ts_default), (T, P_default), (T, P_default, T_default), (T, P_default, Ts_default), @@ -7344,37 +7344,32 @@ def test_type_params_possibilities(self): (T, P_default, P_default2), ] invalid_cases = [ - (T_default, T), - (Ts_default, T), - - # TypeVar after TypeVarTuple + ((T_default, T), f"non-default type parameter {T!r} follows default"), + ((P_default, P), f"non-default type parameter {P!r} follows default"), + ((Ts_default, Ts), f"non-default type parameter {Ts!r} follows default"), + + # Potentially add invalid inputs, e.g. literals or classes + # depends on upstream + ((1,), "Expected a type param, got 1"), + ((str,), f"Expected a type param, got {str!r}"), + # "TypeVars with defaults cannot immediately follow TypeVarTuples" + # is currently not enfored for the type statement, only for Generics # (T, Ts, T_default), # (T, Ts_default, T_default), - - # Two TypeVarTuples in a row, should the reverse be also invalid? - (T, Ts_default, Ts), - - (P_default, T), - (P_default, Ts), - - # Double defintion + + # Double use; however T2 = T; (T, T2) would likely be fine for a type checker # (T, T) # (Ts, *Ts) # (P, **P) - - # Potentially add invalid inputs, e.g. literals or classes - # depends on upstream - # (1,) - # (str,) ] for case in ok_cases: with self.subTest(type_params=case): TypeAliasType("OkCase", List[T], type_params=case) - for case in invalid_cases: + for case, msg in invalid_cases: with self.subTest(type_params=case): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, msg): TypeAliasType("InvalidCase", List[T], type_params=case) class DocTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 6d10506a..8efca3ee 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3541,10 +3541,12 @@ def __init__(self, name: str, value, *, type_params=()): for type_param in type_params: if not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)): raise TypeError(f"Expected a type param, got {type_param!r}") - has_default = getattr(type_param, '__default__', NoDefault) is not NoDefault + has_default = ( + getattr(type_param, '__default__', NoDefault) is not NoDefault + ) if default_value_encountered and not has_default: - raise TypeError(f'Type parameter {type_param!r} without a default' - ' follows type parameter with a default') + raise TypeError(f'non-default type parameter {type_param!r}' + ' follows default type parameter') if has_default: default_value_encountered = True if isinstance(type_param, TypeVarTuple): From 54a67ab03bd5a9a0dd209a981225ade9e85339d2 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 1 Oct 2024 11:32:01 +0200 Subject: [PATCH 05/17] updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db6719c6..1e25365d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ subscripted objects) had wrong parameters if they were directly subscripted with an `Unpack` object. Patch by [Daraan](https://github.com/Daraan). + - Backport of CPython PR [#124795](https://github.com/python/cpython/pull/124795) + and fix that `TypeAliasType` not raising an error on non-tupple inputs for `type_params`. + Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) From 64e4007b2a10e6d2fc353ab52a37d685d5290fb3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 1 Oct 2024 11:59:44 +0200 Subject: [PATCH 06/17] Removed invalid tests and left comment --- src/test_typing_extensions.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index dce876b0..891a145b 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7329,19 +7329,16 @@ def test_type_params_possibilities(self): Ts_default = TypeVarTuple('Ts_default', default=Unpack[Tuple[str, int]]) P = ParamSpec('P') P_default = ParamSpec('P_default', default=[str, int]) - P_default2 = ParamSpec('P_default2', default=P_default) - ok_cases = [ - (T, T_default), - (T, Ts_default), + # NOTE: "TypeVars with defaults cannot immediately follow TypeVarTuples" + # from PEP 696 is currently not enfored for the type statement and are not tested. + # PEP 695: Double usage of the same name is also not enforced and not tested. + valid_cases = [ (T, P, Ts), + (T, Ts_default), + (P_default, T_default) (P, T_default, Ts_default), - (Ts, P, Ts_default), - (T, P_default), - (T, P_default, T_default), - (T, P_default, Ts_default), - (T, Ts_default, P_default), - (T, P_default, P_default2), + (T_default, P_default, Ts_default), ] invalid_cases = [ ((T_default, T), f"non-default type parameter {T!r} follows default"), @@ -7352,19 +7349,9 @@ def test_type_params_possibilities(self): # depends on upstream ((1,), "Expected a type param, got 1"), ((str,), f"Expected a type param, got {str!r}"), - - # "TypeVars with defaults cannot immediately follow TypeVarTuples" - # is currently not enfored for the type statement, only for Generics - # (T, Ts, T_default), - # (T, Ts_default, T_default), - - # Double use; however T2 = T; (T, T2) would likely be fine for a type checker - # (T, T) - # (Ts, *Ts) - # (P, **P) ] - for case in ok_cases: + for case in valid_cases: with self.subTest(type_params=case): TypeAliasType("OkCase", List[T], type_params=case) for case, msg in invalid_cases: From dccb363869ae8a3bedc69492361b8d69071e2d9c Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 1 Oct 2024 13:02:55 +0200 Subject: [PATCH 07/17] Slight modification of tests - compatibility tests have their own test - skip tests for appropriate python versions --- src/test_typing_extensions.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 891a145b..39957155 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7312,17 +7312,25 @@ def test_no_instance_subclassing(self): class MyAlias(TypeAliasType): pass + def test_type_params_compatibility(self): + # Regression test to assure compatibility with typing variants + with self.subTest(type_params="typing.TypeVar"): + TypeAliasType("TypingTypeParams", ..., type_params=(typing.TypeVar('T'),)) + with self.subTest(type_params="typing.TypeAliasType"): + if not hasattr(typing, "TypeAliasType"): + self.skipTest("typing.TypeAliasType is not available before 3.12") + TypeAliasType("TypingTypeParams", ..., type_params=(typing.TypeVarTuple("Ts"),)) + with self.subTest(type_params="typing.TypeAliasType"): + if not hasattr(typing, "ParamSpec"): + self.skipTest("typing.ParamSpec is not available before 3.10") + TypeAliasType("TypingTypeParams", ..., type_params=(typing.ParamSpec("P"),)) + def test_type_params_possibilities(self): T = TypeVar('T') # Test not a tuple with self.assertRaisesRegex(TypeError, "type_params must be a tuple"): TypeAliasType("InvalidTypeParams", List[T], type_params=[T]) - # Regression test to assure compatibility with typing.TypeVar - typing_T = typing.TypeVar('T') - with self.subTest(type_params="typing.TypeVar"): - TypeAliasType("TypingTypeParams", List[typing_T], type_params=(typing_T,)) - # Test default order and other invalid inputs T_default = TypeVar('T_default', default=int) Ts = TypeVarTuple('Ts') @@ -7336,14 +7344,14 @@ def test_type_params_possibilities(self): valid_cases = [ (T, P, Ts), (T, Ts_default), - (P_default, T_default) + (P_default, T_default), (P, T_default, Ts_default), (T_default, P_default, Ts_default), ] invalid_cases = [ ((T_default, T), f"non-default type parameter {T!r} follows default"), ((P_default, P), f"non-default type parameter {P!r} follows default"), - ((Ts_default, Ts), f"non-default type parameter {Ts!r} follows default"), + ((Ts_default, T), f"non-default type parameter {T!r} follows default"), # Potentially add invalid inputs, e.g. literals or classes # depends on upstream @@ -7356,6 +7364,8 @@ def test_type_params_possibilities(self): TypeAliasType("OkCase", List[T], type_params=case) for case, msg in invalid_cases: with self.subTest(type_params=case): + if TYPING_3_12_0 and sys.version_info < (3, 12, 7) or sys.version_info[:3] < (3, 13, 1): + self.skipTest("No backport for <3.12.7 and 3.13.0, requires PR #124795") with self.assertRaisesRegex(TypeError, msg): TypeAliasType("InvalidCase", List[T], type_params=case) From 4edee59a958757d914c31692e35cebfbbdec6946 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 1 Oct 2024 14:24:07 +0200 Subject: [PATCH 08/17] fix indent --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e25365d..78e598e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ subscripted objects) had wrong parameters if they were directly subscripted with an `Unpack` object. Patch by [Daraan](https://github.com/Daraan). - - Backport of CPython PR [#124795](https://github.com/python/cpython/pull/124795) +- Backport of CPython PR [#124795](https://github.com/python/cpython/pull/124795) and fix that `TypeAliasType` not raising an error on non-tupple inputs for `type_params`. Patch by [Daraan](https://github.com/Daraan). From c928b20832ddc6321712e487aaf3bc2a3132ff37 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 1 Oct 2024 19:32:40 +0200 Subject: [PATCH 09/17] Corrected 3.12.8 requirement --- src/test_typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 39957155..23c59cee 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7364,8 +7364,8 @@ def test_type_params_possibilities(self): TypeAliasType("OkCase", List[T], type_params=case) for case, msg in invalid_cases: with self.subTest(type_params=case): - if TYPING_3_12_0 and sys.version_info < (3, 12, 7) or sys.version_info[:3] < (3, 13, 1): - self.skipTest("No backport for <3.12.7 and 3.13.0, requires PR #124795") + if TYPING_3_12_0 and sys.version_info < (3, 12, 8) or sys.version_info[:3] < (3, 13, 1): + self.skipTest("No backport for <3.12.8 and 3.13.0, requires PR #124795") with self.assertRaisesRegex(TypeError, msg): TypeAliasType("InvalidCase", List[T], type_params=case) From 4b9a04c035933ee32aa3606e0899bf7f29da659b Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 11 Oct 2024 17:06:45 +0200 Subject: [PATCH 10/17] Updated barrier to skip 3.12-3.13 --- src/test_typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 23c59cee..7207a241 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7364,8 +7364,8 @@ def test_type_params_possibilities(self): TypeAliasType("OkCase", List[T], type_params=case) for case, msg in invalid_cases: with self.subTest(type_params=case): - if TYPING_3_12_0 and sys.version_info < (3, 12, 8) or sys.version_info[:3] < (3, 13, 1): - self.skipTest("No backport for <3.12.8 and 3.13.0, requires PR #124795") + if TYPING_3_12_0 and sys.version_info < (3, 14): + self.skipTest("No backport for 3.12 and 3.13 requires cpython PR #124795") with self.assertRaisesRegex(TypeError, msg): TypeAliasType("InvalidCase", List[T], type_params=case) From e729c1f2ddb7903fd23e437a5aff4005f1798890 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 21 Oct 2024 19:01:48 +0200 Subject: [PATCH 11/17] Split compatibility checks into more methods --- src/test_typing_extensions.py | 42 ++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7207a241..e49a3423 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7312,18 +7312,38 @@ def test_no_instance_subclassing(self): class MyAlias(TypeAliasType): pass - def test_type_params_compatibility(self): + def test_type_var_compatibility(self): # Regression test to assure compatibility with typing variants - with self.subTest(type_params="typing.TypeVar"): - TypeAliasType("TypingTypeParams", ..., type_params=(typing.TypeVar('T'),)) - with self.subTest(type_params="typing.TypeAliasType"): - if not hasattr(typing, "TypeAliasType"): - self.skipTest("typing.TypeAliasType is not available before 3.12") - TypeAliasType("TypingTypeParams", ..., type_params=(typing.TypeVarTuple("Ts"),)) - with self.subTest(type_params="typing.TypeAliasType"): - if not hasattr(typing, "ParamSpec"): - self.skipTest("typing.ParamSpec is not available before 3.10") - TypeAliasType("TypingTypeParams", ..., type_params=(typing.ParamSpec("P"),)) + typingT = typing.TypeVar('typingT') + T1 = TypeAliasType("TypingTypeVar", ..., type_params=(typingT,)) + self.assertEqual(T1.__type_params__, (typingT,)) + + # Test typing_extensions backports + textT = TypeVar('textT') + T2 = TypeAliasType("TypingExtTypeVar", ..., type_params=(textT,)) + self.assertEqual(T2.__type_params__, (textT,)) + + textP = ParamSpec("textP") + T3 = TypeAliasType("TypingExtParamSpec", ..., type_params=(textP,)) + self.assertEqual(T3.__type_params__, (textP,)) + + textTs = TypeVarTuple("textTs") + T4 = TypeAliasType("TypingExtTypeVarTuple", ..., type_params=(textTs,)) + self.assertEqual(T4.__type_params__, (textTs,)) + + @skipUnless(TYPING_3_10_0, "typing.ParamSpec is not available before 3.10") + def test_param_spec_compatibility(self): + # Regression test to assure compatibility with typing variant + typingP = typing.ParamSpec("typingP") + T5 = TypeAliasType("TypingParamSpec", ..., type_params=(typingP,)) + self.assertEqual(T5.__type_params__, (typingP,)) + + @skipUnless(TYPING_3_12_0, "typing.TypeVarTuple is not available before 3.12") + def test_type_var_tuple_compatibility(self): + # Regression test to assure compatibility with typing variant + typingTs = typing.TypeVarTuple("typingTs") + T6 = TypeAliasType("TypingTypeVarTuple", ..., type_params=(typingTs,)) + self.assertEqual(T6.__type_params__, (typingTs,)) def test_type_params_possibilities(self): T = TypeVar('T') From 94b8c86cb039da69808ffc1dc6ad6879fe87713a Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 22 Oct 2024 16:51:15 +0200 Subject: [PATCH 12/17] Draft for 3.12, 3.13 backport --- src/test_typing_extensions.py | 8 +++++--- src/typing_extensions.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index d38bd90d..be00e52a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6192,6 +6192,10 @@ def test_typing_extensions_defers_when_possible(self): 'AsyncGenerator', 'ContextManager', 'AsyncContextManager', 'ParamSpec', 'TypeVar', 'TypeVarTuple', 'get_type_hints', } + if sys.version_info < (3, 14): + exclude |= { + 'TypeAliasType' + } if not typing_extensions._PEP_728_IMPLEMENTED: exclude |= {'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: @@ -7460,7 +7464,7 @@ def test_type_params_possibilities(self): ] invalid_cases = [ ((T_default, T), f"non-default type parameter {T!r} follows default"), - ((P_default, P), f"non-default type parameter {P!r} follows default"), + ((P_default, P), f"non-default type parameter {P!r} follows default"), ((Ts_default, T), f"non-default type parameter {T!r} follows default"), # Potentially add invalid inputs, e.g. literals or classes @@ -7474,8 +7478,6 @@ def test_type_params_possibilities(self): TypeAliasType("OkCase", List[T], type_params=case) for case, msg in invalid_cases: with self.subTest(type_params=case): - if TYPING_3_12_0 and sys.version_info < (3, 14): - self.skipTest("No backport for 3.12 and 3.13 requires cpython PR #124795") with self.assertRaisesRegex(TypeError, msg): TypeAliasType("InvalidCase", List[T], type_params=case) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index e43772f8..94b11a5a 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3528,8 +3528,36 @@ def __ror__(self, other): return typing.Union[other, self] -if hasattr(typing, "TypeAliasType"): +if hasattr(typing, "TypeAliasType") and sys.version_info >= (3, 14): TypeAliasType = typing.TypeAliasType +# 3.12-3.13 +elif hasattr(typing, "TypeAliasType"): + + class TypeAliasType: + + def __new__(self, name: str, value, *, type_params=()): + default_value_encountered = False + for type_param in type_params: + if not isinstance(type_param, + (typing.TypeVar, typing.TypeVarTuple, typing.ParamSpec) + ): + raise TypeError(f"Expected a type param, got {type_param!r}") + has_default = ( + getattr(type_param, '__default__', NoDefault) is not NoDefault + ) + if default_value_encountered and not has_default: + raise TypeError(f'non-default type parameter {type_param!r}' + ' follows default type parameter') + if has_default: + default_value_encountered = True + + return typing.TypeAliasType(name, value, type_params=type_params) + + def __init_subclass__(cls, *args, **kwargs): + raise TypeError( + "type 'typing_extensions.TypeAliasType' is not an acceptable base type" + ) + else: def _is_unionable(obj): """Corresponds to is_unionable() in unionobject.c in CPython.""" From 46ab3accbddc75d264b58d57b3310187a710a96e Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 22 Oct 2024 17:45:15 +0200 Subject: [PATCH 13/17] Use TypeAliasType backport for <3.14 --- src/typing_extensions.py | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 94b11a5a..00f5855b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3528,36 +3528,9 @@ def __ror__(self, other): return typing.Union[other, self] -if hasattr(typing, "TypeAliasType") and sys.version_info >= (3, 14): +if sys.version_info >= (3, 14): TypeAliasType = typing.TypeAliasType -# 3.12-3.13 -elif hasattr(typing, "TypeAliasType"): - - class TypeAliasType: - - def __new__(self, name: str, value, *, type_params=()): - default_value_encountered = False - for type_param in type_params: - if not isinstance(type_param, - (typing.TypeVar, typing.TypeVarTuple, typing.ParamSpec) - ): - raise TypeError(f"Expected a type param, got {type_param!r}") - has_default = ( - getattr(type_param, '__default__', NoDefault) is not NoDefault - ) - if default_value_encountered and not has_default: - raise TypeError(f'non-default type parameter {type_param!r}' - ' follows default type parameter') - if has_default: - default_value_encountered = True - - return typing.TypeAliasType(name, value, type_params=type_params) - - def __init_subclass__(cls, *args, **kwargs): - raise TypeError( - "type 'typing_extensions.TypeAliasType' is not an acceptable base type" - ) - +# 3.8-3.13 else: def _is_unionable(obj): """Corresponds to is_unionable() in unionobject.c in CPython.""" From b38852e981a30749eb59bf2fc49629da9e460fe3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 22 Oct 2024 17:53:58 +0200 Subject: [PATCH 14/17] fix typo and style --- src/test_typing_extensions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index be00e52a..a49cc16c 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7452,8 +7452,8 @@ def test_type_params_possibilities(self): P = ParamSpec('P') P_default = ParamSpec('P_default', default=[str, int]) - # NOTE: "TypeVars with defaults cannot immediately follow TypeVarTuples" - # from PEP 696 is currently not enfored for the type statement and are not tested. + # NOTE: PEP 696 states: "TypeVars with defaults cannot immediately follow TypeVarTuples" + # this is currently not enforced for the type statement and is not tested. # PEP 695: Double usage of the same name is also not enforced and not tested. valid_cases = [ (T, P, Ts), @@ -7463,13 +7463,13 @@ def test_type_params_possibilities(self): (T_default, P_default, Ts_default), ] invalid_cases = [ - ((T_default, T), f"non-default type parameter {T!r} follows default"), - ((P_default, P), f"non-default type parameter {P!r} follows default"), - ((Ts_default, T), f"non-default type parameter {T!r} follows default"), + ((T_default, T), f"non-default type parameter {T!r} follows default"), + ((P_default, P), f"non-default type parameter {P!r} follows default"), + ((Ts_default, T), f"non-default type parameter {T!r} follows default"), # Potentially add invalid inputs, e.g. literals or classes # depends on upstream - ((1,), "Expected a type param, got 1"), + ((1,), "Expected a type param, got 1"), ((str,), f"Expected a type param, got {str!r}"), ] From 1ef3e008eff127e08461d3df5d467e3642f4e716 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 22 Oct 2024 18:52:54 +0200 Subject: [PATCH 15/17] Unpack should not pass as a TypeVar --- src/test_typing_extensions.py | 6 +++--- src/typing_extensions.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a49cc16c..255a8428 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7466,11 +7466,11 @@ def test_type_params_possibilities(self): ((T_default, T), f"non-default type parameter {T!r} follows default"), ((P_default, P), f"non-default type parameter {P!r} follows default"), ((Ts_default, T), f"non-default type parameter {T!r} follows default"), - - # Potentially add invalid inputs, e.g. literals or classes - # depends on upstream + # Only type params are accepted ((1,), "Expected a type param, got 1"), ((str,), f"Expected a type param, got {str!r}"), + # Unpack backport behaves like TypeVar in some cases + ((Unpack[Ts],), f"Expected a type param, got {re.escape(repr(Unpack[Ts]))}"), ] for case in valid_cases: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 00f5855b..6413a3ec 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3611,7 +3611,10 @@ def __init__(self, name: str, value, *, type_params=()): default_value_encountered = False parameters = [] for type_param in type_params: - if not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)): + if (not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)) + # The Unpack backport passes aboves check + or _is_unpack(type_param) + ): raise TypeError(f"Expected a type param, got {type_param!r}") has_default = ( getattr(type_param, '__default__', NoDefault) is not NoDefault From cdf7fec4725e94b3b7762e0bb8f26b2854297611 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 23 Oct 2024 17:34:33 +0200 Subject: [PATCH 16/17] Clarified statement about Unpack for < 3.12 --- src/test_typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 255a8428..ec629b40 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7469,7 +7469,7 @@ def test_type_params_possibilities(self): # Only type params are accepted ((1,), "Expected a type param, got 1"), ((str,), f"Expected a type param, got {str!r}"), - # Unpack backport behaves like TypeVar in some cases + # Unpack is not a TypeVar but isinstance(Unpack[Ts], TypeVar) is True in Python < 3.12 ((Unpack[Ts],), f"Expected a type param, got {re.escape(repr(Unpack[Ts]))}"), ] From c14567dfbfc847a6f43fd8e36688f44fa63571f9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 23 Oct 2024 17:41:14 +0200 Subject: [PATCH 17/17] Updated unpack comment in main code --- src/typing_extensions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 6413a3ec..f9f93d7c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3611,8 +3611,10 @@ def __init__(self, name: str, value, *, type_params=()): default_value_encountered = False parameters = [] for type_param in type_params: - if (not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)) - # The Unpack backport passes aboves check + if ( + not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)) + # 3.8-3.11 + # Unpack Backport passes isinstance(type_param, TypeVar) or _is_unpack(type_param) ): raise TypeError(f"Expected a type param, got {type_param!r}")