Skip to content

Commit 074e086

Browse files
authored
Merge branch '3.13' into gh-156466-3.13
2 parents d70a626 + 6b490d5 commit 074e086

19 files changed

Lines changed: 334 additions & 46 deletions

Doc/library/imaplib.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,20 @@ An :class:`IMAP4` instance has the following methods:
618618

619619
The following attributes are defined on instances of :class:`IMAP4`:
620620

621+
.. attribute:: IMAP4.capabilities
622+
623+
A tuple of the capabilities advertised by the server, in upper case.
624+
625+
It is set when the connection is established,
626+
and refreshed after a successful :meth:`~IMAP4.login`,
627+
:meth:`~IMAP4.authenticate` or :meth:`~IMAP4.starttls`,
628+
because the server can advertise different capabilities
629+
in different connection states.
630+
631+
.. versionchanged:: 3.13.15
632+
Refreshed after :meth:`~IMAP4.login` and :meth:`~IMAP4.authenticate`.
633+
634+
621635
.. attribute:: IMAP4.PROTOCOL_VERSION
622636

623637
The most recent supported protocol in the ``CAPABILITY`` response from the

Lib/imaplib.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@ def _connect(self):
270270
self._encoding, 'replace')
271271
raise self.error('invalid greeting: ' + greeting)
272272

273-
self._refresh_capabilities()
273+
# The greeting is not a response to a command.
274+
self._refresh_capabilities(consume=True)
274275
if __debug__:
275276
if self.debug >= 3:
276277
self._mesg('CAPABILITIES: %r' % (self.capabilities,))
@@ -1142,10 +1143,14 @@ def _get_capabilities(self):
11421143
self.capabilities = tuple(dat.split())
11431144

11441145

1145-
def _refresh_capabilities(self):
1146+
def _refresh_capabilities(self, consume=False):
11461147
# Use a CAPABILITY response sent by the server, or ask for it.
1148+
# Unless it is consumed, the response can still be read with
1149+
# response('CAPABILITY').
11471150
if 'CAPABILITY' in self.untagged_responses:
1148-
dat = self.untagged_responses.pop('CAPABILITY')[-1]
1151+
dat = self.untagged_responses['CAPABILITY'][-1]
1152+
if consume:
1153+
del self.untagged_responses['CAPABILITY']
11491154
self.capabilities = tuple(str(dat, self._encoding).upper().split())
11501155
else:
11511156
self._get_capabilities()

Lib/inspect.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3422,6 +3422,10 @@ def _main():
34223422
import argparse
34233423
import importlib
34243424

3425+
# The printed text can contain characters unencodable in the encoding
3426+
# of stdout, e.g. undecodable bytes of a file name.
3427+
sys.stdout.reconfigure(errors='backslashreplace')
3428+
34253429
parser = argparse.ArgumentParser()
34263430
parser.add_argument(
34273431
'object',

Lib/ntpath.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -626,12 +626,23 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
626626
allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1005, 1920, 1921
627627

628628
# Non-strict algorithm is to find as much of the target directory
629-
# as we can and join the rest.
629+
# as we can and join the rest. join() is not used, because the tail
630+
# can contain a colon and be mistaken for a drive (gh-102475).
631+
if isinstance(path, bytes):
632+
sep = b'\\'
633+
else:
634+
sep = '\\'
635+
636+
def join(path, tail):
637+
if path[-1:] == sep or not tail:
638+
return path + tail
639+
return path + sep + tail
640+
630641
tail = path[:0]
631642
while path:
632643
try:
633644
path = _getfinalpathname(path)
634-
return join(path, tail) if tail else path
645+
return join(path, tail)
635646
except ignored_error as ex:
636647
if ex.winerror not in allowed_winerror:
637648
raise
@@ -642,7 +653,7 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
642653
new_path = _readlink_deep(path,
643654
ignored_error=ignored_error)
644655
if new_path != path:
645-
return join(new_path, tail) if tail else new_path
656+
return join(new_path, tail)
646657
except ignored_error:
647658
# If we fail to readlink(), let's keep traversing
648659
pass
@@ -657,7 +668,7 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
657668
path, name = split(path)
658669
if path and not name:
659670
return path + tail
660-
tail = join(name, tail) if tail else name
671+
tail = join(name, tail)
661672
return tail
662673

663674
def realpath(path, *, strict=False):
@@ -666,7 +677,7 @@ def realpath(path, *, strict=False):
666677
prefix = b'\\\\?\\'
667678
unc_prefix = b'\\\\?\\UNC\\'
668679
new_unc_prefix = b'\\\\'
669-
cwd = os.getcwdb()
680+
colon_sep = b':\\'
670681
# bpo-38081: Special case for realpath(b'nul')
671682
devnull = b'nul'
672683
if normcase(path) == devnull:
@@ -675,7 +686,7 @@ def realpath(path, *, strict=False):
675686
prefix = '\\\\?\\'
676687
unc_prefix = '\\\\?\\UNC\\'
677688
new_unc_prefix = '\\\\'
678-
cwd = os.getcwd()
689+
colon_sep = ':\\'
679690
# bpo-38081: Special case for realpath('nul')
680691
devnull = 'nul'
681692
if normcase(path) == devnull:
@@ -691,7 +702,9 @@ def realpath(path, *, strict=False):
691702
ignored_error = OSError
692703

693704
if not had_prefix and not isabs(path):
694-
path = join(cwd, path)
705+
# abspath() is used instead of join(cwd, path), because the path
706+
# can be relative to another drive (gh-102475).
707+
path = abspath(path)
695708
try:
696709
path = _getfinalpathname(path)
697710
initial_winerror = 0
@@ -711,25 +724,29 @@ def realpath(path, *, strict=False):
711724
# strip off that prefix unless it was already provided on the original
712725
# path.
713726
if not had_prefix and path.startswith(prefix):
714-
# For UNC paths, the prefix will actually be \\?\UNC\
715-
# Handle that case as well.
727+
# For UNC drives, the path starts with \\?\UNC\.
716728
if path.startswith(unc_prefix):
717729
spath = new_unc_prefix + path[len(unc_prefix):]
718-
else:
730+
# For drive-letter drives, the path starts with \\?\<letter>:\.
731+
elif path.startswith(colon_sep, len(prefix) + 1):
719732
spath = path[len(prefix):]
720-
# Ensure that the non-prefixed path resolves to the same path
721-
try:
722-
if _getfinalpathname(spath) == path:
723-
path = spath
724-
except ValueError as ex:
725-
# Unexpected, as an invalid path should not have gained a prefix
726-
# at any point, but we ignore this error just in case.
727-
pass
728-
except OSError as ex:
729-
# If the path does not exist and originally did not exist, then
730-
# strip the prefix anyway.
731-
if ex.winerror == initial_winerror:
732-
path = spath
733+
# For all others, e.g. volume GUID paths, it cannot be stripped.
734+
else:
735+
spath = None
736+
if spath is not None:
737+
# Ensure that the non-prefixed path resolves to the same path
738+
try:
739+
if _getfinalpathname(spath) == path:
740+
path = spath
741+
except ValueError:
742+
# Unexpected, as an invalid path should not have gained a
743+
# prefix at any point, but we ignore this error just in case.
744+
pass
745+
except OSError as ex:
746+
# If the path does not exist and originally did not exist,
747+
# then strip the prefix anyway.
748+
if ex.winerror == initial_winerror:
749+
path = spath
733750
return path
734751

735752

Lib/test/test_cmd_line.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,41 @@ def test_python_gil(self):
960960
self.assertEqual(proc.stdout.rstrip(), expected)
961961
self.assertEqual(proc.stderr, '')
962962

963+
@unittest.skipUnless(support.MS_WINDOWS, 'Test only applicable on Windows')
964+
def test_python_legacy_windows_stdio_encoding(self):
965+
# gh-86427: In the legacy mode the encoding of the standard streams
966+
# is the encoding of the console.
967+
import ctypes
968+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
969+
try:
970+
fin = open('CONIN$')
971+
except OSError:
972+
self.skipTest('no console')
973+
# We cannot use PIPE, because the standard streams should be
974+
# connected to the console. So we use the exit code.
975+
code = ("import sys; sys.exit(sys.stdin.encoding != 'cp850' or "
976+
"sys.stdout.encoding != 'cp850')")
977+
env = os.environ.copy()
978+
env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
979+
env['PYTHONUTF8'] = '0'
980+
env.pop('PYTHONIOENCODING', None)
981+
old_cp = kernel32.GetConsoleCP()
982+
old_output_cp = kernel32.GetConsoleOutputCP()
983+
with fin, open('CONOUT$', 'w') as fout:
984+
try:
985+
if not kernel32.SetConsoleCP(850):
986+
self.skipTest('cannot set the console input code page')
987+
if not kernel32.SetConsoleOutputCP(850):
988+
self.skipTest('cannot set the console output code page')
989+
proc = subprocess.run([sys.executable, '-c', code], env=env,
990+
stdin=fin, stdout=fout,
991+
stderr=subprocess.DEVNULL)
992+
finally:
993+
kernel32.SetConsoleCP(old_cp)
994+
kernel32.SetConsoleOutputCP(old_output_cp)
995+
support.skip_on_low_desktop_heap_memory_subprocess(proc.returncode)
996+
self.assertEqual(proc.returncode, 0)
997+
963998
@unittest.skipUnless(sys.platform == 'win32',
964999
'bpo-32457 only applies on Windows')
9651000
def test_argv0_normalization(self):

Lib/test/test_curses.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,26 @@ def test_output_string_embedded_null_chars(self):
434434
self.assertRaises(ValueError, stdscr.insstr, arg)
435435
self.assertRaises(ValueError, stdscr.insnstr, arg, 1)
436436

437+
def test_output_string_attr_restored(self):
438+
# A write with an attr restores the window rendition afterwards,
439+
# whether it succeeded or failed.
440+
win = curses.newwin(2, 10, 0, 0)
441+
def current_attrs():
442+
# Write a cell with the window's current rendition and read it
443+
# back, so that the rendition itself is checked.
444+
win.addstr(1, 0, ' ')
445+
return win.inch(1, 0) & curses.A_ATTRIBUTES
446+
for func, args in [(win.addstr, ('x',)), (win.addnstr, ('x', 1)),
447+
(win.insstr, ('x',)), (win.insnstr, ('x', 1))]:
448+
with self.subTest(func.__qualname__):
449+
win.attrset(curses.A_UNDERLINE)
450+
# y=100 is outside the window, so the write fails.
451+
self.assertRaises(curses.error, func, 100, 0, *args,
452+
curses.A_BOLD)
453+
self.assertEqual(current_attrs(), curses.A_UNDERLINE)
454+
func(0, 0, *args, curses.A_BOLD)
455+
self.assertEqual(current_attrs(), curses.A_UNDERLINE)
456+
437457
def test_add_string_behavior(self):
438458
# addstr() advances the cursor past the written text; addnstr()
439459
# writes at most n characters.

Lib/test/test_imaplib.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,8 @@ def cmd_ENABLE(self, tag, args):
785785
client.login('user', 'pass')
786786
self.assertIn('ENABLE', client.capabilities)
787787
self.assertIn('UTF8=ACCEPT', client.capabilities)
788+
self.assertEqual(client.response('CAPABILITY'),
789+
('CAPABILITY', [b'IMAP4rev1 ENABLE UTF8=ACCEPT']))
788790
typ, _ = client.enable('UTF8=ACCEPT')
789791
self.assertEqual(typ, 'OK')
790792

@@ -803,6 +805,8 @@ def cmd_AUTHENTICATE(self, tag, args):
803805
self.assertNotIn('ENABLE', client.capabilities)
804806
client.authenticate('MYAUTH', lambda x: b'fake')
805807
self.assertIn('ENABLE', client.capabilities)
808+
self.assertEqual(client.response('CAPABILITY'),
809+
('CAPABILITY', [b'IMAP4rev1 ENABLE']))
806810

807811
def test_greeting_capabilities(self):
808812
# Capabilities advertised in the greeting are used directly,
@@ -816,6 +820,8 @@ def cmd_CAPABILITY(self, tag, args):
816820
client, server = self._setup(GreetingHandler)
817821
self.assertEqual(client.capabilities, ('IMAP4REV1', 'ENABLE'))
818822
self.assertFalse(getattr(server, 'capability_queried', False))
823+
# The greeting is not a response to a command, so it is consumed.
824+
self.assertEqual(client.response('CAPABILITY'), ('CAPABILITY', [None]))
819825

820826
def test_login_requery_capabilities(self):
821827
# If the server does not advertise capabilities after login,

Lib/test/test_inspect/test_inspect.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from test.support import cpython_only, import_helper, suppress_immortalization
4040
from test.support import MISSING_C_DOCSTRINGS, ALWAYS_EQ
4141
from test.support.import_helper import DirsOnSysPath, ready_to_import
42-
from test.support.os_helper import TESTFN, temp_cwd
42+
from test.support.os_helper import TESTFN, TESTFN_UNDECODABLE, temp_cwd
4343
from test.support.script_helper import assert_python_ok, assert_python_failure, kill_python
4444
from test.support import has_subprocess_support, SuppressCrashReport
4545
from test import support
@@ -6556,6 +6556,25 @@ def test_builtins(self):
65566556
lines = err.decode().splitlines()
65576557
self.assertEqual(lines, ["Can't get info for builtin modules."])
65586558

6559+
@unittest.skipUnless(TESTFN_UNDECODABLE,
6560+
'requires undecodable file names')
6561+
def test_details_undecodable_path(self):
6562+
# gh-69370: the path of the module is not encodable in the encoding
6563+
# of stdout.
6564+
with temp_cwd() as test_dir:
6565+
subdir = os.path.join(os.fsencode(test_dir), TESTFN_UNDECODABLE)
6566+
try:
6567+
os.mkdir(subdir)
6568+
except OSError:
6569+
self.skipTest('undecodable paths are not supported')
6570+
with open(os.path.join(subdir, b'undecodable_mod.py'), 'w') as f:
6571+
f.write('"""Module docstring."""\n')
6572+
rc, out, err = assert_python_ok('-X', 'utf8=0', '-m', 'inspect',
6573+
'--details', 'undecodable_mod',
6574+
PYTHONPATH=os.fsdecode(subdir))
6575+
self.assertIn(b'Target: undecodable_mod', out)
6576+
self.assertEqual(err, b'')
6577+
65596578
def test_details(self):
65606579
module = importlib.import_module('unittest')
65616580
args = support.optim_args_from_interpreter_flags()

Lib/test/test_ntpath.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from test import support
1111
from test.support import os_helper, is_emscripten
1212
from test.support.os_helper import FakePath
13+
from test.support.testcase import ExtraAssertions
1314
from test import test_genericpath
1415
from tempfile import TemporaryFile
1516

@@ -82,7 +83,7 @@ def _parameterize(*parameters):
8283
return support.subTests('kwargs', parameters, _do_cleanups=True)
8384

8485

85-
class NtpathTestCase(unittest.TestCase):
86+
class NtpathTestCase(unittest.TestCase, ExtraAssertions):
8687
def assertPathEqual(self, path1, path2):
8788
if path1 == path2 or _norm(path1) == _norm(path2):
8889
return
@@ -1409,6 +1410,62 @@ def test_isjunction(self):
14091410
self.assertFalse(ntpath.isjunction('tmpdir'))
14101411
self.assertPathEqual(ntpath.realpath('testjunc'), ntpath.realpath('tmpdir'))
14111412

1413+
@unittest.skipIf(sys.platform != 'win32', "Can only test on win32.")
1414+
def test_realpath_drive_like_names(self):
1415+
# gh-102475: the unresolved tail is appended, not joined, so a name
1416+
# which looks like a drive does not reset the path.
1417+
drive = ntpath.splitroot(os.getcwd())[0]
1418+
for path, expected in [
1419+
('C:/spam:eggs', 'C:\\spam:eggs'),
1420+
('C:/nonexistent/spam:eggs', 'C:\\nonexistent\\spam:eggs'),
1421+
('C:/spam:eggs/ham', 'C:\\spam:eggs\\ham'),
1422+
('C:/nonexistent/spam:eggs/ham', 'C:\\nonexistent\\spam:eggs\\ham'),
1423+
]:
1424+
with self.subTest(path=path):
1425+
self.assertEqual(ntpath.realpath(path), expected)
1426+
self.assertEqual(ntpath.realpath(os.fsencode(path)),
1427+
os.fsencode(expected))
1428+
1429+
@unittest.skipIf(sys.platform != 'win32', "Can only test on win32.")
1430+
def test_realpath_drive_relative(self):
1431+
# gh-102475: the working directory of a drive which does not exist
1432+
# is its root directory.
1433+
for drive in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
1434+
if not ntpath.exists(drive + ':'):
1435+
break
1436+
else:
1437+
raise unittest.SkipTest('all drives exist')
1438+
self.assertEqual(ntpath.realpath(drive + ':spam'),
1439+
drive + ':\\spam')
1440+
self.assertEqual(ntpath.realpath(drive + ':'), drive + ':\\')
1441+
1442+
@unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
1443+
def test_realpath_volume_guid_path(self):
1444+
# gh-89760: the \\?\ prefix cannot be stripped from a volume GUID path.
1445+
# Find a volume which is not mounted as a drive.
1446+
for volume in os.listvolumes():
1447+
if not os.listmounts(volume):
1448+
break
1449+
else:
1450+
raise unittest.SkipTest('no volume without a mount point')
1451+
1452+
with os_helper.temp_dir() as d:
1453+
with os_helper.change_cwd(d):
1454+
# _winapi.CreateJunction() adds the \\??\\ prefix to a path
1455+
# which already has a prefix.
1456+
try:
1457+
subprocess.run(['cmd', '/c', 'mklink', '/j',
1458+
'testjunc', volume],
1459+
check=True, capture_output=True)
1460+
except (OSError, subprocess.CalledProcessError):
1461+
raise unittest.SkipTest('creating the test junction failed')
1462+
1463+
for path in 'testjunc', 'testjunc/spam', 'testjunc/spam/eggs':
1464+
with self.subTest(path=path):
1465+
realpath = ntpath.realpath(path)
1466+
self.assertStartsWith(realpath, '\\\\?\\Volume{')
1467+
self.assertTrue(ntpath.isabs(realpath), realpath)
1468+
14121469
def test_isfile_invalid_paths(self):
14131470
isfile = ntpath.isfile
14141471
self.assertIs(isfile('/tmp\udfffabcds'), False)

0 commit comments

Comments
 (0)