diff --git a/.github/workflows/reusable-check-html-ids.yml b/.github/workflows/reusable-check-html-ids.yml
index bdebacc9eed06bc..41ba1288be1ecf4 100644
--- a/.github/workflows/reusable-check-html-ids.yml
+++ b/.github/workflows/reusable-check-html-ids.yml
@@ -20,13 +20,6 @@ jobs:
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}
- - name: 'Downgrade Git'
- # Temporarily downgrade to 2.43 until 2.55 is in the runner image,
- # to avoid "fatal: shallow file has changed since we read it" bug.
- # See https://github.com/python/cpython/issues/151365.
- run: |
- sudo apt-get install -y --allow-downgrades 'git=1:2.43.*' 'git-man=1:2.43.*'
- git --version
- name: 'Find merge base'
id: merge-base
run: |
diff --git a/.github/workflows/reusable-context.yml b/.github/workflows/reusable-context.yml
index f307326bae41209..c998cbff181dd12 100644
--- a/.github/workflows/reusable-context.yml
+++ b/.github/workflows/reusable-context.yml
@@ -90,15 +90,6 @@ jobs:
|| ''
}}
- - name: 'Downgrade Git'
- # Temporarily downgrade to 2.43 until 2.55 is in the runner image,
- # to avoid "fatal: shallow file has changed since we read it" bug.
- # See https://github.com/python/cpython/issues/151365.
- if: github.event_name == 'pull_request'
- run: |
- sudo apt-get install -y --allow-downgrades 'git=1:2.43.*' 'git-man=1:2.43.*'
- git --version
-
# Adapted from https://github.com/actions/checkout/issues/520#issuecomment-1167205721
- name: Fetch commits to get branch diff
if: github.event_name == 'pull_request'
diff --git a/.github/workflows/reusable-docs.yml b/.github/workflows/reusable-docs.yml
index 6b1a9076c8251b0..199e0fd8d181f0b 100644
--- a/.github/workflows/reusable-docs.yml
+++ b/.github/workflows/reusable-docs.yml
@@ -47,14 +47,6 @@ jobs:
&& github.event.pull_request.head.sha
|| ''
}}
- - name: 'Downgrade Git'
- # Temporarily downgrade to 2.43 until 2.55 is in the runner image,
- # to avoid "fatal: shallow file has changed since we read it" bug.
- # See https://github.com/python/cpython/issues/151365.
- if: github.event_name == 'pull_request'
- run: |
- sudo apt-get install -y --allow-downgrades 'git=1:2.43.*' 'git-man=1:2.43.*'
- git --version
# Adapted from https://github.com/actions/checkout/issues/520#issuecomment-1167205721
- name: 'Fetch commits to get branch diff'
if: github.event_name == 'pull_request'
diff --git a/Doc/library/doctest.rst b/Doc/library/doctest.rst
index 3298697af8511b6..4b61c07919dd025 100644
--- a/Doc/library/doctest.rst
+++ b/Doc/library/doctest.rst
@@ -1162,6 +1162,12 @@ from text files and modules with doctests:
.. versionchanged:: 3.15
Run each example as a :ref:`subtest `.
+ .. versionchanged:: next
+ Report every example, as in verbose mode, if the test runner reports
+ more than the test names, i.e. its
+ :attr:`~unittest.TestResult.verbosity` is 3 or higher (for example
+ with ``python -m unittest -vv``).
+
Under the covers, :func:`DocTestSuite` creates a :class:`unittest.TestSuite` out
of :class:`!doctest.DocTestCase` instances, and :class:`!DocTestCase` is a
subclass of :class:`unittest.TestCase`. :class:`!DocTestCase` isn't documented
diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst
index 910b0f00c0e7de5..745683673ccf507 100644
--- a/Doc/library/imaplib.rst
+++ b/Doc/library/imaplib.rst
@@ -907,6 +907,20 @@ An :class:`IMAP4` instance has the following methods:
The following attributes are defined on instances of :class:`IMAP4`:
+.. attribute:: IMAP4.capabilities
+
+ A tuple of the capabilities advertised by the server, in upper case.
+
+ It is set when the connection is established,
+ and refreshed after a successful :meth:`~IMAP4.login`,
+ :meth:`~IMAP4.authenticate` or :meth:`~IMAP4.starttls`,
+ because the server can advertise different capabilities
+ in different connection states.
+
+ .. versionchanged:: 3.14.7
+ Refreshed after :meth:`~IMAP4.login` and :meth:`~IMAP4.authenticate`.
+
+
.. attribute:: IMAP4.PROTOCOL_VERSION
The most recent supported protocol in the ``CAPABILITY`` response from the
diff --git a/Doc/library/test.rst b/Doc/library/test.rst
index 72ffc2a0b760ead..765811eba32fd40 100644
--- a/Doc/library/test.rst
+++ b/Doc/library/test.rst
@@ -232,9 +232,10 @@ The :mod:`!test.support` module defines the following constants:
.. data:: verbose
- ``True`` when verbose output is enabled. Should be checked when more
- detailed information is desired about a running test. *verbose* is set by
- :mod:`test.regrtest`.
+ How verbose the output is: the number of :option:`!-v` options which
+ :mod:`test.regrtest` was run with, and therefore ``0`` when verbose output
+ is not enabled. Should be checked when more detailed information is
+ desired about a running test.
.. data:: is_jython
diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst
index 7afcdb368a3562e..e1bc32fb79c2eb1 100644
--- a/Doc/library/unittest.rst
+++ b/Doc/library/unittest.rst
@@ -180,6 +180,9 @@ You can run tests with more detail (higher verbosity) by passing in the -v flag:
python -m unittest -v test_module
+Repeat it for even more detail: ``-vv`` reports also the individual examples
+of a :mod:`doctest`.
+
When executed without arguments :ref:`unittest-test-discovery` is started::
python -m unittest
@@ -291,7 +294,11 @@ The ``discover`` sub-command has the following options:
.. option:: -v, --verbose
- Verbose output
+ Verbose output. May be repeated: ``-vv`` reports also the individual
+ examples of a :mod:`doctest`.
+
+ .. versionchanged:: next
+ The option can be repeated.
.. option:: -s, --start-directory directory
@@ -2149,6 +2156,15 @@ Loading and running tests
.. versionadded:: 3.5
+ .. attribute:: verbosity
+
+ The level of details which the test runner reports: ``0`` -- quiet,
+ ``1`` -- progress dots, ``2`` -- test names, ``3`` -- also the
+ individual examples of a :mod:`doctest`. A test runner is expected to
+ set it to its own verbosity.
+
+ .. versionadded:: next
+
.. method:: wasSuccessful()
Return ``True`` if all tests run so far have passed, otherwise returns
diff --git a/Lib/doctest.py b/Lib/doctest.py
index d5541ab41f38d4f..a8f08868cf4957e 100644
--- a/Lib/doctest.py
+++ b/Lib/doctest.py
@@ -1201,6 +1201,18 @@ def _find_lineno(self, obj, source_lines):
## 5. DocTest Runner
######################################################################
+def _make_output_function(stream):
+ """Return a function writing to *stream*, whatever it can encode."""
+ encoding = getattr(stream, 'encoding', None)
+ if encoding is None or encoding.lower() == 'utf-8':
+ return stream.write
+ def out(s):
+ # Use backslashreplace error handling on write
+ s = str(s.encode(encoding, 'backslashreplace'), encoding)
+ stream.write(s)
+ return out
+
+
class DocTestRunner:
"""
A class used to run DocTest test cases, and accumulate statistics.
@@ -1561,14 +1573,7 @@ def run(self, test, compileflags=None, out=None, clear_globs=True):
save_stdout = sys.stdout
if out is None:
- encoding = save_stdout.encoding
- if encoding is None or encoding.lower() == 'utf-8':
- out = save_stdout.write
- else:
- # Use backslashreplace error handling on write
- def out(s):
- s = str(s.encode(encoding, 'backslashreplace'), encoding)
- save_stdout.write(s)
+ out = _make_output_function(save_stdout)
sys.stdout = self._fakeout
# Patch pdb.set_trace to restore sys.stdout during interactive
@@ -2322,6 +2327,9 @@ def report_skip(self, out, test, example):
unittest.case._addSkip(self._test_result, self._subTest(), '')
def report_success(self, out, test, example, got):
+ # Report "ok" if verbose, to close what report_start() opened. A
+ # failed or skipped example is reported by the test result instead.
+ super().report_success(out, test, example, got)
self._test_result.addSubTest(self._test_case, self._subTest(), None)
def report_unexpected_exception(self, out, test, example, exc_info):
@@ -2401,10 +2409,23 @@ def runTest(self):
if getattr(result, 'failfast', False):
optionflags |= FAIL_FAST
+ # Report every example only if the test runner is asked for more than
+ # the test names it reports at verbosity 2. Write them to its stream,
+ # so that they are not swallowed by result.buffer.
+ verbose = getattr(result, 'verbosity', 1) >= 3
+ stream = getattr(result, 'stream', None)
+ out = None
+ if verbose and stream is not None:
+ out = _make_output_function(stream)
+ if test.examples and not getattr(result, '_newline', True):
+ # End the line which startTest() left open.
+ out('\n')
+ result._newline = True
+
runner = _DocTestCaseRunner(optionflags=optionflags,
- checker=self._dt_checker, verbose=False,
+ checker=self._dt_checker, verbose=verbose,
test_case=self, test_result=result)
- results = runner.run(test, clear_globs=False)
+ results = runner.run(test, out=out, clear_globs=False)
if results.skipped == results.attempted:
raise unittest.SkipTest("all examples were skipped")
diff --git a/Lib/imaplib.py b/Lib/imaplib.py
index 24d3a27f21d2d19..977d6bdb1c3d86b 100644
--- a/Lib/imaplib.py
+++ b/Lib/imaplib.py
@@ -366,7 +366,8 @@ def _connect(self):
self._encoding, 'replace')
raise self.error('invalid greeting: ' + greeting)
- self._refresh_capabilities()
+ # The greeting is not a response to a command.
+ self._refresh_capabilities(consume=True)
if __debug__:
if self.debug >= 3:
self._mesg('CAPABILITIES: %r' % (self.capabilities,))
@@ -1509,10 +1510,14 @@ def _get_capabilities(self):
self.capabilities = tuple(dat.split())
- def _refresh_capabilities(self):
+ def _refresh_capabilities(self, consume=False):
# Use a CAPABILITY response sent by the server, or ask for it.
+ # Unless it is consumed, the response can still be read with
+ # response('CAPABILITY').
if 'CAPABILITY' in self.untagged_responses:
- dat = self.untagged_responses.pop('CAPABILITY')[-1]
+ dat = self.untagged_responses['CAPABILITY'][-1]
+ if consume:
+ del self.untagged_responses['CAPABILITY']
self.capabilities = tuple(str(dat, self._encoding).upper().split())
else:
self._get_capabilities()
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 3f8991c79652d3f..c52469e63861a22 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -3464,6 +3464,10 @@ def _main():
import argparse
import importlib
+ # The printed text can contain characters unencodable in the encoding
+ # of stdout, e.g. undecodable bytes of a file name.
+ sys.stdout.reconfigure(errors='backslashreplace')
+
parser = argparse.ArgumentParser()
parser.add_argument(
'object',
diff --git a/Lib/multiprocessing/pool.py b/Lib/multiprocessing/pool.py
index f50bcbe4451bea4..ef9460ac0aa6817 100644
--- a/Lib/multiprocessing/pool.py
+++ b/Lib/multiprocessing/pool.py
@@ -763,6 +763,16 @@ def __enter__(self):
def __exit__(self, exc_type, exc_val, exc_tb):
self.terminate()
+def _chain_context(exc, context):
+ 'Set context as the context of exc, avoiding a cycle.'
+ seen = {id(context)}
+ while exc is not None and id(exc) not in seen:
+ seen.add(id(exc))
+ if exc.__context__ is None:
+ exc.__context__ = context
+ return
+ exc = exc.__context__
+
#
# Class whose instances are returned by `Pool.apply_async()`
#
@@ -800,13 +810,25 @@ def get(self, timeout=None):
def _set(self, i, obj):
self._success, self._value = obj
- if self._callback and self._success:
- self._callback(self._value)
- if self._error_callback and not self._success:
- self._error_callback(self._value)
- self._event.set()
- del self._cache[self._job]
- self._pool = None
+ try:
+ if self._success:
+ if self._callback:
+ self._callback(self._value)
+ else:
+ if self._error_callback:
+ self._error_callback(self._value)
+ except BaseException as exc:
+ # A failed callback becomes the result of the job. If it
+ # propagated, it would kill the result handler thread.
+ if not self._success:
+ # do not lose the original error
+ _chain_context(exc, self._value)
+ self._success = False
+ self._value = exc
+ finally:
+ self._event.set()
+ del self._cache[self._job]
+ self._pool = None
__class_getitem__ = classmethod(types.GenericAlias)
@@ -837,11 +859,16 @@ def _set(self, i, success_result):
if success and self._success:
self._value[i*self._chunksize:(i+1)*self._chunksize] = result
if self._number_left == 0:
- if self._callback:
- self._callback(self._value)
- del self._cache[self._job]
- self._event.set()
- self._pool = None
+ try:
+ if self._callback:
+ self._callback(self._value)
+ except BaseException as exc:
+ self._success = False
+ self._value = exc
+ finally:
+ del self._cache[self._job]
+ self._event.set()
+ self._pool = None
else:
if not success and self._success:
# only store first exception
@@ -849,11 +876,16 @@ def _set(self, i, success_result):
self._value = result
if self._number_left == 0:
# only consider the result ready once all jobs are done
- if self._error_callback:
- self._error_callback(self._value)
- del self._cache[self._job]
- self._event.set()
- self._pool = None
+ try:
+ if self._error_callback:
+ self._error_callback(self._value)
+ except BaseException as exc:
+ _chain_context(exc, self._value)
+ self._value = exc
+ finally:
+ del self._cache[self._job]
+ self._event.set()
+ self._pool = None
#
# Class whose instances are returned by `Pool.imap()`
diff --git a/Lib/ntpath.py b/Lib/ntpath.py
index b3c23f0abc2d889..e9e9c4708ee1f1a 100644
--- a/Lib/ntpath.py
+++ b/Lib/ntpath.py
@@ -626,12 +626,23 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1005, 1920, 1921
# Non-strict algorithm is to find as much of the target directory
- # as we can and join the rest.
+ # as we can and join the rest. join() is not used, because the tail
+ # can contain a colon and be mistaken for a drive (gh-102475).
+ if isinstance(path, bytes):
+ sep = b'\\'
+ else:
+ sep = '\\'
+
+ def join(path, tail):
+ if path[-1:] == sep or not tail:
+ return path + tail
+ return path + sep + tail
+
tail = path[:0]
while path:
try:
path = _getfinalpathname(path)
- return join(path, tail) if tail else path
+ return join(path, tail)
except ignored_error as ex:
if ex.winerror not in allowed_winerror:
raise
@@ -642,7 +653,7 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
new_path = _readlink_deep(path,
ignored_error=ignored_error)
if new_path != path:
- return join(new_path, tail) if tail else new_path
+ return join(new_path, tail)
except ignored_error:
# If we fail to readlink(), let's keep traversing
pass
@@ -657,7 +668,7 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
path, name = split(path)
if path and not name:
return path + tail
- tail = join(name, tail) if tail else name
+ tail = join(name, tail)
return tail
def realpath(path, /, *, strict=False):
@@ -666,7 +677,7 @@ def realpath(path, /, *, strict=False):
prefix = b'\\\\?\\'
unc_prefix = b'\\\\?\\UNC\\'
new_unc_prefix = b'\\\\'
- cwd = os.getcwdb()
+ colon_sep = b':\\'
# bpo-38081: Special case for realpath(b'nul')
devnull = b'nul'
if normcase(path) == devnull:
@@ -675,7 +686,7 @@ def realpath(path, /, *, strict=False):
prefix = '\\\\?\\'
unc_prefix = '\\\\?\\UNC\\'
new_unc_prefix = '\\\\'
- cwd = os.getcwd()
+ colon_sep = ':\\'
# bpo-38081: Special case for realpath('nul')
devnull = 'nul'
if normcase(path) == devnull:
@@ -692,7 +703,9 @@ def realpath(path, /, *, strict=False):
ignored_error = OSError
if not had_prefix and not isabs(path):
- path = join(cwd, path)
+ # abspath() is used instead of join(cwd, path), because the path
+ # can be relative to another drive (gh-102475).
+ path = abspath(path)
try:
path = _getfinalpathname(path)
initial_winerror = 0
@@ -718,25 +731,29 @@ def realpath(path, /, *, strict=False):
# strip off that prefix unless it was already provided on the original
# path.
if not had_prefix and path.startswith(prefix):
- # For UNC paths, the prefix will actually be \\?\UNC\
- # Handle that case as well.
+ # For UNC drives, the path starts with \\?\UNC\.
if path.startswith(unc_prefix):
spath = new_unc_prefix + path[len(unc_prefix):]
- else:
+ # For drive-letter drives, the path starts with \\?\:\.
+ elif path.startswith(colon_sep, len(prefix) + 1):
spath = path[len(prefix):]
- # Ensure that the non-prefixed path resolves to the same path
- try:
- if _getfinalpathname(spath) == path:
- path = spath
- except ValueError:
- # Unexpected, as an invalid path should not have gained a prefix
- # at any point, but we ignore this error just in case.
- pass
- except OSError as ex:
- # If the path does not exist and originally did not exist, then
- # strip the prefix anyway.
- if ex.winerror == initial_winerror:
- path = spath
+ # For all others, e.g. volume GUID paths, it cannot be stripped.
+ else:
+ spath = None
+ if spath is not None:
+ # Ensure that the non-prefixed path resolves to the same path
+ try:
+ if _getfinalpathname(spath) == path:
+ path = spath
+ except ValueError:
+ # Unexpected, as an invalid path should not have gained a
+ # prefix at any point, but we ignore this error just in case.
+ pass
+ except OSError as ex:
+ # If the path does not exist and originally did not exist,
+ # then strip the prefix anyway.
+ if ex.winerror == initial_winerror:
+ path = spath
return path
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 3cba08b83af6811..6f3dab24d5bda33 100644
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -71,7 +71,6 @@ class or function within a module or module in a package. If the
import textwrap
import time
import tokenize
-import urllib.parse
import warnings
from annotationlib import Format
from collections import deque
@@ -795,7 +794,8 @@ def docmodule(self, object, name=None, mod=None, *ignored):
head = linkedname
try:
path = inspect.getabsfile(object)
- url = urllib.parse.quote(path)
+ import urllib.request
+ url = urllib.request.pathname2url(path)
filelink = self.filelink(url, path)
except TypeError:
filelink = '(built-in)'
@@ -1790,7 +1790,8 @@ def writedoc(thing, forceload=0):
"""Write HTML documentation to a file in the current directory."""
object, name = resolve(thing, forceload)
page = html.page(describe(object), html.document(object, name))
- with open(name + '.html', 'w', encoding='utf-8') as file:
+ with open(name + '.html', 'w', encoding='utf-8',
+ errors='backslashreplace') as file:
file.write(page)
print('wrote', name + '.html')
@@ -2387,7 +2388,7 @@ def do_GET(self):
self.send_header('Content-Type', '%s; charset=UTF-8' % content_type)
self.end_headers()
self.wfile.write(self.urlhandler(
- self.path, content_type).encode('utf-8'))
+ self.path, content_type).encode('utf-8', 'backslashreplace'))
def log_message(self, *args):
# Don't log messages.
diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py
index e5f618f5f2e84f4..46ed8843fcd0519 100644
--- a/Lib/test/_test_multiprocessing.py
+++ b/Lib/test/_test_multiprocessing.py
@@ -3474,12 +3474,114 @@ def test_resource_warning(self):
pool = None
support.gc_collect()
+class CallbackError(Exception): pass
+
+class CallbackBaseException(BaseException): pass
+
def raising():
raise KeyError("key")
+def raising_map(x):
+ raise KeyError("key")
+
+def reraise(exc):
+ raise exc
+
+def raise_with_context(exc):
+ try:
+ raise ZeroDivisionError
+ except ZeroDivisionError:
+ raise CallbackError('callback failed')
+
def unpickleable_result():
return lambda: 42
+class _TestPoolCallbackErrors(BaseTestCase):
+ ALLOWED_TYPES = ('processes', )
+
+ @staticmethod
+ def _raise(value):
+ raise CallbackError('callback failed')
+
+ @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+ def test_apply_async_callback_raises(self):
+ with multiprocessing.Pool(1) as p:
+ res = p.apply_async(sqr, (7,), callback=self._raise)
+ with self.assertRaises(CallbackError):
+ res.get(support.SHORT_TIMEOUT)
+ # the pool is still usable
+ self.assertEqual(p.apply(sqr, (3,)), 9)
+ self.assertTrue(p._result_handler.is_alive())
+
+ @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+ def test_apply_async_callback_raises_base_exception(self):
+ def raise_base(value):
+ raise CallbackBaseException
+ with multiprocessing.Pool(1) as p:
+ res = p.apply_async(sqr, (7,), callback=raise_base)
+ with self.assertRaises(CallbackBaseException):
+ res.get(support.SHORT_TIMEOUT)
+ # the pool did not hang
+ self.assertEqual(p.apply(sqr, (3,)), 9)
+
+ @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+ def test_apply_async_error_callback_raises(self):
+ with multiprocessing.Pool(1) as p:
+ res = p.apply_async(raising, error_callback=self._raise)
+ with self.assertRaises(CallbackError) as cm:
+ res.get(support.SHORT_TIMEOUT)
+ # the original error is not lost
+ self.assertIsInstance(cm.exception.__context__, KeyError)
+ self.assertEqual(p.apply(sqr, (3,)), 9)
+
+ @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+ def test_apply_async_error_callback_reraises(self):
+ with multiprocessing.Pool(1) as p:
+ res = p.apply_async(raising, error_callback=reraise)
+ with self.assertRaises(KeyError) as cm:
+ res.get(support.SHORT_TIMEOUT)
+ # the error is not its own context
+ self.assertIsNone(cm.exception.__context__)
+ self.assertEqual(p.apply(sqr, (3,)), 9)
+
+ @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+ def test_map_async_error_callback_reraises(self):
+ with multiprocessing.Pool(1) as p:
+ res = p.map_async(raising_map, [0], error_callback=reraise)
+ with self.assertRaises(KeyError) as cm:
+ res.get(support.SHORT_TIMEOUT)
+ self.assertIsNone(cm.exception.__context__)
+ self.assertEqual(p.apply(sqr, (3,)), 9)
+
+ @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+ def test_apply_async_error_callback_raises_with_context(self):
+ # the original error is kept at the end of the context chain
+ with multiprocessing.Pool(1) as p:
+ res = p.apply_async(raising, error_callback=raise_with_context)
+ with self.assertRaises(CallbackError) as cm:
+ res.get(support.SHORT_TIMEOUT)
+ context = cm.exception.__context__
+ self.assertIsInstance(context, ZeroDivisionError)
+ self.assertIsInstance(context.__context__, KeyError)
+ self.assertEqual(p.apply(sqr, (3,)), 9)
+
+ @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+ def test_map_async_callback_raises(self):
+ with multiprocessing.Pool(1) as p:
+ res = p.map_async(sqr, list(range(3)), callback=self._raise)
+ with self.assertRaises(CallbackError):
+ res.get(support.SHORT_TIMEOUT)
+ self.assertEqual(p.apply(sqr, (3,)), 9)
+
+ @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+ def test_map_async_error_callback_raises(self):
+ with multiprocessing.Pool(1) as p:
+ res = p.map_async(raising_map, [0], error_callback=self._raise)
+ with self.assertRaises(CallbackError) as cm:
+ res.get(support.SHORT_TIMEOUT)
+ self.assertIsInstance(cm.exception.__context__, KeyError)
+ self.assertEqual(p.apply(sqr, (3,)), 9)
+
class _TestPoolWorkerErrors(BaseTestCase):
ALLOWED_TYPES = ('processes', )
diff --git a/Lib/test/libregrtest/testresult.py b/Lib/test/libregrtest/testresult.py
index 1820f354572521a..605f1f4e6a89fb6 100644
--- a/Lib/test/libregrtest/testresult.py
+++ b/Lib/test/libregrtest/testresult.py
@@ -16,7 +16,7 @@ class RegressionTestResult(unittest.TextTestResult):
def __init__(self, stream, descriptions, verbosity):
super().__init__(stream=stream, descriptions=descriptions,
- verbosity=2 if verbosity else 0)
+ verbosity=verbosity)
self.buffer = True
if self.USE_XML:
from xml.etree import ElementTree as ET
@@ -150,10 +150,12 @@ def run(self, test):
def get_test_runner_class(verbosity, buffer=False):
if verbosity:
+ # The verbosity of regrtest is one less than the verbosity of
+ # unittest: -v reports the test names, -vv also the doctest examples.
return functools.partial(unittest.TextTestRunner,
resultclass=RegressionTestResult,
buffer=buffer,
- verbosity=verbosity)
+ verbosity=verbosity + 1)
return functools.partial(QuietRegressionTestRunner, buffer=buffer)
def get_test_runner(stream, verbosity, capture_output=False):
diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py
index 555cebeadc60538..2200770b4a45f08 100644
--- a/Lib/test/test_cmd_line.py
+++ b/Lib/test/test_cmd_line.py
@@ -1067,6 +1067,41 @@ def test_python_legacy_windows_stdio(self):
support.skip_on_low_desktop_heap_memory_subprocess(p.returncode)
self.assertEqual(p.returncode, 0)
+ @unittest.skipUnless(support.MS_WINDOWS, 'Test only applicable on Windows')
+ def test_python_legacy_windows_stdio_encoding(self):
+ # gh-86427: In the legacy mode the encoding of the standard streams
+ # is the encoding of the console.
+ import ctypes
+ kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
+ try:
+ fin = open('CONIN$')
+ except OSError:
+ self.skipTest('no console')
+ # We cannot use PIPE, because the standard streams should be
+ # connected to the console. So we use the exit code.
+ code = ("import sys; sys.exit(sys.stdin.encoding != 'cp850' or "
+ "sys.stdout.encoding != 'cp850')")
+ env = os.environ.copy()
+ env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
+ env['PYTHONUTF8'] = '0'
+ env.pop('PYTHONIOENCODING', None)
+ old_cp = kernel32.GetConsoleCP()
+ old_output_cp = kernel32.GetConsoleOutputCP()
+ with fin, open('CONOUT$', 'w') as fout:
+ try:
+ if not kernel32.SetConsoleCP(850):
+ self.skipTest('cannot set the console input code page')
+ if not kernel32.SetConsoleOutputCP(850):
+ self.skipTest('cannot set the console output code page')
+ proc = subprocess.run([sys.executable, '-c', code], env=env,
+ stdin=fin, stdout=fout,
+ stderr=subprocess.DEVNULL)
+ finally:
+ kernel32.SetConsoleCP(old_cp)
+ kernel32.SetConsoleOutputCP(old_output_cp)
+ support.skip_on_low_desktop_heap_memory_subprocess(proc.returncode)
+ self.assertEqual(proc.returncode, 0)
+
@unittest.skipIf("-fsanitize" in sysconfig.get_config_vars().get('PY_CFLAGS', ()),
"PYTHONMALLOCSTATS doesn't work with ASAN")
def test_python_malloc_stats(self):
diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py
index b125693ab0891cd..776ad83ee6d6ce5 100644
--- a/Lib/test/test_doctest/test_doctest.py
+++ b/Lib/test/test_doctest/test_doctest.py
@@ -6,6 +6,7 @@
from test.support import import_helper
import doctest
import functools
+import io
import os
import sys
import importlib
@@ -469,7 +470,7 @@ def basics(): r"""
>>> tests = finder.find(sample_func)
>>> print(tests) # doctest: +ELLIPSIS
- []
+ []
The exact name depends on how test_doctest was invoked, so allow for
leading path components.
@@ -803,6 +804,59 @@ def myfunc():
self.assertEqual((x, y), (2, 3))
+class TestDocTestSuiteVerbosity(unittest.TestCase):
+
+ def run_suite(self, module='test.test_doctest.sample_doctest', **kwargs):
+ """Return what the test runner wrote and what leaked to stdout."""
+ suite = doctest.DocTestSuite(module)
+ stream = io.StringIO()
+ stdout = io.StringIO()
+ with contextlib.redirect_stdout(stdout):
+ unittest.TextTestRunner(stream=stream, **kwargs).run(suite)
+ return stream.getvalue(), stdout.getvalue()
+
+ def test_quiet(self):
+ for verbosity in range(3):
+ with self.subTest(verbosity=verbosity):
+ output, stdout = self.run_suite(verbosity=verbosity)
+ self.assertNotIn('Trying:', output)
+ self.assertNotIn('Expecting:', output)
+ self.assertEqual(stdout, '')
+
+ def test_verbose(self):
+ output, stdout = self.run_suite(verbosity=3)
+ self.assertIn('Trying:\n 2+2\n', output)
+ self.assertIn('Expecting:\n 4\n', output)
+ self.assertIn('\nok\n', output)
+ # Reported to the stream of the test runner, not to the stdout.
+ self.assertEqual(stdout, '')
+
+ def test_verbose_buffered(self):
+ # result.buffer replaces sys.stdout, which would swallow the examples.
+ output, stdout = self.run_suite(verbosity=3, buffer=True)
+ self.assertIn('Trying:\n 2+2\n', output)
+ self.assertEqual(stdout, '')
+
+ def test_verbose_failure_not_duplicated(self):
+ module = 'test.test_doctest.sample_doctest_errors'
+ quiet, _ = self.run_suite(module, verbosity=2)
+ verbose, _ = self.run_suite(module, verbosity=3)
+ self.assertIn('Trying:', verbose)
+ self.assertNotIn('Trying:', quiet)
+ # Reporting the examples does not report the failures once more.
+ self.assertEqual(verbose.count('Failed example:'),
+ quiet.count('Failed example:'))
+ self.assertGreater(quiet.count('Failed example:'), 0)
+
+ def test_plain_result(self):
+ # A result which is not from a text test runner has no stream.
+ suite = doctest.DocTestSuite('test.test_doctest.sample_doctest')
+ stdout = io.StringIO()
+ with contextlib.redirect_stdout(stdout):
+ suite.run(unittest.TestResult())
+ self.assertEqual(stdout.getvalue(), '')
+
+
class TestDocTestFinder(unittest.TestCase):
def test_issue35753(self):
diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py
index d97da398803681d..da63da54e8ef733 100644
--- a/Lib/test/test_imaplib.py
+++ b/Lib/test/test_imaplib.py
@@ -1069,6 +1069,8 @@ def cmd_ENABLE(self, tag, args):
client.login('user', 'pass')
self.assertIn('ENABLE', client.capabilities)
self.assertIn('UTF8=ACCEPT', client.capabilities)
+ self.assertEqual(client.response('CAPABILITY'),
+ ('CAPABILITY', [b'IMAP4rev1 ENABLE UTF8=ACCEPT']))
typ, _ = client.enable('UTF8=ACCEPT')
self.assertEqual(typ, 'OK')
@@ -1087,6 +1089,8 @@ def cmd_AUTHENTICATE(self, tag, args):
self.assertNotIn('ENABLE', client.capabilities)
client.authenticate('MYAUTH', lambda x: b'fake')
self.assertIn('ENABLE', client.capabilities)
+ self.assertEqual(client.response('CAPABILITY'),
+ ('CAPABILITY', [b'IMAP4rev1 ENABLE']))
def test_greeting_capabilities(self):
# Capabilities advertised in the greeting are used directly,
@@ -1100,6 +1104,8 @@ def cmd_CAPABILITY(self, tag, args):
client, server = self._setup(GreetingHandler)
self.assertEqual(client.capabilities, ('IMAP4REV1', 'ENABLE'))
self.assertFalse(getattr(server, 'capability_queried', False))
+ # The greeting is not a response to a command, so it is consumed.
+ self.assertEqual(client.response('CAPABILITY'), ('CAPABILITY', [None]))
def test_login_requery_capabilities(self):
# If the server does not advertise capabilities after login,
diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py
index c68c643cb97fb45..8930f6343ac299d 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -40,7 +40,7 @@
from test.support import MISSING_C_DOCSTRINGS, ALWAYS_EQ
from test.support import run_no_yield_async_fn, EqualToForwardRef
from test.support.import_helper import DirsOnSysPath, ready_to_import
-from test.support.os_helper import TESTFN, temp_cwd
+from test.support.os_helper import TESTFN, TESTFN_UNDECODABLE, temp_cwd
from test.support.script_helper import assert_python_ok, assert_python_failure, kill_python
from test.support import has_subprocess_support
from test import support
@@ -6619,6 +6619,25 @@ def test_error_data(self):
lines = err.decode().splitlines()
self.assertEqual(lines, [self.NO_SOURCE_TARGET_ERROR])
+ @unittest.skipUnless(TESTFN_UNDECODABLE,
+ 'requires undecodable file names')
+ def test_details_undecodable_path(self):
+ # gh-69370: the path of the module is not encodable in the encoding
+ # of stdout.
+ with temp_cwd() as test_dir:
+ subdir = os.path.join(os.fsencode(test_dir), TESTFN_UNDECODABLE)
+ try:
+ os.mkdir(subdir)
+ except OSError:
+ self.skipTest('undecodable paths are not supported')
+ with open(os.path.join(subdir, b'undecodable_mod.py'), 'w') as f:
+ f.write('"""Module docstring."""\n')
+ rc, out, err = assert_python_ok('-X', 'utf8=0', '-m', 'inspect',
+ '--details', 'undecodable_mod',
+ PYTHONPATH=os.fsdecode(subdir))
+ self.assertIn(b'Target: undecodable_mod', out)
+ self.assertEqual(err, b'')
+
def test_details_option_with_package(self):
module_name = 'unittest'
module = importlib.import_module(module_name)
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
index 936332bf94ffe77..a4de2c78f0bf0c2 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -1535,6 +1535,62 @@ def test_isjunction(self):
self.assertFalse(ntpath.isjunction('tmpdir'))
self.assertPathEqual(ntpath.realpath('testjunc'), ntpath.realpath('tmpdir'))
+ @unittest.skipIf(sys.platform != 'win32', "Can only test on win32.")
+ def test_realpath_drive_like_names(self):
+ # gh-102475: the unresolved tail is appended, not joined, so a name
+ # which looks like a drive does not reset the path.
+ drive = ntpath.splitroot(os.getcwd())[0]
+ for path, expected in [
+ ('C:/spam:eggs', 'C:\\spam:eggs'),
+ ('C:/nonexistent/spam:eggs', 'C:\\nonexistent\\spam:eggs'),
+ ('C:/spam:eggs/ham', 'C:\\spam:eggs\\ham'),
+ ('C:/nonexistent/spam:eggs/ham', 'C:\\nonexistent\\spam:eggs\\ham'),
+ ]:
+ with self.subTest(path=path):
+ self.assertEqual(ntpath.realpath(path), expected)
+ self.assertEqual(ntpath.realpath(os.fsencode(path)),
+ os.fsencode(expected))
+
+ @unittest.skipIf(sys.platform != 'win32', "Can only test on win32.")
+ def test_realpath_drive_relative(self):
+ # gh-102475: the working directory of a drive which does not exist
+ # is its root directory.
+ for drive in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
+ if not ntpath.exists(drive + ':'):
+ break
+ else:
+ raise unittest.SkipTest('all drives exist')
+ self.assertEqual(ntpath.realpath(drive + ':spam'),
+ drive + ':\\spam')
+ self.assertEqual(ntpath.realpath(drive + ':'), drive + ':\\')
+
+ @unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
+ def test_realpath_volume_guid_path(self):
+ # gh-89760: the \\?\ prefix cannot be stripped from a volume GUID path.
+ # Find a volume which is not mounted as a drive.
+ for volume in os.listvolumes():
+ if not os.listmounts(volume):
+ break
+ else:
+ raise unittest.SkipTest('no volume without a mount point')
+
+ with os_helper.temp_dir() as d:
+ with os_helper.change_cwd(d):
+ # _winapi.CreateJunction() adds the \\??\\ prefix to a path
+ # which already has a prefix.
+ try:
+ subprocess.run(['cmd', '/c', 'mklink', '/j',
+ 'testjunc', volume],
+ check=True, capture_output=True)
+ except (OSError, subprocess.CalledProcessError):
+ raise unittest.SkipTest('creating the test junction failed')
+
+ for path in 'testjunc', 'testjunc/spam', 'testjunc/spam/eggs':
+ with self.subTest(path=path):
+ realpath = ntpath.realpath(path)
+ self.assertStartsWith(realpath, '\\\\?\\Volume{')
+ self.assertTrue(ntpath.isabs(realpath), realpath)
+
def test_isfile_invalid_paths(self):
isfile = ntpath.isfile
self.assertIs(isfile('/tmp\udfffabcds'), False)
diff --git a/Lib/test/test_os/test_windows.py b/Lib/test/test_os/test_windows.py
index b21dd8a4dca6609..3fb8454cddfde93 100644
--- a/Lib/test/test_os/test_windows.py
+++ b/Lib/test/test_os/test_windows.py
@@ -608,5 +608,43 @@ def cleanup():
self.assertGreaterEqual(stat1.st_atime, stat2.st_atime)
+class Win32DeviceEncodingTests(unittest.TestCase):
+ # gh-87587: any console file descriptor is supported, not only 0, 1 and 2,
+ # and other character devices are not consoles.
+
+ @staticmethod
+ def expected_encoding(cp):
+ return 'utf-8' if cp == 65001 else 'cp%d' % cp
+
+ def test_console(self):
+ import ctypes
+ kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
+ try:
+ fin = open('CONIN$')
+ except OSError:
+ self.skipTest('no console')
+ with fin:
+ self.assertEqual(os.device_encoding(fin.fileno()),
+ self.expected_encoding(kernel32.GetConsoleCP()))
+ with open('CONOUT$', 'w') as fout:
+ self.assertEqual(
+ os.device_encoding(fout.fileno()),
+ self.expected_encoding(kernel32.GetConsoleOutputCP()))
+
+ def test_not_a_console(self):
+ with open('NUL', 'w') as f:
+ self.assertTrue(os.isatty(f.fileno()))
+ self.assertIsNone(os.device_encoding(f.fileno()))
+ # Not a console even if it is a standard file descriptor.
+ saved = os.dup(1)
+ try:
+ os.dup2(f.fileno(), 1)
+ encoding = os.device_encoding(1)
+ finally:
+ os.dup2(saved, 1)
+ os.close(saved)
+ self.assertIsNone(encoding)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py
index 34d30f54e9c9c7f..006fbc8b6968f3b 100644
--- a/Lib/test/test_pydoc/test_pydoc.py
+++ b/Lib/test/test_pydoc/test_pydoc.py
@@ -19,6 +19,7 @@
import unittest
import unittest.mock
import urllib.parse
+import urllib.request
import xml.etree
import xml.etree.ElementTree
import textwrap
@@ -429,7 +430,7 @@ def test_html_doc(self):
expected_lines = [line.strip() for line in expected_lines if line]
self.assertEqual(text_lines, expected_lines)
mod_file = inspect.getabsfile(pydoc_mod)
- mod_url = urllib.parse.quote(mod_file)
+ mod_url = urllib.request.pathname2url(mod_file)
self.assertIn(mod_url, result)
self.assertIn(mod_file, result)
self.assertIn(doc_loc, result)
@@ -1024,6 +1025,31 @@ def test_synopsis_sourceless_empty_doc(self):
synopsis_cached = pydoc.synopsis(cached_path, {})
self.assertIsNone(synopsis_cached)
+ @unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
+ 'requires undecodable file names')
+ def test_html_doc_undecodable_path(self):
+ # gh-69371: the path of the module is not encodable in UTF-8.
+ with os_helper.temp_cwd() as test_dir:
+ subdir = os.path.join(os.fsencode(test_dir),
+ os_helper.TESTFN_UNDECODABLE)
+ try:
+ os.mkdir(subdir)
+ except OSError:
+ self.skipTest('undecodable paths are not supported')
+ with open(os.path.join(subdir, b'undecodable_mod.py'), 'w') as f:
+ f.write('"""Module docstring."""\n')
+ with import_helper.DirsOnSysPath(os.fsdecode(subdir)):
+ mod = import_helper.import_fresh_module('undecodable_mod')
+ doc = pydoc.HTMLDoc().docmodule(mod)
+ with captured_stdout():
+ pydoc.writedoc(mod)
+ # The link contains the percent-encoded path...
+ path = inspect.getabsfile(mod)
+ self.assertIn(urllib.request.pathname2url(path), doc)
+ # ...and the page can be written and served as UTF-8.
+ with open('undecodable_mod.html', encoding='utf-8') as f:
+ self.assertIn('undecodable_mod', f.read())
+
def test_splitdoc_with_description(self):
example_string = "I Am A Doc\n\n\nHere is my description"
self.assertEqual(pydoc.splitdoc(example_string),
diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py
index f4baa965b3f632b..c966f8659e2abb0 100644
--- a/Lib/test/test_regrtest.py
+++ b/Lib/test/test_regrtest.py
@@ -2184,6 +2184,18 @@ def load_tests(loader, tests, pattern):
failed=[testname],
parallel=True,
stats=TestStats(1, 2, 1))
+ # A single -v reports the test names, not the examples.
+ self.assertNotIn('Trying:', output)
+
+ # -vv reports every example, without changing what is run.
+ output = self.run_tests("--fail-env-changed", "-vv", "-j1", testname,
+ exitcode=EXITCODE_BAD_TEST)
+ self.check_executed_tests(output, [testname],
+ failed=[testname],
+ parallel=True,
+ stats=TestStats(1, 2, 1))
+ self.assertIn('Trying:\n 1 + 1\n', output)
+ self.assertIn('Expecting:\n 2\n', output)
def _check_random_seed(self, run_workers: bool):
# gh-109276: When -r/--randomize is used, random.seed() is called
diff --git a/Lib/test/test_unittest/test_discovery.py b/Lib/test/test_unittest/test_discovery.py
index 38c9779daaf87d4..da184bd12be8d17 100644
--- a/Lib/test/test_unittest/test_discovery.py
+++ b/Lib/test/test_unittest/test_discovery.py
@@ -635,7 +635,7 @@ def test_command_line_handling_discover_by_default_with_options(self):
program._do_discovery = args.append
program.parseArgs(['something', '-v', '-b', '-v', '-c', '-f'])
self.assertEqual(args, [[]])
- self.assertEqual(program.verbosity, 2)
+ self.assertEqual(program.verbosity, 3) # -v is passed twice
self.assertIs(program.buffer, True)
self.assertIs(program.catchbreak, True)
self.assertIs(program.failfast, True)
diff --git a/Lib/test/test_unittest/test_program.py b/Lib/test/test_unittest/test_program.py
index 8ed92373e5e984a..a9a73f0d057ee81 100644
--- a/Lib/test/test_unittest/test_program.py
+++ b/Lib/test/test_unittest/test_program.py
@@ -267,6 +267,32 @@ def testVerbosity(self):
program.parseArgs([None, opt])
self.assertEqual(program.verbosity, 2)
+ # -v can be repeated to ask for more details.
+ for args, verbosity in (
+ (['-vv'], 3),
+ (['-v', '-v'], 3),
+ (['--verbose', '--verbose'], 3),
+ (['-vvv'], 4),
+ # -q overrides any number of -v.
+ (['-v', '-q'], 0),
+ ):
+ with self.subTest(args=args):
+ program.verbosity = 1
+ program.parseArgs([None, *args])
+ self.assertEqual(program.verbosity, verbosity)
+
+ def testVerbosityCountedOnce(self):
+ # "python -m unittest -v" falls back to test discovery, which parses
+ # arguments again: -v must not be counted twice.
+ program = self.program
+ program.verbosity = 1
+ program.parseArgs([None, '-v'])
+ self.assertEqual(program.verbosity, 2)
+
+ program.verbosity = 1
+ program.parseArgs([None, 'discover', '-vv'])
+ self.assertEqual(program.verbosity, 3)
+
def testBufferCatchFailfast(self):
program = self.program
for arg, attr in (('buffer', 'buffer'), ('failfast', 'failfast'),
diff --git a/Lib/test/test_unittest/test_result.py b/Lib/test/test_unittest/test_result.py
index 3f44e617303f81d..cb591c23fc964fd 100644
--- a/Lib/test/test_unittest/test_result.py
+++ b/Lib/test/test_unittest/test_result.py
@@ -54,6 +54,13 @@ def test_init(self):
self.assertEqual(result.shouldStop, False)
self.assertIsNone(result._stdout_buffer)
self.assertIsNone(result._stderr_buffer)
+ self.assertEqual(result.verbosity, 1)
+
+ def test_init_verbosity(self):
+ for verbosity in range(4):
+ with self.subTest(verbosity=verbosity):
+ result = unittest.TestResult(None, None, verbosity)
+ self.assertEqual(result.verbosity, verbosity)
# "This method can be called to signal that the set of tests being
# run should be aborted by setting the TestResult's shouldStop
diff --git a/Lib/test/test_unittest/test_runner.py b/Lib/test/test_unittest/test_runner.py
index a47e2ebb59da028..195d5e928030927 100644
--- a/Lib/test/test_unittest/test_runner.py
+++ b/Lib/test/test_unittest/test_runner.py
@@ -1363,6 +1363,35 @@ def MockResultClass(*args):
expectedresult = (runner.stream, DESCRIPTIONS, VERBOSITY)
self.assertEqual(runner._makeResult(), expectedresult)
+ def test_verbosity_set_on_result(self):
+ class Suite:
+ def __call__(self, result):
+ pass
+
+ for verbosity in range(4):
+ with self.subTest(verbosity=verbosity):
+ runner = unittest.TextTestRunner(io.StringIO(),
+ verbosity=verbosity)
+ result = runner.run(Suite())
+ self.assertEqual(result.verbosity, verbosity)
+
+ def test_verbosity_set_on_filtering_result(self):
+ # A result class is free to filter the verbosity which its
+ # constructor gets, as test.libregrtest does.
+ class FilteringResult(unittest.TextTestResult):
+ def __init__(self, stream, descriptions, verbosity):
+ super().__init__(stream, descriptions,
+ 2 if verbosity else 0)
+
+ class Suite:
+ def __call__(self, result):
+ pass
+
+ runner = unittest.TextTestRunner(io.StringIO(), verbosity=3,
+ resultclass=FilteringResult)
+ result = runner.run(Suite())
+ self.assertEqual(result.verbosity, 3)
+
@support.force_not_colorized
@support.requires_subprocess()
def test_warnings(self):
diff --git a/Lib/unittest/main.py b/Lib/unittest/main.py
index 6eeebf9657a3c7b..850c825a7366579 100644
--- a/Lib/unittest/main.py
+++ b/Lib/unittest/main.py
@@ -161,9 +161,12 @@ def _initArgParsers(self):
def _getParentArgParser(self):
parser = argparse.ArgumentParser(add_help=False)
+ # Counted, not a constant: the namespace is the TestProgram, whose
+ # verbosity is already 1, so -v still gives 2 and -vv gives 3.
parser.add_argument('-v', '--verbose', dest='verbosity',
- action='store_const', const=2,
- help='Verbose output')
+ action='count', default=1,
+ help='Verbose output, twice to also report '
+ 'the examples of a doctest')
parser.add_argument('-q', '--quiet', dest='verbosity',
action='store_const', const=0,
help='Quiet output')
diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py
index b8ea396db6772ed..a787686a5bea4ce 100644
--- a/Lib/unittest/result.py
+++ b/Lib/unittest/result.py
@@ -37,6 +37,9 @@ class TestResult(object):
_moduleSetUpFailed = False
def __init__(self, stream=None, descriptions=None, verbosity=None):
self.failfast = False
+ # How much the test runner reports: 0 -- quiet, 1 -- progress dots,
+ # 2 -- test names, 3 -- also the examples of a doctest.
+ self.verbosity = 1 if verbosity is None else verbosity
self.failures = []
self.errors = []
self.testsRun = 0
diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py
index 893fcba968c3ef8..f19d7b6e446d2ac 100644
--- a/Lib/unittest/runner.py
+++ b/Lib/unittest/runner.py
@@ -244,6 +244,9 @@ def run(self, test):
result.failfast = self.failfast
result.buffer = self.buffer
result.tb_locals = self.tb_locals
+ # Not left to _makeResult(): a result class is free to filter the
+ # verbosity which its constructor gets.
+ result.verbosity = self.verbosity
with warnings.catch_warnings():
if self.warnings:
# if self.warnings is set, use it to filter all the warnings
diff --git a/Misc/NEWS.d/next/Library/2026-08-05-16-40-00.gh-issue-69371.pyDocU.rst b/Misc/NEWS.d/next/Library/2026-08-05-16-40-00.gh-issue-69371.pyDocU.rst
new file mode 100644
index 000000000000000..4d8f866366f5beb
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-05-16-40-00.gh-issue-69371.pyDocU.rst
@@ -0,0 +1,5 @@
+Fix :mod:`pydoc` for modules whose path contains undecodable bytes.
+:func:`!pydoc.writedoc` and the pydoc HTTP server no longer fail with
+:exc:`UnicodeEncodeError`: the file URL is now percent-encoded using the
+filesystem encoding, and characters unencodable in the generated HTML page
+are escaped with backslashes.
diff --git a/Misc/NEWS.d/next/Library/2026-08-05-17-30-00.gh-issue-69370.inspCLI.rst b/Misc/NEWS.d/next/Library/2026-08-05-17-30-00.gh-issue-69370.inspCLI.rst
new file mode 100644
index 000000000000000..27c97f9bdf07b48
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-05-17-30-00.gh-issue-69370.inspCLI.rst
@@ -0,0 +1,4 @@
+:program:`python -m inspect --details` no longer fails with
+:exc:`UnicodeEncodeError` if the path of the module contains characters
+unencodable in the encoding of :data:`sys.stdout`. Such characters are now
+escaped with backslashes.
diff --git a/Misc/NEWS.d/next/Library/2026-08-08-09-14-22.gh-issue-108885.C4ktfw.rst b/Misc/NEWS.d/next/Library/2026-08-08-09-14-22.gh-issue-108885.C4ktfw.rst
new file mode 100644
index 000000000000000..e504f9b7d684f88
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-08-09-14-22.gh-issue-108885.C4ktfw.rst
@@ -0,0 +1,6 @@
+Doctests run by the :mod:`unittest` test runner now report every example, as
+in verbose mode, if the runner reports more than the test names, i.e. its
+verbosity is 3 or higher. The :option:`!-v` option of :mod:`unittest` can now
+be repeated, so ``python -m unittest -vv`` asks for this. Added also the
+:attr:`~unittest.TestResult.verbosity` attribute of
+:class:`unittest.TestResult`.
diff --git a/Misc/NEWS.d/next/Library/2026-08-09-00-30-00.gh-issue-89760.rpvol.rst b/Misc/NEWS.d/next/Library/2026-08-09-00-30-00.gh-issue-89760.rpvol.rst
new file mode 100644
index 000000000000000..9218ecfe9ce62e3
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-09-00-30-00.gh-issue-89760.rpvol.rst
@@ -0,0 +1,2 @@
+Fix :func:`os.path.realpath` on Windows: the ``\\?\`` prefix is no longer
+stripped from a volume GUID path, which made the result invalid.
diff --git a/Misc/NEWS.d/next/Library/2026-08-09-01-30-00.gh-issue-102475.rpjoin.rst b/Misc/NEWS.d/next/Library/2026-08-09-01-30-00.gh-issue-102475.rpjoin.rst
new file mode 100644
index 000000000000000..fb647720791dac0
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-09-01-30-00.gh-issue-102475.rpjoin.rst
@@ -0,0 +1,4 @@
+Fix :func:`os.path.realpath` on Windows: the unresolved part of the path is
+now appended, not joined, so a file name which looks like a drive (e.g.
+``spam:eggs``) no longer discards the resolved part. A path relative to
+another drive is now resolved against the root directory of that drive.
diff --git a/Misc/NEWS.d/next/Library/2026-08-14-10-49-45.gh-issue-83371.Km4Rt7.rst b/Misc/NEWS.d/next/Library/2026-08-14-10-49-45.gh-issue-83371.Km4Rt7.rst
new file mode 100644
index 000000000000000..ca30859c4a49c5c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-14-10-49-45.gh-issue-83371.Km4Rt7.rst
@@ -0,0 +1,6 @@
+Fix a deadlock in :class:`multiprocessing.pool.Pool` when *callback* or
+*error_callback* raises an exception.
+It killed the thread which handles results, so that the pool hung forever.
+The exception is now the result of the job,
+as an error raised while iterating the input,
+and is raised by :meth:`!AsyncResult.get`.
diff --git a/Misc/NEWS.d/next/Library/2026-08-14-14-10-00.gh-issue-155775.Kq3vTx.rst b/Misc/NEWS.d/next/Library/2026-08-14-14-10-00.gh-issue-155775.Kq3vTx.rst
new file mode 100644
index 000000000000000..309421ae08240f8
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-14-14-10-00.gh-issue-155775.Kq3vTx.rst
@@ -0,0 +1,4 @@
+Fix a regression in :mod:`imaplib` introduced in the fix for :gh:`63121`:
+refreshing the capabilities after a successful :meth:`~imaplib.IMAP4.login`
+or :meth:`~imaplib.IMAP4.authenticate` consumed the ``CAPABILITY`` response,
+so it could no longer be read with :meth:`~imaplib.IMAP4.response`.
diff --git a/Misc/NEWS.d/next/Tests/2026-08-08-09-15-08.gh-issue-108885.Qm3vLp.rst b/Misc/NEWS.d/next/Tests/2026-08-08-09-15-08.gh-issue-108885.Qm3vLp.rst
new file mode 100644
index 000000000000000..3cdac196dd35c0c
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2026-08-08-09-15-08.gh-issue-108885.Qm3vLp.rst
@@ -0,0 +1,2 @@
+Running the Python test suite with ``-vv`` now reports every example of a
+doctest. A single ``-v`` reports the test names, as before.
diff --git a/Misc/NEWS.d/next/Windows/2026-08-09-04-00-00.gh-issue-87587.devenc.rst b/Misc/NEWS.d/next/Windows/2026-08-09-04-00-00.gh-issue-87587.devenc.rst
new file mode 100644
index 000000000000000..06bba3e1f3d8558
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2026-08-09-04-00-00.gh-issue-87587.devenc.rst
@@ -0,0 +1,4 @@
+:func:`os.device_encoding` on Windows now returns the code page of any
+console file descriptor, not only 0, 1 and 2, and returns ``None`` for other
+character devices like ``NUL``. The UTF-8 code page is now reported as
+``"utf-8"`` instead of ``"cp65001"``.
diff --git a/Misc/NEWS.d/next/Windows/2026-08-09-07-00-00.gh-issue-86427.consolecp.rst b/Misc/NEWS.d/next/Windows/2026-08-09-07-00-00.gh-issue-86427.consolecp.rst
new file mode 100644
index 000000000000000..1cbf188c12e4070
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2026-08-09-07-00-00.gh-issue-86427.consolecp.rst
@@ -0,0 +1,3 @@
+Fix the encoding of the standard streams in the legacy Windows stdio mode
+(:envvar:`PYTHONLEGACYWINDOWSSTDIO`). It is now the code page of the
+console, as in Python 3.7, not the ANSI code page.
diff --git a/Misc/NEWS.d/next/Windows/2026-08-09-14-20-00.gh-issue-72353.Vw7Kq2.rst b/Misc/NEWS.d/next/Windows/2026-08-09-14-20-00.gh-issue-72353.Vw7Kq2.rst
new file mode 100644
index 000000000000000..95c771823ab77f5
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2026-08-09-14-20-00.gh-issue-72353.Vw7Kq2.rst
@@ -0,0 +1,3 @@
+Reading from the console on Windows now continues if the read was cancelled
+by Ctrl+C, but the :const:`~signal.SIGINT` handler did not raise an
+exception. Previously the read ended as if at end of file.
diff --git a/Misc/NEWS.d/next/Windows/2026-08-09-15-40-00.gh-issue-85470.Lm4Xt9.rst b/Misc/NEWS.d/next/Windows/2026-08-09-15-40-00.gh-issue-85470.Lm4Xt9.rst
new file mode 100644
index 000000000000000..1d623c890e960b6
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2026-08-09-15-40-00.gh-issue-85470.Lm4Xt9.rst
@@ -0,0 +1,4 @@
+Importing :mod:`ctypes` on Windows no longer loads ``user32.dll``, which
+made the system treat the process as a GUI process and stop sending logoff
+and shutdown console control events to it. ``ole32.dll``, which pulled
+``user32.dll`` in, is now delay loaded.
diff --git a/Modules/_io/winconsoleio.c b/Modules/_io/winconsoleio.c
index 4cd71094e8f459e..e96c5bd738fe0ad 100644
--- a/Modules/_io/winconsoleio.c
+++ b/Modules/_io/winconsoleio.c
@@ -644,8 +644,8 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
break;
err = 0;
HANDLE hInterruptEvent = _PyOS_SigintEvent();
- if (WaitForSingleObjectEx(hInterruptEvent, 100, FALSE)
- == WAIT_OBJECT_0) {
+ DWORD state = WaitForSingleObjectEx(hInterruptEvent, 100, FALSE);
+ if (state == WAIT_OBJECT_0) {
ResetEvent(hInterruptEvent);
Py_BLOCK_THREADS
sig = PyErr_CheckSignals();
@@ -653,6 +653,13 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
if (sig < 0)
break;
}
+ else if (state != WAIT_TIMEOUT) {
+ err = GetLastError();
+ break;
+ }
+ /* The console cancelled the read and flushed its input buffer,
+ but no exception was raised. Start the read over. */
+ continue;
}
*readlen += n;
diff --git a/PCbuild/_ctypes.vcxproj b/PCbuild/_ctypes.vcxproj
index 63d5fa49cd4e17a..e56d812d78dcd80 100644
--- a/PCbuild/_ctypes.vcxproj
+++ b/PCbuild/_ctypes.vcxproj
@@ -98,6 +98,7 @@
/EXPORT:DllGetClassObject,PRIVATE /EXPORT:DllCanUnloadNow,PRIVATE %(AdditionalOptions)
+ ole32.dll
diff --git a/Parser/myreadline.c b/Parser/myreadline.c
index 457b9598a0e61a5..339f55afa0dcfaa 100644
--- a/Parser/myreadline.c
+++ b/Parser/myreadline.c
@@ -162,8 +162,8 @@ _PyOS_WindowsConsoleReadline(PyThreadState *tstate, HANDLE hStdIn)
goto exit;
err = 0;
HANDLE hInterruptEvent = _PyOS_SigintEvent();
- if (WaitForSingleObjectEx(hInterruptEvent, 100, FALSE)
- == WAIT_OBJECT_0) {
+ DWORD state = WaitForSingleObjectEx(hInterruptEvent, 100, FALSE);
+ if (state == WAIT_OBJECT_0) {
ResetEvent(hInterruptEvent);
PyEval_RestoreThread(tstate);
s = PyErr_CheckSignals();
@@ -172,7 +172,13 @@ _PyOS_WindowsConsoleReadline(PyThreadState *tstate, HANDLE hStdIn)
goto exit;
}
}
- break;
+ else if (state != WAIT_TIMEOUT) {
+ err = GetLastError();
+ goto exit;
+ }
+ /* The console cancelled the read and flushed its input buffer,
+ but no exception was raised. Start the read over. */
+ continue;
}
total_read += n_read;
diff --git a/Python/codegen.c b/Python/codegen.c
index 875c963a6078c66..79b84f13e629c76 100644
--- a/Python/codegen.c
+++ b/Python/codegen.c
@@ -700,13 +700,13 @@ codegen_enter_scope(compiler *c, identifier name, int scope_type,
* of 0. This is because RETURN_GENERATOR pushes the generator
before returning. */
location loc = LOCATION(lineno, lineno, -1, -1);
- ADDOP(c, loc, RETURN_GENERATOR);
- ADDOP(c, loc, POP_TOP);
+ ADDOP_IN_SCOPE(c, loc, RETURN_GENERATOR);
+ ADDOP_IN_SCOPE(c, loc, POP_TOP);
}
- ADDOP_I(c, loc, RESUME, RESUME_AT_FUNC_START);
+ ADDOP_I_IN_SCOPE(c, loc, RESUME, RESUME_AT_FUNC_START);
if (scope_type == COMPILE_SCOPE_MODULE) {
- ADDOP(c, loc, ANNOTATIONS_PLACEHOLDER);
+ ADDOP_IN_SCOPE(c, loc, ANNOTATIONS_PLACEHOLDER);
}
return SUCCESS;
}
diff --git a/Python/fileutils.c b/Python/fileutils.c
index 5cbfd8e6ce4fa08..404fec83385e970 100644
--- a/Python/fileutils.c
+++ b/Python/fileutils.c
@@ -78,34 +78,59 @@ get_surrogateescape(_Py_error_handler errors, int *surrogateescape)
PyObject *
_Py_device_encoding(int fd)
{
- int valid;
- Py_BEGIN_ALLOW_THREADS
+#if defined(MS_WINDOWS) && defined(HAVE_WINDOWS_CONSOLE_IO)
+ HANDLE handle;
+ DWORD temp;
+ UINT cp = 0;
+
_Py_BEGIN_SUPPRESS_IPH
- valid = isatty(fd);
+ handle = (HANDLE)_get_osfhandle(fd);
_Py_END_SUPPRESS_IPH
- Py_END_ALLOW_THREADS
- if (!valid)
+ if (handle == INVALID_HANDLE_VALUE) {
Py_RETURN_NONE;
+ }
+
+ Py_BEGIN_ALLOW_THREADS
+ if (GetFileType(handle) == FILE_TYPE_CHAR) {
+ /* GetConsoleMode() only succeeds for a console handle. */
+ if (!GetConsoleMode(handle, &temp)) {
+ /* Assume that access denied implies an output handle. */
+ if (GetLastError() == ERROR_ACCESS_DENIED) {
+ cp = GetConsoleOutputCP();
+ }
+ }
+ else if (GetNumberOfConsoleInputEvents(handle, &temp)) {
+ cp = GetConsoleCP();
+ }
+ else {
+ cp = GetConsoleOutputCP();
+ }
+ }
+ Py_END_ALLOW_THREADS
-#ifdef MS_WINDOWS
-#ifdef HAVE_WINDOWS_CONSOLE_IO
- UINT cp;
- if (fd == 0)
- cp = GetConsoleCP();
- else if (fd == 1 || fd == 2)
- cp = GetConsoleOutputCP();
- else
- cp = 0;
/* GetConsoleCP() and GetConsoleOutputCP() return 0 if the application
has no console */
+ if (cp == CP_UTF8) {
+ _Py_DECLARE_STR(utf_8, "utf-8");
+ return &_Py_STR(utf_8);
+ }
if (cp == 0) {
Py_RETURN_NONE;
}
-
return PyUnicode_FromFormat("cp%u", (unsigned int)cp);
#else
+ int valid;
+ Py_BEGIN_ALLOW_THREADS
+ _Py_BEGIN_SUPPRESS_IPH
+ valid = isatty(fd);
+ _Py_END_SUPPRESS_IPH
+ Py_END_ALLOW_THREADS
+ if (!valid) {
+ Py_RETURN_NONE;
+ }
+
+#ifdef MS_WINDOWS
Py_RETURN_NONE;
-#endif /* HAVE_WINDOWS_CONSOLE_IO */
#else
if (_PyRuntime.preconfig.utf8_mode) {
_Py_DECLARE_STR(utf_8, "utf-8");
@@ -113,6 +138,7 @@ _Py_device_encoding(int fd)
}
return _Py_GetLocaleEncodingObject();
#endif
+#endif /* MS_WINDOWS && HAVE_WINDOWS_CONSOLE_IO */
}
diff --git a/Python/initconfig.c b/Python/initconfig.c
index b9bacb17a66454a..69d47a5872f5e38 100644
--- a/Python/initconfig.c
+++ b/Python/initconfig.c
@@ -2710,8 +2710,30 @@ config_init_stdio_encoding(PyConfig *config,
/* Choose the default error handler based on the current locale. */
if (config->stdio_encoding == NULL) {
- status = config_get_locale_encoding(config, preconfig,
- &config->stdio_encoding);
+#ifdef MS_WINDOWS
+ /* gh-86427: use the console code page. Only one encoding can be
+ specified, so the output code page is used: it affects two
+ streams of three. */
+ UINT cp = config->legacy_windows_stdio ? GetConsoleOutputCP() : 0;
+ if (cp != 0) {
+ if (cp == CP_UTF8) {
+ status = PyConfig_SetString(config, &config->stdio_encoding,
+ L"utf-8");
+ }
+ else {
+ wchar_t encoding[20];
+ swprintf(encoding, Py_ARRAY_LENGTH(encoding), L"cp%u",
+ (unsigned int)cp);
+ status = PyConfig_SetString(config, &config->stdio_encoding,
+ encoding);
+ }
+ }
+ else
+#endif
+ {
+ status = config_get_locale_encoding(config, preconfig,
+ &config->stdio_encoding);
+ }
if (_PyStatus_EXCEPTION(status)) {
return status;
}