Skip to content

Commit c9ae7ef

Browse files
authored
Merge branch 'main' into gh-137288
2 parents ea4f664 + 158b28d commit c9ae7ef

File tree

13 files changed

+346
-57
lines changed

13 files changed

+346
-57
lines changed

Doc/library/tulip_coro.dia

-4.35 KB
Binary file not shown.

Doc/library/tulip_coro.png

-35.9 KB
Binary file not shown.

Doc/whatsnew/3.15.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,15 @@ typing
477477
or ``TD = TypedDict("TD", {})`` instead.
478478
(Contributed by Bénédikt Tran in :gh:`133823`.)
479479

480+
* Code like ``class ExtraTypeVars(P1[S], Protocol[T, T2]): ...`` now raises
481+
a :exc:`TypeError`, because ``S`` is not listed in ``Protocol`` parameters.
482+
(Contributed by Nikita Sobolev in :gh:`137191`.)
483+
484+
* Code like ``class B2(A[T2], Protocol[T1, T2]): ...`` now correctly handles
485+
type parameters order: it is ``(T1, T2)``, not ``(T2, T1)``
486+
as it was incorrectly infered in runtime before.
487+
(Contributed by Nikita Sobolev in :gh:`137191`.)
488+
480489

481490
wave
482491
----

Include/internal/pycore_opcode_metadata.h

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_uop_metadata.h

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_ast/test_ast.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,131 @@ def test_negative_locations_for_compile(self):
220220
# This also must not crash:
221221
ast.parse(tree, optimize=2)
222222

223+
def test_docstring_optimization_single_node(self):
224+
# https://github.com/python/cpython/issues/137308
225+
class_example1 = textwrap.dedent('''
226+
class A:
227+
"""Docstring"""
228+
''')
229+
class_example2 = textwrap.dedent('''
230+
class A:
231+
"""
232+
Docstring"""
233+
''')
234+
def_example1 = textwrap.dedent('''
235+
def some():
236+
"""Docstring"""
237+
''')
238+
def_example2 = textwrap.dedent('''
239+
def some():
240+
"""Docstring
241+
"""
242+
''')
243+
async_def_example1 = textwrap.dedent('''
244+
async def some():
245+
"""Docstring"""
246+
''')
247+
async_def_example2 = textwrap.dedent('''
248+
async def some():
249+
"""
250+
Docstring
251+
"""
252+
''')
253+
for code in [
254+
class_example1,
255+
class_example2,
256+
def_example1,
257+
def_example2,
258+
async_def_example1,
259+
async_def_example2,
260+
]:
261+
for opt_level in [0, 1, 2]:
262+
with self.subTest(code=code, opt_level=opt_level):
263+
mod = ast.parse(code, optimize=opt_level)
264+
self.assertEqual(len(mod.body[0].body), 1)
265+
if opt_level == 2:
266+
pass_stmt = mod.body[0].body[0]
267+
self.assertIsInstance(pass_stmt, ast.Pass)
268+
self.assertEqual(
269+
vars(pass_stmt),
270+
{
271+
'lineno': 3,
272+
'col_offset': 4,
273+
'end_lineno': 3,
274+
'end_col_offset': 8,
275+
},
276+
)
277+
else:
278+
self.assertIsInstance(mod.body[0].body[0], ast.Expr)
279+
self.assertIsInstance(
280+
mod.body[0].body[0].value,
281+
ast.Constant,
282+
)
283+
284+
compile(code, "a", "exec")
285+
compile(code, "a", "exec", optimize=opt_level)
286+
compile(mod, "a", "exec")
287+
compile(mod, "a", "exec", optimize=opt_level)
288+
289+
def test_docstring_optimization_multiple_nodes(self):
290+
# https://github.com/python/cpython/issues/137308
291+
class_example = textwrap.dedent(
292+
"""
293+
class A:
294+
'''
295+
Docstring
296+
'''
297+
x = 1
298+
"""
299+
)
300+
301+
def_example = textwrap.dedent(
302+
"""
303+
def some():
304+
'''
305+
Docstring
306+
307+
'''
308+
x = 1
309+
"""
310+
)
311+
312+
async_def_example = textwrap.dedent(
313+
"""
314+
async def some():
315+
316+
'''Docstring
317+
318+
'''
319+
x = 1
320+
"""
321+
)
322+
323+
for code in [
324+
class_example,
325+
def_example,
326+
async_def_example,
327+
]:
328+
for opt_level in [0, 1, 2]:
329+
with self.subTest(code=code, opt_level=opt_level):
330+
mod = ast.parse(code, optimize=opt_level)
331+
if opt_level == 2:
332+
self.assertNotIsInstance(
333+
mod.body[0].body[0],
334+
(ast.Pass, ast.Expr),
335+
)
336+
else:
337+
self.assertIsInstance(mod.body[0].body[0], ast.Expr)
338+
self.assertIsInstance(
339+
mod.body[0].body[0].value,
340+
ast.Constant,
341+
)
342+
343+
compile(code, "a", "exec")
344+
compile(code, "a", "exec", optimize=opt_level)
345+
compile(mod, "a", "exec")
346+
compile(mod, "a", "exec", optimize=opt_level)
347+
223348
def test_slice(self):
224349
slc = ast.parse("x[::]").body[0].value.slice
225350
self.assertIsNone(slc.upper)

Lib/test/test_typing.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3958,6 +3958,7 @@ class C: pass
39583958

39593959
def test_defining_generic_protocols(self):
39603960
T = TypeVar('T')
3961+
T2 = TypeVar('T2')
39613962
S = TypeVar('S')
39623963

39633964
@runtime_checkable
@@ -3967,17 +3968,26 @@ def meth(self): pass
39673968
class P(PR[int, T], Protocol[T]):
39683969
y = 1
39693970

3971+
self.assertEqual(P.__parameters__, (T,))
3972+
39703973
with self.assertRaises(TypeError):
39713974
PR[int]
39723975
with self.assertRaises(TypeError):
39733976
P[int, str]
3977+
with self.assertRaisesRegex(
3978+
TypeError,
3979+
re.escape('Some type variables (~S) are not listed in Protocol[~T, ~T2]'),
3980+
):
3981+
class ExtraTypeVars(P[S], Protocol[T, T2]): ...
39743982

39753983
class C(PR[int, T]): pass
39763984

3985+
self.assertEqual(C.__parameters__, (T,))
39773986
self.assertIsInstance(C[str](), C)
39783987

39793988
def test_defining_generic_protocols_old_style(self):
39803989
T = TypeVar('T')
3990+
T2 = TypeVar('T2')
39813991
S = TypeVar('S')
39823992

39833993
@runtime_checkable
@@ -3996,9 +4006,19 @@ class P(PR[int, str], Protocol):
39964006
class P1(Protocol, Generic[T]):
39974007
def bar(self, x: T) -> str: ...
39984008

4009+
self.assertEqual(P1.__parameters__, (T,))
4010+
39994011
class P2(Generic[T], Protocol):
40004012
def bar(self, x: T) -> str: ...
40014013

4014+
self.assertEqual(P2.__parameters__, (T,))
4015+
4016+
msg = re.escape('Some type variables (~S) are not listed in Protocol[~T, ~T2]')
4017+
with self.assertRaisesRegex(TypeError, msg):
4018+
class ExtraTypeVars(P1[S], Protocol[T, T2]): ...
4019+
with self.assertRaisesRegex(TypeError, msg):
4020+
class ExtraTypeVars(P2[S], Protocol[T, T2]): ...
4021+
40024022
@runtime_checkable
40034023
class PSub(P1[str], Protocol):
40044024
x = 1
@@ -4011,6 +4031,28 @@ def bar(self, x: str) -> str:
40114031

40124032
self.assertIsInstance(Test(), PSub)
40134033

4034+
def test_protocol_parameter_order(self):
4035+
# https://github.com/python/cpython/issues/137191
4036+
T1 = TypeVar("T1")
4037+
T2 = TypeVar("T2", default=object)
4038+
4039+
class A(Protocol[T1]): ...
4040+
4041+
class B0(A[T2], Generic[T1, T2]): ...
4042+
self.assertEqual(B0.__parameters__, (T1, T2))
4043+
4044+
class B1(A[T2], Protocol, Generic[T1, T2]): ...
4045+
self.assertEqual(B1.__parameters__, (T1, T2))
4046+
4047+
class B2(A[T2], Protocol[T1, T2]): ...
4048+
self.assertEqual(B2.__parameters__, (T1, T2))
4049+
4050+
class B3[T1, T2](A[T2], Protocol):
4051+
@staticmethod
4052+
def get_typeparams():
4053+
return (T1, T2)
4054+
self.assertEqual(B3.__parameters__, B3.get_typeparams())
4055+
40144056
def test_pep695_generic_protocol_callable_members(self):
40154057
@runtime_checkable
40164058
class Foo[T](Protocol):

Lib/typing.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,16 +256,27 @@ def _type_repr(obj):
256256
return _lazy_annotationlib.type_repr(obj)
257257

258258

259-
def _collect_type_parameters(args, *, enforce_default_ordering: bool = True):
259+
def _collect_type_parameters(
260+
args,
261+
*,
262+
enforce_default_ordering: bool = True,
263+
validate_all: bool = False,
264+
):
260265
"""Collect all type parameters in args
261266
in order of first appearance (lexicographic order).
262267
268+
Having an explicit `Generic` or `Protocol` base class determines
269+
the exact parameter order.
270+
263271
For example::
264272
265273
>>> P = ParamSpec('P')
266274
>>> T = TypeVar('T')
267275
>>> _collect_type_parameters((T, Callable[P, T]))
268276
(~T, ~P)
277+
>>> _collect_type_parameters((list[T], Generic[P, T]))
278+
(~P, ~T)
279+
269280
"""
270281
# required type parameter cannot appear after parameter with default
271282
default_encountered = False
@@ -297,6 +308,17 @@ def _collect_type_parameters(args, *, enforce_default_ordering: bool = True):
297308
' follows type parameter with a default')
298309

299310
parameters.append(t)
311+
elif (
312+
not validate_all
313+
and isinstance(t, _GenericAlias)
314+
and t.__origin__ in (Generic, Protocol)
315+
):
316+
# If we see explicit `Generic[...]` or `Protocol[...]` base classes,
317+
# we need to just copy them as-is.
318+
# Unless `validate_all` is passed, in this case it means that
319+
# we are doing a validation of `Generic` subclasses,
320+
# then we collect all unique parameters to be able to inspect them.
321+
parameters = t.__parameters__
300322
else:
301323
if _is_unpacked_typevartuple(t):
302324
type_var_tuple_encountered = True
@@ -1156,28 +1178,30 @@ def _generic_init_subclass(cls, *args, **kwargs):
11561178
if error:
11571179
raise TypeError("Cannot inherit from plain Generic")
11581180
if '__orig_bases__' in cls.__dict__:
1159-
tvars = _collect_type_parameters(cls.__orig_bases__)
1181+
tvars = _collect_type_parameters(cls.__orig_bases__, validate_all=True)
11601182
# Look for Generic[T1, ..., Tn].
11611183
# If found, tvars must be a subset of it.
11621184
# If not found, tvars is it.
11631185
# Also check for and reject plain Generic,
11641186
# and reject multiple Generic[...].
11651187
gvars = None
1188+
basename = None
11661189
for base in cls.__orig_bases__:
11671190
if (isinstance(base, _GenericAlias) and
1168-
base.__origin__ is Generic):
1191+
base.__origin__ in (Generic, Protocol)):
11691192
if gvars is not None:
11701193
raise TypeError(
11711194
"Cannot inherit from Generic[...] multiple times.")
11721195
gvars = base.__parameters__
1196+
basename = base.__origin__.__name__
11731197
if gvars is not None:
11741198
tvarset = set(tvars)
11751199
gvarset = set(gvars)
11761200
if not tvarset <= gvarset:
11771201
s_vars = ', '.join(str(t) for t in tvars if t not in gvarset)
11781202
s_args = ', '.join(str(g) for g in gvars)
11791203
raise TypeError(f"Some type variables ({s_vars}) are"
1180-
f" not listed in Generic[{s_args}]")
1204+
f" not listed in {basename}[{s_args}]")
11811205
tvars = gvars
11821206
cls.__parameters__ = tuple(tvars)
11831207

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
A standalone docstring in a node body is optimized as a :keyword:`pass`
2+
statement to ensure that the node's body is never empty. There was a
3+
:exc:`ValueError` in :func:`compile` otherwise.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix how type parameters are collected, when :class:`typing.Protocol` are
2+
specified with explicit parameters. Now, :class:`typing.Generic` and
3+
:class:`typing.Protocol` always dictate the parameter number
4+
and parameter ordering of types. Previous behavior was a bug.

0 commit comments

Comments
 (0)