https://github.com/python/cpython/commit/158b28dd1906c5d3fac7955f87ba808f1e89fdad
commit: 158b28dd1906c5d3fac7955f87ba808f1e89fdad
branch: main
author: sobolevn <[email protected]>
committer: sobolevn <[email protected]>
date: 2025-08-03T10:40:55+03:00
summary:
gh-137191: Fix how type parameters are collected from `Protocol` and `Generic`
bases with parameters (#137281)
files:
A Misc/NEWS.d/next/Library/2025-07-31-16-43-16.gh-issue-137191.FIogE8.rst
M Doc/whatsnew/3.15.rst
M Lib/test/test_typing.py
M Lib/typing.py
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 1e54a61a449adc..010f6ce7f50e1e 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.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]