mirror of
https://github.com/python/cpython.git
synced 2025-12-11 03:20:01 +00:00
gh-137191: Fix how type parameters are collected from Protocol and Generic bases with parameters (#137281)
Some checks are pending
Tests / (push) Blocked by required conditions
Tests / Windows MSI (push) Blocked by required conditions
Tests / Change detection (push) Waiting to run
Tests / Docs (push) Blocked by required conditions
Tests / Check if Autoconf files are up to date (push) Blocked by required conditions
Tests / Check if generated files are up to date (push) Blocked by required conditions
Tests / Ubuntu SSL tests with OpenSSL (push) Blocked by required conditions
Tests / Sanitizers (push) Blocked by required conditions
Tests / Ubuntu SSL tests with AWS-LC (push) Blocked by required conditions
Tests / WASI (push) Blocked by required conditions
Tests / Hypothesis tests on Ubuntu (push) Blocked by required conditions
Tests / Address sanitizer (push) Blocked by required conditions
Tests / Cross build Linux (push) Blocked by required conditions
Tests / CIFuzz (push) Blocked by required conditions
Tests / All required checks pass (push) Blocked by required conditions
Lint / lint (push) Waiting to run
mypy / Run mypy on Lib/_pyrepl (push) Waiting to run
mypy / Run mypy on Lib/test/libregrtest (push) Waiting to run
mypy / Run mypy on Lib/tomllib (push) Waiting to run
mypy / Run mypy on Tools/build (push) Waiting to run
mypy / Run mypy on Tools/cases_generator (push) Waiting to run
mypy / Run mypy on Tools/clinic (push) Waiting to run
mypy / Run mypy on Tools/jit (push) Waiting to run
mypy / Run mypy on Tools/peg_generator (push) Waiting to run
Some checks are pending
Tests / (push) Blocked by required conditions
Tests / Windows MSI (push) Blocked by required conditions
Tests / Change detection (push) Waiting to run
Tests / Docs (push) Blocked by required conditions
Tests / Check if Autoconf files are up to date (push) Blocked by required conditions
Tests / Check if generated files are up to date (push) Blocked by required conditions
Tests / Ubuntu SSL tests with OpenSSL (push) Blocked by required conditions
Tests / Sanitizers (push) Blocked by required conditions
Tests / Ubuntu SSL tests with AWS-LC (push) Blocked by required conditions
Tests / WASI (push) Blocked by required conditions
Tests / Hypothesis tests on Ubuntu (push) Blocked by required conditions
Tests / Address sanitizer (push) Blocked by required conditions
Tests / Cross build Linux (push) Blocked by required conditions
Tests / CIFuzz (push) Blocked by required conditions
Tests / All required checks pass (push) Blocked by required conditions
Lint / lint (push) Waiting to run
mypy / Run mypy on Lib/_pyrepl (push) Waiting to run
mypy / Run mypy on Lib/test/libregrtest (push) Waiting to run
mypy / Run mypy on Lib/tomllib (push) Waiting to run
mypy / Run mypy on Tools/build (push) Waiting to run
mypy / Run mypy on Tools/cases_generator (push) Waiting to run
mypy / Run mypy on Tools/clinic (push) Waiting to run
mypy / Run mypy on Tools/jit (push) Waiting to run
mypy / Run mypy on Tools/peg_generator (push) Waiting to run
This commit is contained in:
parent
801cf3fcdd
commit
158b28dd19
4 changed files with 83 additions and 4 deletions
|
|
@ -477,6 +477,15 @@ typing
|
||||||
or ``TD = TypedDict("TD", {})`` instead.
|
or ``TD = TypedDict("TD", {})`` instead.
|
||||||
(Contributed by Bénédikt Tran in :gh:`133823`.)
|
(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
|
wave
|
||||||
----
|
----
|
||||||
|
|
|
||||||
|
|
@ -3958,6 +3958,7 @@ class ProtocolTests(BaseTestCase):
|
||||||
|
|
||||||
def test_defining_generic_protocols(self):
|
def test_defining_generic_protocols(self):
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
T2 = TypeVar('T2')
|
||||||
S = TypeVar('S')
|
S = TypeVar('S')
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
|
|
@ -3967,17 +3968,26 @@ class ProtocolTests(BaseTestCase):
|
||||||
class P(PR[int, T], Protocol[T]):
|
class P(PR[int, T], Protocol[T]):
|
||||||
y = 1
|
y = 1
|
||||||
|
|
||||||
|
self.assertEqual(P.__parameters__, (T,))
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
PR[int]
|
PR[int]
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
P[int, str]
|
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
|
class C(PR[int, T]): pass
|
||||||
|
|
||||||
|
self.assertEqual(C.__parameters__, (T,))
|
||||||
self.assertIsInstance(C[str](), C)
|
self.assertIsInstance(C[str](), C)
|
||||||
|
|
||||||
def test_defining_generic_protocols_old_style(self):
|
def test_defining_generic_protocols_old_style(self):
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
T2 = TypeVar('T2')
|
||||||
S = TypeVar('S')
|
S = TypeVar('S')
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
|
|
@ -3996,9 +4006,19 @@ class ProtocolTests(BaseTestCase):
|
||||||
class P1(Protocol, Generic[T]):
|
class P1(Protocol, Generic[T]):
|
||||||
def bar(self, x: T) -> str: ...
|
def bar(self, x: T) -> str: ...
|
||||||
|
|
||||||
|
self.assertEqual(P1.__parameters__, (T,))
|
||||||
|
|
||||||
class P2(Generic[T], Protocol):
|
class P2(Generic[T], Protocol):
|
||||||
def bar(self, x: T) -> str: ...
|
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
|
@runtime_checkable
|
||||||
class PSub(P1[str], Protocol):
|
class PSub(P1[str], Protocol):
|
||||||
x = 1
|
x = 1
|
||||||
|
|
@ -4011,6 +4031,28 @@ class ProtocolTests(BaseTestCase):
|
||||||
|
|
||||||
self.assertIsInstance(Test(), PSub)
|
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):
|
def test_pep695_generic_protocol_callable_members(self):
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class Foo[T](Protocol):
|
class Foo[T](Protocol):
|
||||||
|
|
|
||||||
|
|
@ -256,16 +256,27 @@ def _type_repr(obj):
|
||||||
return _lazy_annotationlib.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
|
"""Collect all type parameters in args
|
||||||
in order of first appearance (lexicographic order).
|
in order of first appearance (lexicographic order).
|
||||||
|
|
||||||
|
Having an explicit `Generic` or `Protocol` base class determines
|
||||||
|
the exact parameter order.
|
||||||
|
|
||||||
For example::
|
For example::
|
||||||
|
|
||||||
>>> P = ParamSpec('P')
|
>>> P = ParamSpec('P')
|
||||||
>>> T = TypeVar('T')
|
>>> T = TypeVar('T')
|
||||||
>>> _collect_type_parameters((T, Callable[P, T]))
|
>>> _collect_type_parameters((T, Callable[P, T]))
|
||||||
(~T, ~P)
|
(~T, ~P)
|
||||||
|
>>> _collect_type_parameters((list[T], Generic[P, T]))
|
||||||
|
(~P, ~T)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# required type parameter cannot appear after parameter with default
|
# required type parameter cannot appear after parameter with default
|
||||||
default_encountered = False
|
default_encountered = False
|
||||||
|
|
@ -297,6 +308,17 @@ def _collect_type_parameters(args, *, enforce_default_ordering: bool = True):
|
||||||
' follows type parameter with a default')
|
' follows type parameter with a default')
|
||||||
|
|
||||||
parameters.append(t)
|
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:
|
else:
|
||||||
if _is_unpacked_typevartuple(t):
|
if _is_unpacked_typevartuple(t):
|
||||||
type_var_tuple_encountered = True
|
type_var_tuple_encountered = True
|
||||||
|
|
@ -1156,20 +1178,22 @@ def _generic_init_subclass(cls, *args, **kwargs):
|
||||||
if error:
|
if error:
|
||||||
raise TypeError("Cannot inherit from plain Generic")
|
raise TypeError("Cannot inherit from plain Generic")
|
||||||
if '__orig_bases__' in cls.__dict__:
|
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].
|
# Look for Generic[T1, ..., Tn].
|
||||||
# If found, tvars must be a subset of it.
|
# If found, tvars must be a subset of it.
|
||||||
# If not found, tvars is it.
|
# If not found, tvars is it.
|
||||||
# Also check for and reject plain Generic,
|
# Also check for and reject plain Generic,
|
||||||
# and reject multiple Generic[...].
|
# and reject multiple Generic[...].
|
||||||
gvars = None
|
gvars = None
|
||||||
|
basename = None
|
||||||
for base in cls.__orig_bases__:
|
for base in cls.__orig_bases__:
|
||||||
if (isinstance(base, _GenericAlias) and
|
if (isinstance(base, _GenericAlias) and
|
||||||
base.__origin__ is Generic):
|
base.__origin__ in (Generic, Protocol)):
|
||||||
if gvars is not None:
|
if gvars is not None:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"Cannot inherit from Generic[...] multiple times.")
|
"Cannot inherit from Generic[...] multiple times.")
|
||||||
gvars = base.__parameters__
|
gvars = base.__parameters__
|
||||||
|
basename = base.__origin__.__name__
|
||||||
if gvars is not None:
|
if gvars is not None:
|
||||||
tvarset = set(tvars)
|
tvarset = set(tvars)
|
||||||
gvarset = set(gvars)
|
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_vars = ', '.join(str(t) for t in tvars if t not in gvarset)
|
||||||
s_args = ', '.join(str(g) for g in gvars)
|
s_args = ', '.join(str(g) for g in gvars)
|
||||||
raise TypeError(f"Some type variables ({s_vars}) are"
|
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
|
tvars = gvars
|
||||||
cls.__parameters__ = tuple(tvars)
|
cls.__parameters__ = tuple(tvars)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue