diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 6c00a0fcb83fee..90c5da788f30ce 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -477,6 +477,15 @@ typing or ``TD = TypedDict("TD", {})`` instead. (Contributed by Bénédikt Tran in :gh:`133823`.) +* Code like ``class ExtraTypeVars(P1[S], Protocol[T, T2]): ...`` now raises + a :exc:`TypeError`, because ``S`` is not listed in ``Protocol`` parameters. + (Contributed by Nikita Sobolev in :gh:`137191`.) + +* Code like ``class B2(A[T2], Protocol[T1, T2]): ...`` now correctly handles + type parameters order: it is ``(T1, T2)``, not ``(T2, T1)`` + as it was incorrectly infered in runtime before. + (Contributed by Nikita Sobolev in :gh:`137191`.) + wave ---- diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index b1615bbff383c2..6317d4657619f0 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -3958,6 +3958,7 @@ class C: pass def test_defining_generic_protocols(self): T = TypeVar('T') + T2 = TypeVar('T2') S = TypeVar('S') @runtime_checkable @@ -3967,17 +3968,26 @@ def meth(self): pass class P(PR[int, T], Protocol[T]): y = 1 + self.assertEqual(P.__parameters__, (T,)) + with self.assertRaises(TypeError): PR[int] with self.assertRaises(TypeError): P[int, str] + with self.assertRaisesRegex( + TypeError, + re.escape('Some type variables (~S) are not listed in Protocol[~T, ~T2]'), + ): + class ExtraTypeVars(P[S], Protocol[T, T2]): ... class C(PR[int, T]): pass + self.assertEqual(C.__parameters__, (T,)) self.assertIsInstance(C[str](), C) def test_defining_generic_protocols_old_style(self): T = TypeVar('T') + T2 = TypeVar('T2') S = TypeVar('S') @runtime_checkable @@ -3996,9 +4006,19 @@ class P(PR[int, str], Protocol): class P1(Protocol, Generic[T]): def bar(self, x: T) -> str: ... + self.assertEqual(P1.__parameters__, (T,)) + class P2(Generic[T], Protocol): def bar(self, x: T) -> str: ... + self.assertEqual(P2.__parameters__, (T,)) + + msg = re.escape('Some type variables (~S) are not listed in Protocol[~T, ~T2]') + with self.assertRaisesRegex(TypeError, msg): + class ExtraTypeVars(P1[S], Protocol[T, T2]): ... + with self.assertRaisesRegex(TypeError, msg): + class ExtraTypeVars(P2[S], Protocol[T, T2]): ... + @runtime_checkable class PSub(P1[str], Protocol): x = 1 @@ -4011,6 +4031,28 @@ def bar(self, x: str) -> str: self.assertIsInstance(Test(), PSub) + def test_protocol_parameter_order(self): + # https://github.com/python/cpython/issues/137191 + T1 = TypeVar("T1") + T2 = TypeVar("T2", default=object) + + class A(Protocol[T1]): ... + + class B0(A[T2], Generic[T1, T2]): ... + self.assertEqual(B0.__parameters__, (T1, T2)) + + class B1(A[T2], Protocol, Generic[T1, T2]): ... + self.assertEqual(B1.__parameters__, (T1, T2)) + + class B2(A[T2], Protocol[T1, T2]): ... + self.assertEqual(B2.__parameters__, (T1, T2)) + + class B3[T1, T2](A[T2], Protocol): + @staticmethod + def get_typeparams(): + return (T1, T2) + self.assertEqual(B3.__parameters__, B3.get_typeparams()) + def test_pep695_generic_protocol_callable_members(self): @runtime_checkable class Foo[T](Protocol): diff --git a/Lib/typing.py b/Lib/typing.py index f1455c273d31ca..036636f7e0e6a8 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -256,16 +256,27 @@ def _type_repr(obj): return _lazy_annotationlib.type_repr(obj) -def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): +def _collect_type_parameters( + args, + *, + enforce_default_ordering: bool = True, + validate_all: bool = False, +): """Collect all type parameters in args in order of first appearance (lexicographic order). + Having an explicit `Generic` or `Protocol` base class determines + the exact parameter order. + For example:: >>> P = ParamSpec('P') >>> T = TypeVar('T') >>> _collect_type_parameters((T, Callable[P, T])) (~T, ~P) + >>> _collect_type_parameters((list[T], Generic[P, T])) + (~P, ~T) + """ # required type parameter cannot appear after parameter with default default_encountered = False @@ -297,6 +308,17 @@ def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): ' follows type parameter with a default') parameters.append(t) + elif ( + not validate_all + and isinstance(t, _GenericAlias) + and t.__origin__ in (Generic, Protocol) + ): + # If we see explicit `Generic[...]` or `Protocol[...]` base classes, + # we need to just copy them as-is. + # Unless `validate_all` is passed, in this case it means that + # we are doing a validation of `Generic` subclasses, + # then we collect all unique parameters to be able to inspect them. + parameters = t.__parameters__ else: if _is_unpacked_typevartuple(t): type_var_tuple_encountered = True @@ -1156,20 +1178,22 @@ def _generic_init_subclass(cls, *args, **kwargs): if error: raise TypeError("Cannot inherit from plain Generic") if '__orig_bases__' in cls.__dict__: - tvars = _collect_type_parameters(cls.__orig_bases__) + tvars = _collect_type_parameters(cls.__orig_bases__, validate_all=True) # Look for Generic[T1, ..., Tn]. # If found, tvars must be a subset of it. # If not found, tvars is it. # Also check for and reject plain Generic, # and reject multiple Generic[...]. gvars = None + basename = None for base in cls.__orig_bases__: if (isinstance(base, _GenericAlias) and - base.__origin__ is Generic): + base.__origin__ in (Generic, Protocol)): if gvars is not None: raise TypeError( "Cannot inherit from Generic[...] multiple times.") gvars = base.__parameters__ + basename = base.__origin__.__name__ if gvars is not None: tvarset = set(tvars) gvarset = set(gvars) @@ -1177,7 +1201,7 @@ def _generic_init_subclass(cls, *args, **kwargs): s_vars = ', '.join(str(t) for t in tvars if t not in gvarset) s_args = ', '.join(str(g) for g in gvars) raise TypeError(f"Some type variables ({s_vars}) are" - f" not listed in Generic[{s_args}]") + f" not listed in {basename}[{s_args}]") tvars = gvars cls.__parameters__ = tuple(tvars) diff --git a/Misc/NEWS.d/next/Library/2025-07-31-16-43-16.gh-issue-137191.FIogE8.rst b/Misc/NEWS.d/next/Library/2025-07-31-16-43-16.gh-issue-137191.FIogE8.rst new file mode 100644 index 00000000000000..b2dba81251eed6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-31-16-43-16.gh-issue-137191.FIogE8.rst @@ -0,0 +1,4 @@ +Fix how type parameters are collected, when :class:`typing.Protocol` are +specified with explicit parameters. Now, :class:`typing.Generic` and +:class:`typing.Protocol` always dictate the parameter number +and parameter ordering of types. Previous behavior was a bug.