Skip to content

Commit e57a8dd

Browse files
authored
Merge branch 'main' into gh-134869
2 parents 3f81355 + 2bd3895 commit e57a8dd

18 files changed

Lines changed: 1871 additions & 680 deletions

Doc/reference/expressions.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,9 @@ brackets or curly braces.
406406
Variables used in the generator expression are evaluated lazily when the
407407
:meth:`~generator.__next__` method is called for the generator object (in the same
408408
fashion as normal generators). However, the iterable expression in the
409-
leftmost :keyword:`!for` clause is immediately evaluated, so that an error
410-
produced by it will be emitted at the point where the generator expression
409+
leftmost :keyword:`!for` clause is immediately evaluated, and the
410+
:term:`iterator` is immediately created for that iterable, so that an error
411+
produced while creating the iterator will be emitted at the point where the generator expression
411412
is defined, rather than at the point where the first value is retrieved.
412413
Subsequent :keyword:`!for` clauses and any filter condition in the leftmost
413414
:keyword:`!for` clause cannot be evaluated in the enclosing scope as they may

Doc/using/configure.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,14 @@ Options for third-party dependencies
445445
C compiler and linker flags for ``libuuid``, used by :mod:`uuid` module,
446446
overriding ``pkg-config``.
447447

448+
.. option:: LIBZSTD_CFLAGS
449+
.. option:: LIBZSTD_LIBS
450+
451+
C compiler and linker flags for ``libzstd``, used by :mod:`compression.zstd` module,
452+
overriding ``pkg-config``.
453+
454+
.. versionadded:: 3.14
455+
448456
.. option:: PANEL_CFLAGS
449457
.. option:: PANEL_LIBS
450458

Doc/whatsnew/3.14.rst

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -816,43 +816,58 @@ Executing the new tool on the running process will yield a table like this:
816816
817817
python -m asyncio ps 12345
818818
819-
tid task id task name coroutine chain awaiter name awaiter id
820-
---------------------------------------------------------------------------------------------------------------------------------------
821-
8138752 0x564bd3d0210 Task-1 0x0
822-
8138752 0x564bd3d0410 Sundowning _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
823-
8138752 0x564bd3d0610 TMBTE _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
824-
8138752 0x564bd3d0810 TNDNBTG _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
825-
8138752 0x564bd3d0a10 Levitate _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
826-
8138752 0x564bd3e0550 DYWTYLM _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
827-
8138752 0x564bd3e0710 Aqua Regia _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
828-
829-
830-
or:
819+
tid task id task name coroutine stack awaiter chain awaiter name awaiter id
820+
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
821+
1935500 0x7fc930c18050 Task-1 TaskGroup._aexit -> TaskGroup.__aexit__ -> main 0x0
822+
1935500 0x7fc930c18230 Sundowning TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x7fc930c18050
823+
1935500 0x7fc93173fa50 TMBTE TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x7fc930c18050
824+
1935500 0x7fc93173fdf0 TNDNBTG sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x7fc930c18230
825+
1935500 0x7fc930d32510 Levitate sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x7fc930c18230
826+
1935500 0x7fc930d32890 DYWTYLM sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x7fc93173fa50
827+
1935500 0x7fc93161ec30 Aqua Regia sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x7fc93173fa50
828+
829+
or a tree like this:
831830

832831
.. code-block:: bash
833832
834833
python -m asyncio pstree 12345
835834
836835
└── (T) Task-1
837-
└── main
838-
└── __aexit__
839-
└── _aexit
836+
└── main example.py:13
837+
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
838+
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
840839
├── (T) Sundowning
841-
│ └── album
842-
│ └── __aexit__
843-
│ └── _aexit
840+
│ └── album example.py:8
841+
│ └── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
842+
│ └── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
844843
│ ├── (T) TNDNBTG
844+
│ │ └── play example.py:4
845+
│ │ └── sleep Lib/asyncio/tasks.py:702
845846
│ └── (T) Levitate
847+
│ └── play example.py:4
848+
│ └── sleep Lib/asyncio/tasks.py:702
846849
└── (T) TMBTE
847-
└── album
848-
└── __aexit__
849-
└── _aexit
850+
└── album example.py:8
851+
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
852+
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
850853
├── (T) DYWTYLM
854+
│ └── play example.py:4
855+
│ └── sleep Lib/asyncio/tasks.py:702
851856
└── (T) Aqua Regia
857+
└── play example.py:4
858+
└── sleep Lib/asyncio/tasks.py:702
852859
853860
If a cycle is detected in the async await graph (which could indicate a
854861
programming issue), the tool raises an error and lists the cycle paths that
855-
prevent tree construction.
862+
prevent tree construction:
863+
864+
.. code-block:: bash
865+
866+
python -m asyncio pstree 12345
867+
868+
ERROR: await-graph contains cycles - cannot print a tree!
869+
870+
cycle: Task-2 → Task-3 → Task-2
856871
857872
(Contributed by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
858873
Gomez Macias in :gh:`91048`.)

Lib/asyncio/tools.py

Lines changed: 95 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
"""Tools to analyze tasks running in asyncio programs."""
22

3-
from collections import defaultdict
3+
from collections import defaultdict, namedtuple
44
from itertools import count
55
from enum import Enum
66
import sys
7-
from _remote_debugging import RemoteUnwinder
8-
7+
from _remote_debugging import RemoteUnwinder, FrameInfo
98

109
class NodeType(Enum):
1110
COROUTINE = 1
@@ -26,51 +25,75 @@ def __init__(
2625

2726

2827
# ─── indexing helpers ───────────────────────────────────────────
29-
def _format_stack_entry(elem: tuple[str, str, int] | str) -> str:
30-
if isinstance(elem, tuple):
31-
fqname, path, line_no = elem
32-
return f"{fqname} {path}:{line_no}"
33-
28+
def _format_stack_entry(elem: str|FrameInfo) -> str:
29+
if not isinstance(elem, str):
30+
if elem.lineno == 0 and elem.filename == "":
31+
return f"{elem.funcname}"
32+
else:
33+
return f"{elem.funcname} {elem.filename}:{elem.lineno}"
3434
return elem
3535

3636

3737
def _index(result):
38-
id2name, awaits = {}, []
39-
for _thr_id, tasks in result:
40-
for tid, tname, awaited in tasks:
41-
id2name[tid] = tname
42-
for stack, parent_id in awaited:
43-
stack = [_format_stack_entry(elem) for elem in stack]
44-
awaits.append((parent_id, stack, tid))
45-
return id2name, awaits
46-
47-
48-
def _build_tree(id2name, awaits):
38+
id2name, awaits, task_stacks = {}, [], {}
39+
for awaited_info in result:
40+
for task_info in awaited_info.awaited_by:
41+
task_id = task_info.task_id
42+
task_name = task_info.task_name
43+
id2name[task_id] = task_name
44+
45+
# Store the internal coroutine stack for this task
46+
if task_info.coroutine_stack:
47+
for coro_info in task_info.coroutine_stack:
48+
call_stack = coro_info.call_stack
49+
internal_stack = [_format_stack_entry(frame) for frame in call_stack]
50+
task_stacks[task_id] = internal_stack
51+
52+
# Add the awaited_by relationships (external dependencies)
53+
if task_info.awaited_by:
54+
for coro_info in task_info.awaited_by:
55+
call_stack = coro_info.call_stack
56+
parent_task_id = coro_info.task_name
57+
stack = [_format_stack_entry(frame) for frame in call_stack]
58+
awaits.append((parent_task_id, stack, task_id))
59+
return id2name, awaits, task_stacks
60+
61+
62+
def _build_tree(id2name, awaits, task_stacks):
4963
id2label = {(NodeType.TASK, tid): name for tid, name in id2name.items()}
5064
children = defaultdict(list)
51-
cor_names = defaultdict(dict) # (parent) -> {frame: node}
52-
cor_id_seq = count(1)
53-
54-
def _cor_node(parent_key, frame_name):
55-
"""Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*."""
56-
bucket = cor_names[parent_key]
57-
if frame_name in bucket:
58-
return bucket[frame_name]
59-
node_key = (NodeType.COROUTINE, f"c{next(cor_id_seq)}")
60-
id2label[node_key] = frame_name
61-
children[parent_key].append(node_key)
62-
bucket[frame_name] = node_key
65+
cor_nodes = defaultdict(dict) # Maps parent -> {frame_name: node_key}
66+
next_cor_id = count(1)
67+
68+
def get_or_create_cor_node(parent, frame):
69+
"""Get existing coroutine node or create new one under parent"""
70+
if frame in cor_nodes[parent]:
71+
return cor_nodes[parent][frame]
72+
73+
node_key = (NodeType.COROUTINE, f"c{next(next_cor_id)}")
74+
id2label[node_key] = frame
75+
children[parent].append(node_key)
76+
cor_nodes[parent][frame] = node_key
6377
return node_key
6478

65-
# lay down parent ➜ …frames… ➜ child paths
79+
# Build task dependency tree with coroutine frames
6680
for parent_id, stack, child_id in awaits:
6781
cur = (NodeType.TASK, parent_id)
68-
for frame in reversed(stack): # outer-most → inner-most
69-
cur = _cor_node(cur, frame)
82+
for frame in reversed(stack):
83+
cur = get_or_create_cor_node(cur, frame)
84+
7085
child_key = (NodeType.TASK, child_id)
7186
if child_key not in children[cur]:
7287
children[cur].append(child_key)
7388

89+
# Add coroutine stacks for leaf tasks
90+
awaiting_tasks = {parent_id for parent_id, _, _ in awaits}
91+
for task_id in id2name:
92+
if task_id not in awaiting_tasks and task_id in task_stacks:
93+
cur = (NodeType.TASK, task_id)
94+
for frame in reversed(task_stacks[task_id]):
95+
cur = get_or_create_cor_node(cur, frame)
96+
7497
return id2label, children
7598

7699

@@ -129,12 +152,12 @@ def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
129152
The call tree is produced by `get_all_async_stacks()`, prefixing tasks
130153
with `task_emoji` and coroutine frames with `cor_emoji`.
131154
"""
132-
id2name, awaits = _index(result)
155+
id2name, awaits, task_stacks = _index(result)
133156
g = _task_graph(awaits)
134157
cycles = _find_cycles(g)
135158
if cycles:
136159
raise CycleFoundException(cycles, id2name)
137-
labels, children = _build_tree(id2name, awaits)
160+
labels, children = _build_tree(id2name, awaits, task_stacks)
138161

139162
def pretty(node):
140163
flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
@@ -154,35 +177,40 @@ def render(node, prefix="", last=True, buf=None):
154177

155178

156179
def build_task_table(result):
157-
id2name, awaits = _index(result)
180+
id2name, _, _ = _index(result)
158181
table = []
159-
for tid, tasks in result:
160-
for task_id, task_name, awaited in tasks:
161-
if not awaited:
162-
table.append(
163-
[
164-
tid,
165-
hex(task_id),
166-
task_name,
167-
"",
168-
"",
169-
"0x0"
170-
]
171-
)
172-
for stack, awaiter_id in awaited:
173-
stack = [elem[0] if isinstance(elem, tuple) else elem for elem in stack]
174-
coroutine_chain = " -> ".join(stack)
175-
awaiter_name = id2name.get(awaiter_id, "Unknown")
176-
table.append(
177-
[
178-
tid,
179-
hex(task_id),
180-
task_name,
181-
coroutine_chain,
182-
awaiter_name,
183-
hex(awaiter_id),
184-
]
185-
)
182+
183+
for awaited_info in result:
184+
thread_id = awaited_info.thread_id
185+
for task_info in awaited_info.awaited_by:
186+
# Get task info
187+
task_id = task_info.task_id
188+
task_name = task_info.task_name
189+
190+
# Build coroutine stack string
191+
frames = [frame for coro in task_info.coroutine_stack
192+
for frame in coro.call_stack]
193+
coro_stack = " -> ".join(_format_stack_entry(x).split(" ")[0]
194+
for x in frames)
195+
196+
# Handle tasks with no awaiters
197+
if not task_info.awaited_by:
198+
table.append([thread_id, hex(task_id), task_name, coro_stack,
199+
"", "", "0x0"])
200+
continue
201+
202+
# Handle tasks with awaiters
203+
for coro_info in task_info.awaited_by:
204+
parent_id = coro_info.task_name
205+
awaiter_frames = [_format_stack_entry(x).split(" ")[0]
206+
for x in coro_info.call_stack]
207+
awaiter_chain = " -> ".join(awaiter_frames)
208+
awaiter_name = id2name.get(parent_id, "Unknown")
209+
parent_id_str = (hex(parent_id) if isinstance(parent_id, int)
210+
else str(parent_id))
211+
212+
table.append([thread_id, hex(task_id), task_name, coro_stack,
213+
awaiter_chain, awaiter_name, parent_id_str])
186214

187215
return table
188216

@@ -211,11 +239,11 @@ def display_awaited_by_tasks_table(pid: int) -> None:
211239
table = build_task_table(tasks)
212240
# Print the table in a simple tabular format
213241
print(
214-
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
242+
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine stack':<50} {'awaiter chain':<50} {'awaiter name':<15} {'awaiter id':<15}"
215243
)
216-
print("-" * 135)
244+
print("-" * 180)
217245
for row in table:
218-
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<20} {row[5]:<15}")
246+
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<50} {row[5]:<15} {row[6]:<15}")
219247

220248

221249
def display_awaited_by_tasks_tree(pid: int) -> None:

Lib/http/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
class HTTPServer(socketserver.TCPServer):
116116

117117
allow_reuse_address = True # Seems to make sense in testing environment
118-
allow_reuse_port = True
118+
allow_reuse_port = False
119119

120120
def server_bind(self):
121121
"""Override server_bind to store the server name."""

Lib/logging/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,7 @@ class ConfigSocketReceiver(ThreadingTCPServer):
10181018
"""
10191019

10201020
allow_reuse_address = True
1021-
allow_reuse_port = True
1021+
allow_reuse_port = False
10221022

10231023
def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
10241024
handler=None, ready=None, verify=None):

0 commit comments

Comments
 (0)