Skip to content

Commit e8477c5

Browse files
committed
Merge remote-tracking branch 'origin/main' into httplib-query-method-rfc-10008
2 parents b7c9a20 + f3fd9dc commit e8477c5

10 files changed

Lines changed: 258 additions & 15 deletions

File tree

Doc/library/asyncio-graph.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
.. _asyncio-graph:
55

66
========================
7-
Call Graph Introspection
7+
Call graph introspection
88
========================
99

1010
**Source code:** :source:`Lib/asyncio/graph.py`
@@ -17,6 +17,12 @@ a suspended *future*. These utilities and the underlying machinery
1717
can be used from within a Python program or by external profilers
1818
and debuggers.
1919

20+
.. seealso::
21+
22+
:ref:`asyncio-introspection-tools`
23+
Command-line tools for inspecting tasks in another running Python
24+
process.
25+
2026
.. versionadded:: 3.14
2127

2228

Doc/library/asyncio-tools.rst

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
.. currentmodule:: asyncio
2+
3+
.. _asyncio-introspection-tools:
4+
5+
================================
6+
Command-line introspection tools
7+
================================
8+
9+
**Source code:** :source:`Lib/asyncio/tools.py`
10+
11+
-------------------------------------
12+
13+
The :mod:`!asyncio` module can be invoked as a script via ``python -m
14+
asyncio`` to inspect the task graph of another running Python process without
15+
modifying it or restarting it. The :mod:`!asyncio.tools` submodule implements
16+
this interface.
17+
18+
The following commands inspect the process identified by ``PID``:
19+
20+
.. code-block:: shell-session
21+
22+
$ python -m asyncio pstree [--retries N] PID
23+
$ python -m asyncio ps [--retries N] PID
24+
25+
The commands read the target process state without executing any code in it.
26+
They are only available on supported platforms and may require permission to
27+
inspect another process. See the :ref:`permission-requirements <permission-requirements>` for details.
28+
29+
.. seealso::
30+
31+
:ref:`asyncio-graph`
32+
Programmatic APIs for inspecting the async call graph of a task or
33+
future in the current process.
34+
35+
The command examples below use this program, which creates a task hierarchy
36+
suitable for inspection and prints its process ID:
37+
38+
.. code-block:: python
39+
:caption: example.py
40+
41+
import asyncio
42+
import os
43+
44+
async def play(track):
45+
await asyncio.sleep(3600)
46+
print(f"🎵 Finished: {track}")
47+
48+
async def album(name, tracks):
49+
async with asyncio.TaskGroup() as tg:
50+
for track in tracks:
51+
tg.create_task(play(track), name=track)
52+
53+
async def main():
54+
print(f"PID: {os.getpid()}")
55+
async with asyncio.TaskGroup() as tg:
56+
tg.create_task(
57+
album("Sundowning", ["TNDNBTG", "Levitate"]),
58+
name="Sundowning",
59+
)
60+
tg.create_task(
61+
album("TMBTE", ["DYWTYLM", "Aqua Regia"]),
62+
name="TMBTE",
63+
)
64+
65+
asyncio.run(main())
66+
67+
Run the program in one terminal and leave it running:
68+
69+
.. code-block:: shell-session
70+
71+
$ python example.py
72+
PID: 12345
73+
74+
Then pass the printed process ID to the commands from another terminal.
75+
Thread IDs, task IDs, file paths, and line numbers vary between runs and
76+
source layouts.
77+
78+
.. versionadded:: 3.14
79+
80+
Command-line options
81+
====================
82+
83+
.. option:: pstree PID
84+
85+
Display task and coroutine relationships as a tree. Each task is shown
86+
with its full coroutine stack, nested under the task (if any) that is
87+
awaiting it. This subcommand is useful for quickly identifying which branch
88+
of a task hierarchy is blocked and where in its coroutine stack execution
89+
has paused:
90+
91+
.. code-block:: shell-session
92+
93+
$ python -m asyncio pstree 12345
94+
└── (T) Task-1
95+
└── main example.py:12
96+
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:75
97+
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:124
98+
├── (T) Sundowning
99+
│ └── album example.py:7
100+
│ └── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:75
101+
│ └── TaskGroup._aexit Lib/asyncio/taskgroups.py:124
102+
│ ├── (T) TNDNBTG
103+
│ │ └── play example.py:4
104+
│ │ └── sleep Lib/asyncio/tasks.py:702
105+
│ └── (T) Levitate
106+
│ └── play example.py:4
107+
│ └── sleep Lib/asyncio/tasks.py:702
108+
└── (T) TMBTE
109+
└── album example.py:7
110+
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:75
111+
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:124
112+
├── (T) DYWTYLM
113+
│ └── play example.py:4
114+
│ └── sleep Lib/asyncio/tasks.py:702
115+
└── (T) Aqua Regia
116+
└── play example.py:4
117+
└── sleep Lib/asyncio/tasks.py:702
118+
119+
If the await graph contains a cycle, ``pstree`` reports an error instead
120+
of printing a tree. A cycle in the await graph is unusual and typically
121+
indicates a programming error:
122+
123+
.. code-block:: shell-session
124+
125+
$ python -m asyncio pstree 12345
126+
ERROR: await-graph contains cycles - cannot print a tree!
127+
128+
cycle: Task-2 → Task-3 → Task-2
129+
130+
.. option:: ps PID
131+
132+
Display a flat table of all pending tasks in the process *PID*. Each row
133+
shows the event-loop thread ID, task ID and name, coroutine stack, and the
134+
awaiting task's stack, name, and ID, if any.
135+
136+
This subcommand prints all tasks regardless of whether the await graph
137+
contains cycles:
138+
139+
.. code-block:: shell-session
140+
141+
$ python -m asyncio ps 12345
142+
tid task id task name coroutine stack awaiter chain awaiter name awaiter id
143+
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
144+
18445801 0x10a456060 Task-1 TaskGroup._aexit -> TaskGroup.__aexit__ -> main 0x0
145+
18445801 0x10a439f60 Sundowning TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x10a456060
146+
18445801 0x10a439d70 TMBTE TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x10a456060
147+
18445801 0x10a2a3a80 TNDNBTG sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x10a439f60
148+
18445801 0x10a2a38a0 Levitate sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x10a439f60
149+
18445801 0x10a2d7150 DYWTYLM sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x10a439d70
150+
18445801 0x10a6bdaa0 Aqua Regia sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x10a439d70
151+
152+
.. option:: --retries N
153+
154+
Retry failed attempts to inspect the target process up to *N* times. This
155+
can help when the target process changes while its state is being read.
156+
157+
.. versionadded:: 3.15

Doc/library/asyncio.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ asyncio provides a set of **high-level** APIs to:
4747

4848
* :ref:`synchronize <asyncio-sync>` concurrent code;
4949

50+
For **introspection**, asyncio provides APIs and tools for:
51+
52+
* inspecting the :ref:`async call graph <asyncio-graph>` of tasks and futures;
53+
54+
* inspecting tasks in another running Python process with
55+
:ref:`command-line tools <asyncio-introspection-tools>`;
56+
5057
Additionally, there are **low-level** APIs for
5158
*library and framework developers* to:
5259

@@ -108,7 +115,13 @@ for full functionality and the latest features.
108115
asyncio-subprocess.rst
109116
asyncio-queue.rst
110117
asyncio-exceptions.rst
118+
119+
.. toctree::
120+
:caption: Introspection APIs
121+
:maxdepth: 1
122+
111123
asyncio-graph.rst
124+
asyncio-tools.rst
112125

113126
.. toctree::
114127
:caption: Low-level APIs

Doc/library/urllib.parse.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -649,8 +649,9 @@ task isn't already covered by the URL parsing functions above.
649649

650650
The optional *encoding* and *errors* parameters specify how to deal with
651651
non-ASCII characters, as accepted by the :meth:`str.encode` method.
652-
*encoding* defaults to ``'utf-8'``.
653-
*errors* defaults to ``'strict'``, meaning unsupported characters raise a
652+
Although these parameters default to ``None`` in the function signature,
653+
when processing :class:`str` inputs, *encoding* effectively defaults to ``'utf-8'``
654+
and *errors* to ``'strict'``, meaning unsupported characters raise a
654655
:class:`UnicodeEncodeError`.
655656
*encoding* and *errors* must not be supplied if *string* is a
656657
:class:`bytes`, or a :class:`TypeError` is raised.

Lib/test/test_curses.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import functools
22
import inspect
33
import os
4+
import platform
45
import select
56
import string
67
import sys
@@ -86,6 +87,18 @@ def wrapped(self, *args, **kwargs):
8687
BROKEN_NEWTERM = _ncurses_version is not None and _ncurses_version < (6, 5)
8788
USE_NEWTERM = hasattr(curses, 'newterm') and not BROKEN_NEWTERM
8889

90+
# Older macOS reports a variation selector as a spacing character (wcwidth()
91+
# == 1) rather than a combining mark, so it cannot share a cell with its base.
92+
# The failure is confirmed on 14.2 and gone by 26, so skip below 26.
93+
def _broken_variation_selector_width():
94+
if sys.platform == 'darwin':
95+
mac_ver = platform.mac_ver()[0]
96+
if mac_ver:
97+
return tuple(map(int, mac_ver.split('.'))) < (26,)
98+
return False
99+
100+
BROKEN_VARIATION_SELECTOR_WIDTH = _broken_variation_selector_width()
101+
89102
# newterm() is used when available (it reports errors instead of exiting), but
90103
# initscr() is still the fallback, and an unusable $TERM has no terminal to
91104
# drive either way.
@@ -411,7 +424,8 @@ def test_addch_emoji(self):
411424
stdscr = self.stdscr
412425
if self._encodable('\U0001f600'):
413426
stdscr.addch(0, 0, '\U0001f600') # single emoji
414-
if self._encodable('\u263a\ufe0f'):
427+
# Skip the variation selector where the platform reports it as spacing.
428+
if not BROKEN_VARIATION_SELECTOR_WIDTH and self._encodable('\u263a\ufe0f'):
415429
stdscr.addch(1, 0, '\u263a\ufe0f') # WHITE SMILING FACE + VS-16
416430
# An emoji ZWJ sequence or an emoji with a modifier is more than one
417431
# spacing character and cannot share a single cell.

Lib/test/test_ttk/test_style.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,8 @@ def test_theme_create_image(self):
440440

441441
b = ttk.Label(self.root, style='TestWidget')
442442
b.pack(expand=True, fill='both')
443-
self.assertEqual(b.winfo_reqwidth(), 134)
443+
# The exact width varies with the Tk version and display scaling.
444+
self.assertGreater(b.winfo_reqwidth(), 130)
444445
self.assertEqual(b.winfo_reqheight(), 100)
445446

446447
style.theme_use(curr_theme)

Lib/test/test_ttk/test_widgets.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,8 +1500,13 @@ def test_configure_height(self):
15001500

15011501
def test_configure_selectmode(self):
15021502
widget = self.create()
1503-
self.checkEnumParam(widget, 'selectmode',
1504-
'none', 'browse', 'extended')
1503+
if tk_version >= (9, 1):
1504+
self.checkEnumParam(widget, 'selectmode',
1505+
'none', 'single', 'browse', 'extended',
1506+
'multiple')
1507+
else:
1508+
self.checkEnumParam(widget, 'selectmode',
1509+
'none', 'browse', 'extended')
15051510

15061511
@requires_tk(8, 7)
15071512
def test_configure_selecttype(self):
@@ -1784,6 +1789,9 @@ def simulate_heading_click(x, y):
17841789
commands = self.tv.master._tclCommands
17851790
self.tv.heading('#0', command=str(self.tv.heading('#0', command=None)))
17861791
self.assertEqual(commands, self.tv.master._tclCommands)
1792+
# Click elsewhere first, so the second heading click is not reported
1793+
# as a double click (which does not invoke the command).
1794+
simulate_mouse_click(self.tv, 5, 50)
17871795
simulate_heading_click(5, 5)
17881796
if not success:
17891797
self.fail("The command associated to the treeview heading wasn't "
@@ -1892,10 +1900,11 @@ def test_insert_item(self):
18921900
value)
18931901

18941902
# test for values which are not None
1903+
keep_type = self.wantobjects and tk_version >= (9, 1)
18951904
itemid = self.tv.insert('', 'end', 0)
1896-
self.assertEqual(itemid, '0')
1905+
self.assertEqual(itemid, 0 if keep_type else '0')
18971906
itemid = self.tv.insert('', 'end', 0.0)
1898-
self.assertEqual(itemid, '0.0')
1907+
self.assertEqual(itemid, 0.0 if keep_type else '0.0')
18991908
# this is because False resolves to 0 and element with 0 iid is already present
19001909
self.assertRaises(tkinter.TclError, self.tv.insert, '', 'end', False)
19011910
self.assertRaises(tkinter.TclError, self.tv.insert, '', 'end', '')
@@ -1955,7 +1964,10 @@ def test_selection(self):
19551964

19561965
self.tv.insert('', 'end', id=b'bytes\xe2\x82\xac')
19571966
self.tv.selection_set(b'bytes\xe2\x82\xac')
1958-
self.assertEqual(self.tv.selection(), ('bytes\xe2\x82\xac',))
1967+
self.assertEqual(self.tv.selection(),
1968+
(b'bytes\xe2\x82\xac',)
1969+
if self.wantobjects and tk_version >= (9, 1)
1970+
else ('bytes\xe2\x82\xac',))
19591971

19601972
self.tv.selection_set()
19611973
self.assertEqual(self.tv.selection(), ())
@@ -2004,14 +2016,19 @@ def test_virtual_events(self):
20042016
def test_set(self):
20052017
self.tv['columns'] = ['A', 'B']
20062018
item = self.tv.insert('', 'end', values=['a', 'b'])
2007-
self.assertEqual(self.tv.set(item), {'A': 'a', 'B': 'b'})
2019+
values = self.tv.set(item)
2020+
if tk_version >= (9, 1):
2021+
self.assertEqual(values.pop('#0'), '')
2022+
self.assertEqual(values, {'A': 'a', 'B': 'b'})
20082023

20092024
self.tv.set(item, 'B', 'a')
20102025
self.assertEqual(self.tv.item(item, values=None),
20112026
('a', 'a') if self.wantobjects else 'a a')
20122027

20132028
self.tv['columns'] = ['B']
2014-
self.assertEqual(self.tv.set(item), {'B': 'a'})
2029+
values = self.tv.set(item)
2030+
values.pop('#0', None)
2031+
self.assertEqual(values, {'B': 'a'})
20152032

20162033
self.tv.set(item, 'B', 'b')
20172034
self.assertEqual(self.tv.set(item, column='B'), 'b')
@@ -2023,7 +2040,9 @@ def test_set(self):
20232040
123 if self.wantobjects else '123')
20242041
self.assertEqual(self.tv.item(item, values=None),
20252042
(123, 'a') if self.wantobjects else '123 a')
2026-
self.assertEqual(self.tv.set(item),
2043+
values = self.tv.set(item)
2044+
values.pop('#0', None)
2045+
self.assertEqual(values,
20272046
{'B': 123} if self.wantobjects else {'B': '123'})
20282047

20292048
# inexistent column

Lib/test/test_urllib2.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,35 @@ def test_http(self):
980980
self.assertEqual(req.unredirected_hdrs["Host"], "baz")
981981
self.assertEqual(req.unredirected_hdrs["Spam"], "foo")
982982

983+
def test_http_header_priority(self):
984+
# gh-47005: regular headers set via add_header() must override
985+
# unredirected headers with the same name in do_open(), consistent
986+
# with get_header() and header_items().
987+
cases = [
988+
("Content-Type", "application/json", "application/x-www-form-urlencoded"),
989+
("Content-Length", "99", "0"),
990+
("Host", "override.example.com", "internal.example.com"),
991+
("Authorization", "Bearer user-token", "Basic stale="),
992+
("Cookie", "a=1", "b=2"),
993+
("User-Agent", "MyApp/1.0", "Python-urllib/test"),
994+
]
995+
h = urllib.request.AbstractHTTPHandler()
996+
h.parent = MockOpener()
997+
998+
for key, regular, unredirected in cases:
999+
req = Request("http://example.com/", headers={key: regular})
1000+
req.timeout = None
1001+
req.add_unredirected_header(key, unredirected)
1002+
1003+
http = MockHTTPClass()
1004+
h.do_open(http, req)
1005+
1006+
sent_headers = dict(http.req_headers)
1007+
self.assertEqual(sent_headers[key], regular)
1008+
# key is capitalized by add_header() and add_unredirected_header() calls
1009+
self.assertEqual(req.get_header(key.capitalize()), regular)
1010+
self.assertEqual(dict(req.header_items())[key.capitalize()], regular)
1011+
9831012
def test_http_body_file(self):
9841013
# A regular file - chunked encoding is used unless Content Length is
9851014
# already set.

Lib/urllib/request.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,8 +1293,7 @@ def do_open(self, http_class, req, **http_conn_args):
12931293
h.set_debuglevel(self._debuglevel)
12941294

12951295
headers = dict(req.unredirected_hdrs)
1296-
headers.update({k: v for k, v in req.headers.items()
1297-
if k not in headers})
1296+
headers.update(req.headers)
12981297

12991298
# TODO(jhylton): Should this be redesigned to handle
13001299
# persistent connections?

0 commit comments

Comments
 (0)