From ebb8ef6c937fc7d8b8b579011f0ae65a74996995 Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Fri, 27 Oct 2023 11:22:25 +0000 Subject: [PATCH 01/13] defaulted TypeVar doesn't require argument --- src/typing_extensions.py | 70 ++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 58706dc9..759c2535 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -145,27 +145,6 @@ def __repr__(self): _marker = _Sentinel() -def _check_generic(cls, parameters, elen=_marker): - """Check correct count for parameters of a generic cls (internal helper). - This gives a nice error message in case of count mismatch. - """ - if not elen: - raise TypeError(f"{cls} is not a generic class") - if elen is _marker: - if not hasattr(cls, "__parameters__") or not cls.__parameters__: - raise TypeError(f"{cls} is not a generic class") - elen = len(cls.__parameters__) - alen = len(parameters) - if alen != elen: - if hasattr(cls, "__parameters__"): - parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] - num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) - if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): - return - raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};" - f" actual {alen}, expected {elen}") - - if sys.version_info >= (3, 10): def _should_collect_from_parameters(t): return isinstance( @@ -2379,6 +2358,55 @@ def wrapper(*args, **kwargs): # the runtime doesn't try to substitute the Unpack with the subscripted type. if not hasattr(typing, "TypeVarTuple"): typing._collect_type_vars = _collect_type_vars + def _check_generic(cls, parameters, elen=_marker): + """Check correct count for parameters of a generic cls (internal helper). + This gives a nice error message in case of count mismatch. + """ + if not elen: + raise TypeError(f"{cls} is not a generic class") + if elen is _marker: + if not hasattr(cls, "__parameters__") or not cls.__parameters__: + raise TypeError(f"{cls} is not a generic class") + elen = len(cls.__parameters__) + alen = len(parameters) + if alen != elen: + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) + if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): + return + + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + if all(hasattr(p, '__default__') for p in parameters[alen:]): + return + + raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};" + f" actual {alen}, expected {elen}") + + typing._check_generic = _check_generic +else: + # Python 3.11+ + + def _check_generic(cls, parameters, elen): + """Check correct count for parameters of a generic cls (internal helper). + + This gives a nice error message in case of count mismatch. + """ + if not elen: + raise TypeError(f"{cls} is not a generic class") + alen = len(parameters) + if alen != elen: + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + if all(hasattr(p, '__default__') for p in parameters[alen:]): + return + + raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments for {cls};" + f" actual {alen}, expected {elen}") + typing._check_generic = _check_generic From 8dfebef306dc48d3c05e15cab6cc997977645e7a Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Fri, 27 Oct 2023 15:38:41 +0000 Subject: [PATCH 02/13] TypeVar cannot appear after defaulted TypeVar --- src/typing_extensions.py | 120 +++++++++++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 31 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 759c2535..78a2d7f2 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -158,27 +158,6 @@ def _should_collect_from_parameters(t): return isinstance(t, typing._GenericAlias) and not t._special -def _collect_type_vars(types, typevar_types=None): - """Collect all type variable contained in types in order of - first appearance (lexicographic order). For example:: - - _collect_type_vars((T, List[S, T])) == (T, S) - """ - if typevar_types is None: - typevar_types = typing.TypeVar - tvars = [] - for t in types: - if ( - isinstance(t, typevar_types) and - t not in tvars and - not _is_unpack(t) - ): - tvars.append(t) - if _should_collect_from_parameters(t): - tvars.extend([t for t in t.__parameters__ if t not in tvars]) - return tuple(tvars) - - NoReturn = typing.NoReturn # Some unconstrained type variables. These are used by the container types. @@ -2357,7 +2336,6 @@ def wrapper(*args, **kwargs): # counting generic parameters, so that when we subscript a generic, # the runtime doesn't try to substitute the Unpack with the subscripted type. if not hasattr(typing, "TypeVarTuple"): - typing._collect_type_vars = _collect_type_vars def _check_generic(cls, parameters, elen=_marker): """Check correct count for parameters of a generic cls (internal helper). This gives a nice error message in case of count mismatch. @@ -2376,11 +2354,13 @@ def _check_generic(cls, parameters, elen=_marker): if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): return - # deal with TypeVarLike defaults - # required TypeVarLikes cannot appear after a defaulted one. - if alen < elen: - if all(hasattr(p, '__default__') for p in parameters[alen:]): - return + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars / _collect_parameters + # we can safely check parameters[alen] + if getattr(parameters[alen], '__default__', None) is not None: + return raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};" f" actual {alen}, expected {elen}") @@ -2398,17 +2378,95 @@ def _check_generic(cls, parameters, elen): raise TypeError(f"{cls} is not a generic class") alen = len(parameters) if alen != elen: - # deal with TypeVarLike defaults - # required TypeVarLikes cannot appear after a defaulted one. - if alen < elen: - if all(hasattr(p, '__default__') for p in parameters[alen:]): + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) + if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): return + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars / _collect_parameters + # we can safely check parameters[alen] + if getattr(parameters[alen], '__default__', None) is not None: + return + raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments for {cls};" f" actual {alen}, expected {elen}") typing._check_generic = _check_generic +# Python 3.11+ _collect_type_vars was renamed to _collect_parameters +if hasattr(typing, '_collect_type_vars'): + def _collect_type_vars(types, typevar_types=None): + """Collect all type variable contained in types in order of + first appearance (lexicographic order). For example:: + + _collect_type_vars((T, List[S, T])) == (T, S) + """ + if typevar_types is None: + typevar_types = typing.TypeVar + tvars = [] + # required TypeVarLike cannot appear after TypeVarLike with default + default_encountered = False + for t in types: + if ( + isinstance(t, typevar_types) and + t not in tvars and + not _is_unpack(t) + ): + if getattr(t, '__default__', None) is not None: + if not default_encountered: + default_encountered = True + elif default_encountered: + raise TypeError(f'expected TypeVar with default type, found {t!r}') + + tvars.append(t) + if _should_collect_from_parameters(t): + tvars.extend([t for t in t.__parameters__ if t not in tvars]) + return tuple(tvars) + + typing._collect_type_vars = _collect_type_vars +else: + def _collect_parameters(args): + """Collect all type variables and parameter specifications in args + in order of first appearance (lexicographic order). + + For example:: + + assert _collect_parameters((T, Callable[P, T])) == (T, P) + """ + parameters = [] + # required TypeVarLike cannot appear after TypeVarLike with default + default_encountered = False + for t in args: + if isinstance(t, type): + # We don't want __parameters__ descriptor of a bare Python class. + pass + elif isinstance(t, tuple): + # `t` might be a tuple, when `ParamSpec` is substituted with + # `[T, int]`, or `[int, *Ts]`, etc. + for x in t: + for collected in _collect_parameters([x]): + if collected not in parameters: + parameters.append(collected) + elif hasattr(t, '__typing_subst__'): + if t not in parameters: + if getattr(t, '__default__', None) is not None: + if not default_encountered: + default_encountered = True + elif default_encountered: + raise TypeError(f'expected TypeVar with default type, found {t!r}') + + parameters.append(t) + else: + if _should_collect_from_parameters(t): + parameters.extend([t for t in t.__parameters__ if t not in parameters]) + + return tuple(parameters) + + typing._collect_parameters = _collect_parameters # Backport typing.NamedTuple as it exists in Python 3.13. # In 3.11, the ability to define generic `NamedTuple`s was supported. From 1d29798a42e39ab5055ea208bb06289ff5463010 Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Fri, 27 Oct 2023 16:50:05 +0000 Subject: [PATCH 03/13] add unit tests --- src/test_typing_extensions.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7d8e2553..b318f88c 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5605,6 +5605,23 @@ def test_typevartuple(self): class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] + def test_erroneous_generic(self): + DefaultStrT = TypeVar('DefaultStrT', default=str) + T = TypeVar('T') + + with self.assertRaises(TypeError): + Test = Generic[DefaultStrT, T] + + def test_need_more_params(self): + DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) + T = typing_extensions.TypeVar('T') + U = typing_extensions.TypeVar('U') + + class A(Generic[T, U, DefaultStrT]): ... + + with self.assertRaises(TypeError): + Test = A[int] + def test_pickle(self): global U, U_co, U_contra, U_default # pickle wants to reference the class by name U = typing_extensions.TypeVar('U') From b90fabe09eaded26053d9a3aea1a7d6039223598 Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Fri, 27 Oct 2023 18:23:17 +0000 Subject: [PATCH 04/13] lint fix --- src/typing_extensions.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 78a2d7f2..975f55da 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2357,13 +2357,13 @@ def _check_generic(cls, parameters, elen=_marker): # deal with TypeVarLike defaults # required TypeVarLikes cannot appear after a defaulted one. if alen < elen: - # since we validate TypeVarLike default in _collect_type_vars / _collect_parameters - # we can safely check parameters[alen] + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] if getattr(parameters[alen], '__default__', None) is not None: return - raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};" - f" actual {alen}, expected {elen}") + raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters" + f" for {cls}; actual {alen}, expected {elen}") typing._check_generic = _check_generic else: @@ -2387,13 +2387,13 @@ def _check_generic(cls, parameters, elen): # deal with TypeVarLike defaults # required TypeVarLikes cannot appear after a defaulted one. if alen < elen: - # since we validate TypeVarLike default in _collect_type_vars / _collect_parameters - # we can safely check parameters[alen] + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] if getattr(parameters[alen], '__default__', None) is not None: return - raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments for {cls};" - f" actual {alen}, expected {elen}") + raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters" + f" for {cls}; actual {alen}, expected {elen}") typing._check_generic = _check_generic @@ -2457,12 +2457,14 @@ def _collect_parameters(args): if not default_encountered: default_encountered = True elif default_encountered: - raise TypeError(f'expected TypeVar with default type, found {t!r}') + raise TypeError('expected TypeVar with default type, found' + f' {t!r}') parameters.append(t) else: if _should_collect_from_parameters(t): - parameters.extend([t for t in t.__parameters__ if t not in parameters]) + parameters.extend( + [t for t in t.__parameters__ if t not in parameters]) return tuple(parameters) From 5c5f43035630b80bc78fdba4f0fea5a94ce5ba4e Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Tue, 31 Oct 2023 17:42:04 +0000 Subject: [PATCH 05/13] change according to review --- src/test_typing_extensions.py | 9 +++++---- src/typing_extensions.py | 30 ++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b318f88c..99ef82e1 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3096,6 +3096,9 @@ def __call__(self, *args: Unpack[Ts]) -> T: ... self.assertEqual(MemoizedFunc.__parameters__, (Ts, T, T2)) self.assertTrue(MemoizedFunc._is_protocol) + # 3.11+ moves where this exception is thrown + # 3.11: TypeVarTuple.__typing_prepare_subst__ + # 3.12+: typing._typevartuple_prepare_subst things = "arguments" if sys.version_info >= (3, 11) else "parameters" # A bug was fixed in 3.11.1 @@ -5231,9 +5234,7 @@ class Y(Generic[T], NamedTuple): self.assertIsInstance(a, G) self.assertEqual(a.x, 3) - things = "arguments" if sys.version_info >= (3, 11) else "parameters" - - with self.assertRaisesRegex(TypeError, f'Too many {things}'): + with self.assertRaisesRegex(TypeError, f'Too many parameters'): G[int, str] @skipUnless(TYPING_3_9_0, "tuple.__class_getitem__ was added in 3.9") @@ -5606,7 +5607,7 @@ class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] def test_erroneous_generic(self): - DefaultStrT = TypeVar('DefaultStrT', default=str) + DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) T = TypeVar('T') with self.assertRaises(TypeError): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 975f55da..dadf4872 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2348,6 +2348,7 @@ def _check_generic(cls, parameters, elen=_marker): elen = len(cls.__parameters__) alen = len(parameters) if alen != elen: + expect_val = elen if hasattr(cls, "__parameters__"): parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) @@ -2362,10 +2363,14 @@ def _check_generic(cls, parameters, elen=_marker): if getattr(parameters[alen], '__default__', None) is not None: return - raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters" - f" for {cls}; actual {alen}, expected {elen}") + num_default_tv = sum(getattr(p, '__default__', None) is not None for p in parameters) + + elen -= num_default_tv - typing._check_generic = _check_generic + expect_val = f"at least {elen}" + + raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters" + f" for {cls}; actual {alen}, expected {expect_val}") else: # Python 3.11+ @@ -2378,6 +2383,7 @@ def _check_generic(cls, parameters, elen): raise TypeError(f"{cls} is not a generic class") alen = len(parameters) if alen != elen: + expect_val = elen if hasattr(cls, "__parameters__"): parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) @@ -2392,10 +2398,16 @@ def _check_generic(cls, parameters, elen): if getattr(parameters[alen], '__default__', None) is not None: return + num_default_tv = sum(getattr(p, '__default__', None) is not None for p in parameters) + + elen -= num_default_tv + + expect_val = f"at least {elen}" + raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters" - f" for {cls}; actual {alen}, expected {elen}") + f" for {cls}; actual {alen}, expected {expect_val}") - typing._check_generic = _check_generic +typing._check_generic = _check_generic # Python 3.11+ _collect_type_vars was renamed to _collect_parameters if hasattr(typing, '_collect_type_vars'): @@ -2420,7 +2432,8 @@ def _collect_type_vars(types, typevar_types=None): if not default_encountered: default_encountered = True elif default_encountered: - raise TypeError(f'expected TypeVar with default type, found {t!r}') + raise TypeError(f'type parameter {t!r} without a default' + ' follows type parameter with a default') tvars.append(t) if _should_collect_from_parameters(t): @@ -2457,8 +2470,9 @@ def _collect_parameters(args): if not default_encountered: default_encountered = True elif default_encountered: - raise TypeError('expected TypeVar with default type, found' - f' {t!r}') + raise TypeError(f'type parameter {t!r} without a default' + ' follows type parameter with a default') + parameters.append(t) else: From 93a5726d07284dce1d4078b687bb5d44c03ac13a Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Tue, 31 Oct 2023 17:46:41 +0000 Subject: [PATCH 06/13] lint --- src/typing_extensions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index dadf4872..ba5c92fa 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2363,7 +2363,8 @@ def _check_generic(cls, parameters, elen=_marker): if getattr(parameters[alen], '__default__', None) is not None: return - num_default_tv = sum(getattr(p, '__default__', None) is not None for p in parameters) + num_default_tv = sum(getattr(p, '__default__', None) + is not None for p in parameters) elen -= num_default_tv @@ -2398,7 +2399,8 @@ def _check_generic(cls, parameters, elen): if getattr(parameters[alen], '__default__', None) is not None: return - num_default_tv = sum(getattr(p, '__default__', None) is not None for p in parameters) + num_default_tv = sum(getattr(p, '__default__', None) + is not None for p in parameters) elen -= num_default_tv @@ -2433,7 +2435,7 @@ def _collect_type_vars(types, typevar_types=None): default_encountered = True elif default_encountered: raise TypeError(f'type parameter {t!r} without a default' - ' follows type parameter with a default') + ' follows type parameter with a default') tvars.append(t) if _should_collect_from_parameters(t): @@ -2473,7 +2475,6 @@ def _collect_parameters(args): raise TypeError(f'type parameter {t!r} without a default' ' follows type parameter with a default') - parameters.append(t) else: if _should_collect_from_parameters(t): From ae5f478f2470343eb8efbf6d738daffb4f530f0b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 7 Mar 2024 06:12:43 -0800 Subject: [PATCH 07/13] Update src/test_typing_extensions.py Co-authored-by: James Hilton-Balfe --- 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 2151d50b..d3c98da7 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5714,7 +5714,7 @@ class Y(Generic[T], NamedTuple): self.assertIsInstance(a, G) self.assertEqual(a.x, 3) - with self.assertRaisesRegex(TypeError, f'Too many parameters'): + with self.assertRaisesRegex(TypeError, 'Too many parameters'): G[int, str] @skipUnless(TYPING_3_9_0, "tuple.__class_getitem__ was added in 3.9") From 4c004138aeda8d9f8f6f9932b9ec0499afcda47b Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Thu, 7 Mar 2024 23:35:51 +0000 Subject: [PATCH 08/13] Apply suggestions from code review Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- src/test_typing_extensions.py | 13 +++++++------ src/typing_extensions.py | 15 +++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index d3c98da7..0049d471 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3262,10 +3262,7 @@ def __call__(self, *args: Unpack[Ts]) -> T: ... self.assertEqual(MemoizedFunc.__parameters__, (Ts, T, T2)) self.assertTrue(MemoizedFunc._is_protocol) - # 3.11+ moves where this exception is thrown - # 3.11: TypeVarTuple.__typing_prepare_subst__ - # 3.12+: typing._typevartuple_prepare_subst - things = "arguments" if sys.version_info >= (3, 11) else "parameters" + things = "arguments" # A bug was fixed in 3.11.1 # (https://github.com/python/cpython/commit/74920aa27d0c57443dd7f704d6272cca9c507ab3) @@ -5714,7 +5711,7 @@ class Y(Generic[T], NamedTuple): self.assertIsInstance(a, G) self.assertEqual(a.x, 3) - with self.assertRaisesRegex(TypeError, 'Too many parameters'): + with self.assertRaisesRegex(TypeError, 'Too many arguments'): G[int, str] @skipUnless(TYPING_3_9_0, "tuple.__class_getitem__ was added in 3.9") @@ -6229,8 +6226,12 @@ def test_need_more_params(self): U = typing_extensions.TypeVar('U') class A(Generic[T, U, DefaultStrT]): ... + A[int, bool] + A[int, bool, str] - with self.assertRaises(TypeError): + with self.assertRaises( + TypeError, "Too few arguments for .+; actual 1, expected at least 2" + ): Test = A[int] def test_pickle(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 593b4856..186ad50c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2649,6 +2649,7 @@ def wrapper(*args, **kwargs): if not hasattr(typing, "TypeVarTuple"): def _check_generic(cls, parameters, elen=_marker): """Check correct count for parameters of a generic cls (internal helper). + This gives a nice error message in case of count mismatch. """ if not elen: @@ -2681,7 +2682,7 @@ def _check_generic(cls, parameters, elen=_marker): expect_val = f"at least {elen}" - raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters" + raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments" f" for {cls}; actual {alen}, expected {expect_val}") else: # Python 3.11+ @@ -2717,7 +2718,7 @@ def _check_generic(cls, parameters, elen): expect_val = f"at least {elen}" - raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters" + raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments" f" for {cls}; actual {alen}, expected {expect_val}") typing._check_generic = _check_generic @@ -2742,10 +2743,9 @@ def _collect_type_vars(types, typevar_types=None): not _is_unpack(t) ): if getattr(t, '__default__', None) is not None: - if not default_encountered: - default_encountered = True + default_encountered = True elif default_encountered: - raise TypeError(f'type parameter {t!r} without a default' + raise TypeError(f'Type parameter {t!r} without a default' ' follows type parameter with a default') tvars.append(t) @@ -2780,10 +2780,9 @@ def _collect_parameters(args): elif hasattr(t, '__typing_subst__'): if t not in parameters: if getattr(t, '__default__', None) is not None: - if not default_encountered: - default_encountered = True + default_encountered = True elif default_encountered: - raise TypeError(f'type parameter {t!r} without a default' + raise TypeError(f'Type parameter {t!r} without a default' ' follows type parameter with a default') parameters.append(t) From b1fa05656d4e8b70473359fe0907b09ad7d90783 Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Thu, 7 Mar 2024 23:53:08 +0000 Subject: [PATCH 09/13] fix bug in assertRaises --- 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 0049d471..a352a53e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6230,7 +6230,7 @@ class A(Generic[T, U, DefaultStrT]): ... A[int, bool, str] with self.assertRaises( - TypeError, "Too few arguments for .+; actual 1, expected at least 2" + TypeError, msg="Too few arguments for .+; actual 1, expected at least 2" ): Test = A[int] From a15bbc871e23ff1cf3378e9efcc0a242b8dcac33 Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Fri, 8 Mar 2024 13:59:31 +0000 Subject: [PATCH 10/13] remove `things` variable --- src/test_typing_extensions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a352a53e..6c608a85 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3262,14 +3262,12 @@ def __call__(self, *args: Unpack[Ts]) -> T: ... self.assertEqual(MemoizedFunc.__parameters__, (Ts, T, T2)) self.assertTrue(MemoizedFunc._is_protocol) - things = "arguments" - # A bug was fixed in 3.11.1 # (https://github.com/python/cpython/commit/74920aa27d0c57443dd7f704d6272cca9c507ab3) # That means this assertion doesn't pass on 3.11.0, # but it passes on all other Python versions if sys.version_info[:3] != (3, 11, 0): - with self.assertRaisesRegex(TypeError, f"Too few {things}"): + with self.assertRaisesRegex(TypeError, "Too few arguments"): MemoizedFunc[int] X = MemoizedFunc[int, T, T2] From 9ff1db098ed7a7e36400ed66827ea41a4649b709 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Mar 2024 20:56:42 -0700 Subject: [PATCH 11/13] Fix Python 3.8/9 tests --- src/typing_extensions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 186ad50c..4b155659 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2682,7 +2682,8 @@ def _check_generic(cls, parameters, elen=_marker): expect_val = f"at least {elen}" - raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments" + things = "arguments" if sys.version_info >= (3, 10) else "parameters" + raise TypeError(f"Too {'many' if alen > elen else 'few'} {things}" f" for {cls}; actual {alen}, expected {expect_val}") else: # Python 3.11+ From 8deee2aaaaa31b0c7f02041ea398acbe91d7374f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Mar 2024 20:58:35 -0700 Subject: [PATCH 12/13] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9d3f0f..4ac948a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Fix the runtime behavior of type parameters with defaults (PEP 696). + Patch by Nadir Chowdhury. - Fix minor discrepancy between error messages produced by `typing` and `typing_extensions` on Python 3.10. Patch by Jelle Zijlstra. - When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation. From 86efeae6b92e9154308a2c24bfc9e9cfbea2fab1 Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Tue, 12 Mar 2024 14:29:15 +0000 Subject: [PATCH 13/13] fixes according to review --- src/typing_extensions.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 4b155659..09fcfd87 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2700,9 +2700,6 @@ def _check_generic(cls, parameters, elen): expect_val = elen if hasattr(cls, "__parameters__"): parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] - num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) - if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): - return # deal with TypeVarLike defaults # required TypeVarLikes cannot appear after a defaulted one. @@ -2788,9 +2785,9 @@ def _collect_parameters(args): parameters.append(t) else: - if _should_collect_from_parameters(t): - parameters.extend( - [t for t in t.__parameters__ if t not in parameters]) + for x in getattr(t, '__parameters__', ()): + if x not in parameters: + parameters.append(x) return tuple(parameters)