Skip to content

Commit 3f78f54

Browse files
authored
Merge branch 'main' into fix-fromfd-test-race
2 parents ca1bbd8 + ef1fce0 commit 3f78f54

13 files changed

Lines changed: 163 additions & 15 deletions

‎Lib/_pyrepl/utils.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,21 @@
1919
from .trace import trace
2020

2121

22-
ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]")
22+
ANSI_ESCAPE_SEQUENCE = re.compile(
23+
r"""
24+
\x1b
25+
(?:
26+
# CSI: ESC [ followed by parameter/intermediate bytes and a final byte.
27+
# For example, ESC [ 31 m ("\x1b[31m") selects red foreground text.
28+
\[ [ -@]* [A-~]
29+
|
30+
# OSC: ESC ] followed by a payload terminated by BEL or ST (ESC \).
31+
# For example, ESC ] 0 ; title BEL ("\x1b]0;title\x07") sets the title.
32+
\] [^\x1b\x07]* (?: \x07 | \x1b\\ )
33+
)
34+
""",
35+
re.X,
36+
)
2337
ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02")
2438
ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})
2539
IDENTIFIERS_AFTER = frozenset({"def", "class"})

‎Lib/sqlite3/__main__.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
from ._completer import completer
1616

1717

18+
# IDLE uses CTRL-D for EOF on all platforms (gh-102628).
19+
if sys.platform == "win32" and "idlelib.run" not in sys.modules:
20+
EOF_KEY = "CTRL-Z"
21+
else:
22+
EOF_KEY = "CTRL-D"
23+
24+
1825
def execute(c, sql, suppress_errors=True, theme=theme_no_color):
1926
"""Helper that wraps execution of SQL code.
2027
@@ -70,7 +77,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
7077
print(f"Enter SQL code or one of the below commands, and press enter.\n\n"
7178
f"{t.builtin}.version{t.reset} Print underlying SQLite library version\n"
7279
f"{t.builtin}.help{t.reset} Print this help message\n"
73-
f"{t.builtin}.quit{t.reset} Exit the CLI, equivalent to CTRL-D\n")
80+
f"{t.builtin}.quit{t.reset} Exit the CLI, equivalent to {EOF_KEY}\n")
7481
case "quit":
7582
sys.exit(0)
7683
case "":
@@ -115,16 +122,12 @@ def main(*args):
115122
db_name = repr(args.filename)
116123

117124
# Prepare REPL banner and prompts.
118-
if sys.platform == "win32" and "idlelib.run" not in sys.modules:
119-
eofkey = "CTRL-Z"
120-
else:
121-
eofkey = "CTRL-D"
122125
banner = dedent(f"""
123126
sqlite3 shell, running on SQLite version {sqlite3.sqlite_version}
124127
Connected to {db_name}
125128
126129
Each command will be run using execute() on the cursor.
127-
Type ".help" for more information; type ".quit" or {eofkey} to quit.
130+
Type ".help" for more information; type ".quit" or {EOF_KEY} to quit.
128131
""").strip()
129132

130133
theme = get_theme()

‎Lib/test/test_pyrepl/test_render.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ def test_from_rendered_text_empty_string(self):
5555
self.assertEqual(line.text, "")
5656
self.assertEqual(line.width, 0)
5757

58+
def test_from_rendered_text_with_osc_control(self):
59+
osc = "\x1b]633;A\x1b\\"
60+
prompt = ">>> "
61+
line = RenderLine.from_rendered_text(osc + prompt)
62+
63+
self.assertEqual(line.width, len(prompt))
64+
self.assertEqual(line.cells[0].controls, (osc,))
65+
self.assertEqual(line.cells[0].text, "")
66+
5867
def test_from_rendered_text_with_non_sgr_controls(self):
5968
# \x1b[H is a cursor-home control (not SGR since it doesn't end with 'm')
6069
line = RenderLine.from_rendered_text("\x1b[Hx")

‎Lib/test/test_pyrepl/test_utils.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ def test_wlen(self):
4747
self.assertEqual(wlen('e\N{COMBINING ACUTE ACCENT}'), 1)
4848
self.assertEqual(wlen('a\N{ZERO WIDTH JOINER}b'), 2)
4949

50+
def test_wlen_with_osc_sequence(self):
51+
prompt = ">>> "
52+
self.assertEqual(wlen("\x1b]633;A\x07" + prompt), len(prompt))
53+
54+
def test_wlen_with_unterminated_osc_sequence_as_individual_characters(self):
55+
# \x1b is treated as a single character (ESC)
56+
self.assertEqual(wlen("\x1b]633;A"), len("\x1b]633;A"))
57+
5058
def test_prev_next_window(self):
5159
def gen_normal():
5260
yield 1

‎Lib/test/test_sqlite3/test_cli.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class InteractiveSession(unittest.TestCase):
8282
MEMORY_DB_MSG = "Connected to a transient in-memory database"
8383
PS1 = "\001\002sqlite> \001\002"
8484
PS2 = "\001\002 ... \001\002"
85+
EOF_KEY = "CTRL-Z" if sys.platform == "win32" else "CTRL-D"
8586

8687
def run_cli(self, *args, commands=()):
8788
with (
@@ -104,11 +105,19 @@ def run_cli(self, *args, commands=()):
104105
def test_interact(self):
105106
out, err = self.run_cli()
106107
self.assertIn(self.MEMORY_DB_MSG, err)
107-
self.assertIn(self.MEMORY_DB_MSG, err)
108+
self.assertIn(f'type ".quit" or {self.EOF_KEY} to quit', err)
108109
self.assertEndsWith(out, self.PS1)
109110
self.assertEqual(out.count(self.PS1), 1)
110111
self.assertEqual(out.count(self.PS2), 0)
111112

113+
def test_interact_help(self):
114+
out, err = self.run_cli(commands=(".help",))
115+
self.assertIn(self.MEMORY_DB_MSG, err)
116+
self.assertIn(f"Exit the CLI, equivalent to {self.EOF_KEY}", out)
117+
self.assertEndsWith(out, self.PS1)
118+
self.assertEqual(out.count(self.PS1), 2)
119+
self.assertEqual(out.count(self.PS2), 0)
120+
112121
def test_interact_quit(self):
113122
out, err = self.run_cli(commands=(".quit",))
114123
self.assertIn(self.MEMORY_DB_MSG, err)

‎Lib/test/test_urllib2.py‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,6 +1751,66 @@ def test_proxy_basic_auth(self):
17511751
"proxy.example.com:3128",
17521752
)
17531753

1754+
def test_proxy_digest_auth(self):
1755+
# gh-60299: ProxyDigestAuthHandler must look up credentials using
1756+
# the proxy's authority
1757+
opener = OpenerDirector()
1758+
ph = urllib.request.ProxyHandler(dict(http="proxy.example.com:3128"))
1759+
opener.add_handler(ph)
1760+
password_manager = MockPasswordManager()
1761+
digest_handler = urllib.request.ProxyDigestAuthHandler(password_manager)
1762+
realm = "ACME Networks"
1763+
http_handler = MockHTTPHandlerRedirect(
1764+
407,
1765+
'Proxy-Authenticate: Digest realm="%s", nonce="abcdef0123456789", '
1766+
'qop="auth"\r\n\r\n' % realm)
1767+
opener.add_handler(digest_handler)
1768+
opener.add_handler(http_handler)
1769+
1770+
user, password = "wile", "coyote"
1771+
request_url = "http://acme.example.com/protected"
1772+
proxy_authority = "proxy.example.com:3128"
1773+
1774+
digest_handler.add_password(realm, proxy_authority, user, password)
1775+
self.assertEqual(realm, password_manager.realm)
1776+
self.assertEqual(proxy_authority, password_manager.url)
1777+
1778+
opener.open(request_url)
1779+
1780+
self.assertEqual(password_manager.target_realm, realm)
1781+
self.assertEqual(password_manager.target_url, proxy_authority)
1782+
1783+
self.assertEqual(len(http_handler.requests), 2)
1784+
self.assertFalse(
1785+
http_handler.requests[0].has_header("Proxy-authorization"))
1786+
self.assertTrue(
1787+
http_handler.requests[1].has_header("Proxy-authorization"))
1788+
self.assertTrue(
1789+
http_handler.requests[1]
1790+
.get_header("Proxy-authorization").startswith("Digest "))
1791+
1792+
def test_digest_auth_uses_full_url(self):
1793+
# The non-proxy case must keep using the full request URL (not
1794+
# just the host) for the password lookup, so that sub-URI realm
1795+
# matching (HTTPPasswordMgr.is_suburi) keeps working
1796+
opener = OpenerDirector()
1797+
password_manager = MockPasswordManager()
1798+
digest_handler = urllib.request.HTTPDigestAuthHandler(password_manager)
1799+
realm = "ACME Networks"
1800+
request_url = "http://acme.example.com/protected"
1801+
http_handler = MockHTTPHandlerRedirect(
1802+
401,
1803+
'WWW-Authenticate: Digest realm="%s", nonce="abcdef0123456789", '
1804+
'qop="auth"\r\n\r\n' % realm)
1805+
opener.add_handler(digest_handler)
1806+
opener.add_handler(http_handler)
1807+
1808+
digest_handler.add_password(realm, request_url, "wile", "coyote")
1809+
opener.open(request_url)
1810+
1811+
self.assertEqual(password_manager.target_realm, realm)
1812+
self.assertEqual(password_manager.target_url, request_url)
1813+
17541814
def test_basic_and_digest_auth_handlers(self):
17551815
# HTTPDigestAuthHandler raised an exception if it couldn't handle a 40*
17561816
# response (https://bugs.python.org/issue1479302), where it should instead
@@ -1811,6 +1871,25 @@ def test_unsupported_auth_digest_handler(self):
18111871
opener.add_handler(http_handler)
18121872
self.assertRaises(ValueError, opener.open, "http://www.example.com")
18131873

1874+
def test_digest_auth_failure_code(self):
1875+
# gh-53852: a failed proxy digest auth must report 407, not 401.
1876+
headers = http.client.HTTPMessage()
1877+
for handler_class, auth_header, code in [
1878+
(urllib.request.HTTPDigestAuthHandler,
1879+
'www-authenticate', 401),
1880+
(urllib.request.ProxyDigestAuthHandler,
1881+
'proxy-authenticate', 407)]:
1882+
with self.subTest(handler=handler_class.__name__):
1883+
handler = handler_class()
1884+
handler.retried = 6
1885+
req = Request("http://www.example.com/")
1886+
with self.assertRaises(urllib.error.HTTPError) as cm:
1887+
handler.http_error_auth_reqed(auth_header,
1888+
"www.example.com",
1889+
req, headers)
1890+
self.assertEqual(cm.exception.code, code)
1891+
cm.exception.close()
1892+
18141893
def test_unsupported_auth_basic_handler(self):
18151894
# While using BasicAuthHandler
18161895
opener = OpenerDirector()

‎Lib/test/test_urllib2_localnet.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ def create_fake_proxy_handler(*args, **kwargs):
351351
self.server.start()
352352
self.server.ready.wait()
353353
proxy_url = "http://127.0.0.1:%d" % self.server.port
354+
# Credentials must be registered against proxy authority
355+
self.proxy_authority = "127.0.0.1:%d" % self.server.port
354356
handler = urllib.request.ProxyHandler({"http" : proxy_url})
355357
self.proxy_digest_handler = urllib.request.ProxyDigestAuthHandler()
356358
self.opener = urllib.request.build_opener(
@@ -361,29 +363,31 @@ def stop_server(self):
361363
self.server = None
362364

363365
def test_proxy_with_bad_password_raises_httperror(self):
364-
self.proxy_digest_handler.add_password(self.REALM, self.URL,
366+
self.proxy_digest_handler.add_password(self.REALM, self.proxy_authority,
365367
self.USER, self.PASSWD+"bad")
366368
self.digest_auth_handler.set_qop("auth")
367369
with self.assertRaises(urllib.error.HTTPError) as cm:
368370
self.opener.open(self.URL)
371+
self.assertEqual(cm.exception.code, 407)
369372
cm.exception.close()
370373

371374
def test_proxy_with_no_password_raises_httperror(self):
372375
self.digest_auth_handler.set_qop("auth")
373376
with self.assertRaises(urllib.error.HTTPError) as cm:
374377
self.opener.open(self.URL)
378+
self.assertEqual(cm.exception.code, 407)
375379
cm.exception.close()
376380

377381
def test_proxy_qop_auth_works(self):
378-
self.proxy_digest_handler.add_password(self.REALM, self.URL,
382+
self.proxy_digest_handler.add_password(self.REALM, self.proxy_authority,
379383
self.USER, self.PASSWD)
380384
self.digest_auth_handler.set_qop("auth")
381385
with self.opener.open(self.URL) as result:
382386
while result.read():
383387
pass
384388

385389
def test_proxy_qop_auth_int_works_or_throws_urlerror(self):
386-
self.proxy_digest_handler.add_password(self.REALM, self.URL,
390+
self.proxy_digest_handler.add_password(self.REALM, self.proxy_authority,
387391
self.USER, self.PASSWD)
388392
self.digest_auth_handler.set_qop("auth-int")
389393
try:

‎Lib/urllib/request.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,7 +1093,11 @@ def http_error_auth_reqed(self, auth_header, host, req, headers):
10931093
# prompting for the information. Crap. This isn't great
10941094
# but it's better than the current 'repeat until recursion
10951095
# depth exceeded' approach <wink>
1096-
raise HTTPError(req.full_url, 401, "digest auth failed",
1096+
if auth_header.lower() == 'proxy-authenticate':
1097+
code = 407
1098+
else:
1099+
code = 401
1100+
raise HTTPError(req.full_url, code, "digest auth failed",
10971101
headers, None)
10981102
else:
10991103
self.retried += 1
@@ -1144,7 +1148,13 @@ def get_authorization(self, req, chal):
11441148
if H is None:
11451149
return None
11461150

1147-
user, pw = self.passwd.find_user_password(realm, req.full_url)
1151+
# proxy auth is looked up by the proxy's authority
1152+
# req.host, set by Request.set_proxy()
1153+
if self.auth_header == 'Proxy-Authorization':
1154+
authority = req.host
1155+
else:
1156+
authority = req.full_url
1157+
user, pw = self.passwd.find_user_password(realm, authority)
11481158
if user is None:
11491159
return None
11501160

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Correct the EOF key shown in the :mod:`sqlite3` CLI ``.help`` text on Windows.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Correctly detect and handle OSC sequences in PyREPL.

0 commit comments

Comments
 (0)