From e5ebb8d3036197337639bc52935ec0a14698e0eb Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 22 Jun 2022 21:36:15 +0300 Subject: [PATCH 01/25] gh-74696: Pass root_dir to custom archivers which support it --- Doc/library/shutil.rst | 12 ++++- Doc/whatsnew/3.12.rst | 9 ++++ Lib/shutil.py | 20 ++++---- Lib/test/test_shutil.py | 50 ++++++++++++++++--- ...2-06-25-09-12-23.gh-issue-74696.fxC9ua.rst | 2 + 5 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-06-25-09-12-23.gh-issue-74696.fxC9ua.rst diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index e79caecf310f21..9c2d54a54b4e10 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -575,7 +575,8 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules. .. note:: This function is not thread-safe when custom archivers registered - with :func:`register_archive_format` are used. In this case it + with :func:`register_archive_format` does not support the *root_dir* + argument. In this case it temporarily changes the current working directory of the process to perform archiving. @@ -614,12 +615,21 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules. Further arguments are passed as keyword arguments: *owner*, *group*, *dry_run* and *logger* (as passed in :func:`make_archive`). + If *function* has the *supports_root_dir* attribute set to ``True``, + the *root_dir* argument is passed as a keyword argument. + Otherwise the current working directory of the process is temporarily + changed before calling *function*. + In this case :func:`make_archive` is not thread-safe. + If given, *extra_args* is a sequence of ``(name, value)`` pairs that will be used as extra keywords arguments when the archiver callable is used. *description* is used by :func:`get_archive_formats` which returns the list of archivers. Defaults to an empty string. + .. versionchanged:: 3.12 + Added support of functions supporting the *root_dir* argument. + .. function:: unregister_archive_format(name) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 625790151f70cb..f8888746c104ed 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -97,6 +97,15 @@ os for a process with :func:`os.pidfd_open` in non-blocking mode. (Contributed by Kumar Aditya in :gh:`93312`.) +shutil +------ + +* :func:`shutil.make_archive` now passes the *root_dir* argument to custom + archivers which support it. + In this case it no longer temporarily changes the current working directory + of the process to perform archiving. + (Contributed by Serhiy Storchaka in :gh:`74696`.) + Optimizations ============= diff --git a/Lib/shutil.py b/Lib/shutil.py index f4128647861b81..39f036e36aae2a 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1023,28 +1023,30 @@ def _make_zipfile(base_name, base_dir, verbose=0, dry_run=0, zip_filename = os.path.abspath(zip_filename) return zip_filename +_make_tarball.supports_root_dir = True +_make_zipfile.supports_root_dir = True + # Maps the name of the archive format to a tuple containing: # * the archiving function # * extra keyword arguments # * description -# * does it support the root_dir argument? _ARCHIVE_FORMATS = { 'tar': (_make_tarball, [('compress', None)], - "uncompressed tar file", True), + "uncompressed tar file"), } if _ZLIB_SUPPORTED: _ARCHIVE_FORMATS['gztar'] = (_make_tarball, [('compress', 'gzip')], - "gzip'ed tar-file", True) - _ARCHIVE_FORMATS['zip'] = (_make_zipfile, [], "ZIP file", True) + "gzip'ed tar-file") + _ARCHIVE_FORMATS['zip'] = (_make_zipfile, [], "ZIP file") if _BZ2_SUPPORTED: _ARCHIVE_FORMATS['bztar'] = (_make_tarball, [('compress', 'bzip2')], - "bzip2'ed tar-file", True) + "bzip2'ed tar-file") if _LZMA_SUPPORTED: _ARCHIVE_FORMATS['xztar'] = (_make_tarball, [('compress', 'xz')], - "xz'ed tar-file", True) + "xz'ed tar-file") def get_archive_formats(): """Returns a list of supported formats for archiving and unarchiving. @@ -1075,7 +1077,7 @@ def register_archive_format(name, function, extra_args=None, description=''): if not isinstance(element, (tuple, list)) or len(element) !=2: raise TypeError('extra_args elements are : (arg_name, value)') - _ARCHIVE_FORMATS[name] = (function, extra_args, description, False) + _ARCHIVE_FORMATS[name] = (function, extra_args, description) def unregister_archive_format(name): del _ARCHIVE_FORMATS[name] @@ -1114,10 +1116,10 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0, if base_dir is None: base_dir = os.curdir - support_root_dir = format_info[3] + supports_root_dir = getattr(func, 'supports_root_dir', False) save_cwd = None if root_dir is not None: - if support_root_dir: + if supports_root_dir: kwargs['root_dir'] = root_dir else: save_cwd = os.getcwd() diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 6fa07399007a3e..f7da0d86ac5893 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1572,26 +1572,62 @@ def test_tarfile_root_owner(self): def test_make_archive_cwd(self): current_dir = os.getcwd() - root_dir = self.mkdtemp() - def _breaks(*args, **kw): + def archiver(base_name, base_dir, **kw): + self.assertNotIn('root_dir', kw) + if root_dir is None: + self.assertEqual(base_name, 'basename') + self.assertEqual(os.getcwd(), current_dir) + else: + self.assertEqual(base_name, os.path.join(current_dir, 'basename')) + self.assertEqual(os.getcwd(), root_dir) raise RuntimeError() dirs = [] def _chdir(path): dirs.append(path) orig_chdir(path) - register_archive_format('xxx', _breaks, [], 'xxx file') + register_archive_format('xxx', archiver, [], 'xxx file') try: + root_dir = None with support.swap_attr(os, 'chdir', _chdir) as orig_chdir: - try: - make_archive('xxx', 'xxx', root_dir=root_dir) - except Exception: - pass + with self.assertRaises(RuntimeError): + make_archive('basename', 'xxx') + self.assertEqual(os.getcwd(), current_dir) + self.assertEqual(dirs, []) + + root_dir = self.mkdtemp() + with support.swap_attr(os, 'chdir', _chdir) as orig_chdir: + with self.assertRaises(RuntimeError): + make_archive('basename', 'xxx', root_dir=root_dir) self.assertEqual(os.getcwd(), current_dir) self.assertEqual(dirs, [root_dir, current_dir]) finally: unregister_archive_format('xxx') + def test_make_archive_cwd_supports_root_dir(self): + current_dir = os.getcwd() + root_dir = self.mkdtemp() + def archiver(base_name, base_dir, **kw): + self.assertEqual(base_name, 'basename') + self.assertEqual(kw['root_dir'], root_dir) + self.assertEqual(os.getcwd(), current_dir) + raise RuntimeError() + archiver.supports_root_dir = True + dirs = [] + def _chdir(path): + dirs.append(path) + orig_chdir(path) + + register_archive_format('xxx', archiver, [], 'xxx file') + try: + with support.swap_attr(os, 'chdir', _chdir) as orig_chdir: + with self.assertRaises(RuntimeError): + make_archive('basename', 'xxx', root_dir=root_dir) + self.assertEqual(os.getcwd(), current_dir) + self.assertEqual(dirs, []) + finally: + unregister_archive_format('xxx') + def test_make_tarfile_in_curdir(self): # Issue #21280 root_dir = self.mkdtemp() diff --git a/Misc/NEWS.d/next/Library/2022-06-25-09-12-23.gh-issue-74696.fxC9ua.rst b/Misc/NEWS.d/next/Library/2022-06-25-09-12-23.gh-issue-74696.fxC9ua.rst new file mode 100644 index 00000000000000..48beaff59a16a8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-06-25-09-12-23.gh-issue-74696.fxC9ua.rst @@ -0,0 +1,2 @@ +:func:`shutil.make_archive` now passes the *root_dir* argument to custom +archivers which support it. From 563b0f56a4be23c4268a5fd59ba7a3bcab563119 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 25 Jun 2022 17:19:12 +0300 Subject: [PATCH 02/25] Update Doc/library/shutil.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Éric --- Doc/library/shutil.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 9c2d54a54b4e10..046e9c093283ee 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -575,7 +575,7 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules. .. note:: This function is not thread-safe when custom archivers registered - with :func:`register_archive_format` does not support the *root_dir* + with :func:`register_archive_format` do not support the *root_dir* argument. In this case it temporarily changes the current working directory of the process to perform archiving. From 7dc8fb3c3f9918ce7578a3be501b30fd5cc93002 Mon Sep 17 00:00:00 2001 From: NoSuck Date: Thu, 29 Sep 2022 17:53:41 -0500 Subject: [PATCH 03/25] closes gh-97650: correct sphinx executable (gh-97651) --- Doc/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/README.rst b/Doc/README.rst index 4326086e9e3529..d67cad79916b38 100644 --- a/Doc/README.rst +++ b/Doc/README.rst @@ -40,7 +40,7 @@ If you'd like to create the virtual environment in a different location, you can specify it using the ``VENVDIR`` variable. You can also skip creating the virtual environment altogether, in which case -the Makefile will look for instances of ``sphinxbuild`` and ``blurb`` +the Makefile will look for instances of ``sphinx-build`` and ``blurb`` installed on your process ``PATH`` (configurable with the ``SPHINXBUILD`` and ``BLURB`` variables). From 679cf9640b4c279adbad986799db85d0d640fab2 Mon Sep 17 00:00:00 2001 From: Jeff Allen Date: Fri, 30 Sep 2022 00:02:27 +0100 Subject: [PATCH 04/25] gh-96397: Document that attributes need not be identifiers (#96454) Co-authored-by: C.A.M. Gerlach --- Doc/glossary.rst | 11 +++++++++-- Doc/library/functions.rst | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Doc/glossary.rst b/Doc/glossary.rst index e0dd4fc96760e7..9385b8ddd13d10 100644 --- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -136,10 +136,17 @@ Glossary :exc:`StopAsyncIteration` exception. Introduced by :pep:`492`. attribute - A value associated with an object which is referenced by name using - dotted expressions. For example, if an object *o* has an attribute + A value associated with an object which is usually referenced by name + using dotted expressions. + For example, if an object *o* has an attribute *a* it would be referenced as *o.a*. + It is possible to give an object an attribute whose name is not an + identifier as defined by :ref:`identifiers`, for example using + :func:`setattr`, if the object allows it. + Such an attribute will not be accessible using a dotted expression, + and would instead need to be retrieved with :func:`getattr`. + awaitable An object that can be used in an :keyword:`await` expression. Can be a :term:`coroutine` or an object with an :meth:`__await__` method. diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index ccb691dd9f009f..26ee302d2eabc3 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -397,6 +397,7 @@ are always available. They are listed here in alphabetical order. string. The string must be the name of one of the object's attributes. The function deletes the named attribute, provided the object allows it. For example, ``delattr(x, 'foobar')`` is equivalent to ``del x.foobar``. + *name* need not be a Python identifier (see :func:`setattr`). .. _func-dict: @@ -738,6 +739,7 @@ are always available. They are listed here in alphabetical order. value of that attribute. For example, ``getattr(x, 'foobar')`` is equivalent to ``x.foobar``. If the named attribute does not exist, *default* is returned if provided, otherwise :exc:`AttributeError` is raised. + *name* need not be a Python identifier (see :func:`setattr`). .. note:: @@ -1582,6 +1584,12 @@ are always available. They are listed here in alphabetical order. object allows it. For example, ``setattr(x, 'foobar', 123)`` is equivalent to ``x.foobar = 123``. + *name* need not be a Python identifier as defined in :ref:`identifiers` + unless the object chooses to enforce that, for example in a custom + :meth:`~object.__getattribute__` or via :attr:`~object.__slots__`. + An attribute whose name is not an identifier will not be accessible using + the dot notation, but is accessible through :func:`getattr` etc.. + .. note:: Since :ref:`private name mangling ` happens at From 3c84af2d0ce28999004254a18c472a98ebee25e2 Mon Sep 17 00:00:00 2001 From: Ofey Chan Date: Fri, 30 Sep 2022 16:43:02 +0800 Subject: [PATCH 05/25] gh-96348: Deprecate the 3-arg signature of coroutine.throw and generator.throw (GH-96428) --- Doc/reference/datamodel.rst | 5 +++ Doc/reference/expressions.rst | 13 +++++++- Doc/whatsnew/3.12.rst | 5 +++ Lib/contextlib.py | 4 +-- Lib/test/test_asyncgen.py | 22 +++++++++---- Lib/test/test_asyncio/test_futures.py | 13 +++++--- Lib/test/test_coroutines.py | 9 +++++- Lib/test/test_generators.py | 21 ++++++++++++ Lib/test/test_types.py | 2 +- Misc/ACKS | 1 + ...2-08-31-18-46-13.gh-issue-96348.xzCoTP.rst | 2 ++ Modules/_asynciomodule.c | 8 +++++ Objects/genobject.c | 32 +++++++++++++++++-- Objects/iterobject.c | 9 ++++-- 14 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-08-31-18-46-13.gh-issue-96348.xzCoTP.rst diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 758f3aef3ee34d..c93269ab04b64f 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -2996,6 +2996,11 @@ generators, coroutines do not directly support iteration. above. If the exception is not caught in the coroutine, it propagates back to the caller. + .. versionchanged:: 3.12 + + The second signature \(type\[, value\[, traceback\]\]\) is deprecated and + may be removed in a future version of Python. + .. method:: coroutine.close() Causes the coroutine to clean itself up and exit. If the coroutine diff --git a/Doc/reference/expressions.rst b/Doc/reference/expressions.rst index 6d23e473cdcd41..a6ca55dafe5365 100644 --- a/Doc/reference/expressions.rst +++ b/Doc/reference/expressions.rst @@ -582,6 +582,11 @@ is already executing raises a :exc:`ValueError` exception. :attr:`~BaseException.__traceback__` attribute stored in *value* may be cleared. + .. versionchanged:: 3.12 + + The second signature \(type\[, value\[, traceback\]\]\) is deprecated and + may be removed in a future version of Python. + .. index:: exception: GeneratorExit @@ -738,7 +743,8 @@ which are used to control the execution of a generator function. because there is no yield expression that could receive the value. -.. coroutinemethod:: agen.athrow(type[, value[, traceback]]) +.. coroutinemethod:: agen.athrow(value) + agen.athrow(type[, value[, traceback]]) Returns an awaitable that raises an exception of type ``type`` at the point where the asynchronous generator was paused, and returns the next value @@ -750,6 +756,11 @@ which are used to control the execution of a generator function. raises a different exception, then when the awaitable is run that exception propagates to the caller of the awaitable. + .. versionchanged:: 3.12 + + The second signature \(type\[, value\[, traceback\]\]\) is deprecated and + may be removed in a future version of Python. + .. index:: exception: GeneratorExit diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 981de9e7401e2b..952a0a491e6e0f 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -191,6 +191,11 @@ Deprecated and tailor them to your needs. (Contributed by Erlend E. Aasland in :gh:`90016`.) +* The 3-arg signatures (type, value, traceback) of :meth:`~coroutine.throw`, + :meth:`~generator.throw` and :meth:`~agen.athrow` are deprecated and + may be removed in a future version of Python. Use the single-arg versions + of these functions instead. (Contributed by Ofey Chan in :gh:`89874`.) + Pending Removal in Python 3.13 ------------------------------ diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 625bb33b12d5fd..d5822219b3e25b 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -152,7 +152,7 @@ def __exit__(self, typ, value, traceback): # tell if we get the same exception back value = typ() try: - self.gen.throw(typ, value, traceback) + self.gen.throw(value) except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration @@ -219,7 +219,7 @@ async def __aexit__(self, typ, value, traceback): # tell if we get the same exception back value = typ() try: - await self.gen.athrow(typ, value, traceback) + await self.gen.athrow(value) except StopAsyncIteration as exc: # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index fb22f411c2e296..f6184c0cab4694 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -2,6 +2,7 @@ import types import unittest import contextlib +import warnings from test.support.import_helper import import_module from test.support import gc_collect, requires_working_socket @@ -377,6 +378,13 @@ async def async_gen_wrapper(): self.compare_generators(sync_gen_wrapper(), async_gen_wrapper()) + def test_async_gen_3_arg_deprecation_warning(self): + async def gen(): + yield 123 + + with self.assertWarns(DeprecationWarning): + gen().athrow(GeneratorExit, GeneratorExit(), None) + def test_async_gen_api_01(self): async def gen(): yield 123 @@ -650,7 +658,7 @@ def test1(anext): agen = agenfn() with contextlib.closing(anext(agen, "default").__await__()) as g: self.assertEqual(g.send(None), 1) - self.assertEqual(g.throw(MyError, MyError(), None), 2) + self.assertEqual(g.throw(MyError()), 2) try: g.send(None) except StopIteration as e: @@ -663,9 +671,9 @@ def test2(anext): agen = agenfn() with contextlib.closing(anext(agen, "default").__await__()) as g: self.assertEqual(g.send(None), 1) - self.assertEqual(g.throw(MyError, MyError(), None), 2) + self.assertEqual(g.throw(MyError()), 2) with self.assertRaises(MyError): - g.throw(MyError, MyError(), None) + g.throw(MyError()) def test3(anext): agen = agenfn() @@ -692,9 +700,9 @@ async def agenfn(): agen = agenfn() with contextlib.closing(anext(agen, "default").__await__()) as g: self.assertEqual(g.send(None), 10) - self.assertEqual(g.throw(MyError, MyError(), None), 20) + self.assertEqual(g.throw(MyError()), 20) with self.assertRaisesRegex(MyError, 'val'): - g.throw(MyError, MyError('val'), None) + g.throw(MyError('val')) def test5(anext): @types.coroutine @@ -713,7 +721,7 @@ async def agenfn(): with contextlib.closing(anext(agen, "default").__await__()) as g: self.assertEqual(g.send(None), 10) with self.assertRaisesRegex(StopIteration, 'default'): - g.throw(MyError, MyError(), None) + g.throw(MyError()) def test6(anext): @types.coroutine @@ -728,7 +736,7 @@ async def agenfn(): agen = agenfn() with contextlib.closing(anext(agen, "default").__await__()) as g: with self.assertRaises(MyError): - g.throw(MyError, MyError(), None) + g.throw(MyError()) def run_test(test): with self.subTest('pure-Python anext()'): diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index f4a46ec90a16fe..11d4273930804f 100644 --- a/Lib/test/test_asyncio/test_futures.py +++ b/Lib/test/test_asyncio/test_futures.py @@ -10,6 +10,7 @@ from types import GenericAlias import asyncio from asyncio import futures +import warnings from test.test_asyncio import utils as test_utils from test import support @@ -619,10 +620,14 @@ def test_future_stop_iteration_args(self): def test_future_iter_throw(self): fut = self._new_future(loop=self.loop) fi = iter(fut) - self.assertRaises(TypeError, fi.throw, - Exception, Exception("elephant"), 32) - self.assertRaises(TypeError, fi.throw, - Exception("elephant"), Exception("elephant")) + with self.assertWarns(DeprecationWarning): + self.assertRaises(Exception, fi.throw, Exception, Exception("zebra"), None) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + self.assertRaises(TypeError, fi.throw, + Exception, Exception("elephant"), 32) + self.assertRaises(TypeError, fi.throw, + Exception("elephant"), Exception("elephant")) self.assertRaises(TypeError, fi.throw, list) def test_future_del_collect(self): diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index 8fff2d47c10fd5..9a2279d353ffa4 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -709,9 +709,16 @@ async def foo(): aw = coro.__await__() next(aw) with self.assertRaises(ZeroDivisionError): - aw.throw(ZeroDivisionError, None, None) + aw.throw(ZeroDivisionError()) self.assertEqual(N, 102) + coro = foo() + aw = coro.__await__() + next(aw) + with self.assertRaises(ZeroDivisionError): + with self.assertWarns(DeprecationWarning): + aw.throw(ZeroDivisionError, ZeroDivisionError(), None) + def test_func_11(self): async def func(): pass coro = func() diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index e5aa7da1e0df13..fb2d9ced0633f1 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -342,6 +342,15 @@ def generator(): with self.assertRaises(StopIteration): gen.throw(E) + def test_gen_3_arg_deprecation_warning(self): + def g(): + yield 42 + + gen = g() + with self.assertWarns(DeprecationWarning): + with self.assertRaises(TypeError): + gen.throw(TypeError, TypeError(24), None) + def test_stopiteration_error(self): # See also PEP 479. @@ -2113,6 +2122,12 @@ def printsolution(self, x): >>> g.throw(ValueError("xyz")) # value only caught ValueError (xyz) +>>> import warnings +>>> warnings.filterwarnings("ignore", category=DeprecationWarning) + +# Filter DeprecationWarning: regarding the (type, val, tb) signature of throw(). +# Deprecation warnings are re-enabled below. + >>> g.throw(ValueError, ValueError(1)) # value+matching type caught ValueError (1) @@ -2181,6 +2196,12 @@ def printsolution(self, x): ... ValueError: 7 +>>> warnings.filters.pop(0) +('ignore', None, , None, 0) + +# Re-enable DeprecationWarning: the (type, val, tb) exception representation is deprecated, +# and may be removed in a future version of Python. + Plain "raise" inside a generator should preserve the traceback (#13188). The traceback should have 3 levels: - g.throw() diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index f00da0a758d46f..af095632a36fcb 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -2072,7 +2072,7 @@ def foo(): return gen wrapper = foo() wrapper.send(None) with self.assertRaisesRegex(Exception, 'ham'): - wrapper.throw(Exception, Exception('ham')) + wrapper.throw(Exception('ham')) # decorate foo second time foo = types.coroutine(foo) diff --git a/Misc/ACKS b/Misc/ACKS index 0edea9219d05ef..fc0e745e28ceab 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -297,6 +297,7 @@ Michael Cetrulo Dave Chambers Pascal Chambon Nicholas Chammas +Ofey Chan John Chandler Hye-Shik Chang Jeffrey Chang diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-08-31-18-46-13.gh-issue-96348.xzCoTP.rst b/Misc/NEWS.d/next/Core and Builtins/2022-08-31-18-46-13.gh-issue-96348.xzCoTP.rst new file mode 100644 index 00000000000000..5d3bd17b578669 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-08-31-18-46-13.gh-issue-96348.xzCoTP.rst @@ -0,0 +1,2 @@ +Emit a DeprecationWarning when :meth:`~generator.throw`, :meth:`~coroutine.throw` or :meth:`~agen.athrow` +are called with more than one argument. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 9d2f83bf6c73c1..5a5881b873e245 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1668,6 +1668,14 @@ FutureIter_throw(futureiterobject *self, PyObject *const *args, Py_ssize_t nargs if (!_PyArg_CheckPositional("throw", nargs, 1, 3)) { return NULL; } + if (nargs > 1) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "the (type, exc, tb) signature of throw() is deprecated, " + "use the single-arg signature instead.", + 1) < 0) { + return NULL; + } + } type = args[0]; if (nargs == 3) { diff --git a/Objects/genobject.c b/Objects/genobject.c index da4afecc69c8c1..ad4fbed6d8d579 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -418,7 +418,9 @@ PyDoc_STRVAR(throw_doc, throw(type[,value[,tb]])\n\ \n\ Raise exception in generator, return next yielded value or raise\n\ -StopIteration."); +StopIteration.\n\ +the (type, val, tb) signature is deprecated, \n\ +and may be removed in a future version of Python."); static PyObject * _gen_throw(PyGenObject *gen, int close_on_genexit, @@ -559,6 +561,14 @@ gen_throw(PyGenObject *gen, PyObject *const *args, Py_ssize_t nargs) if (!_PyArg_CheckPositional("throw", nargs, 1, 3)) { return NULL; } + if (nargs > 1) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "the (type, exc, tb) signature of throw() is deprecated, " + "use the single-arg signature instead.", + 1) < 0) { + return NULL; + } + } typ = args[0]; if (nargs == 3) { val = args[1]; @@ -1147,7 +1157,10 @@ PyDoc_STRVAR(coro_throw_doc, throw(type[,value[,traceback]])\n\ \n\ Raise exception in coroutine, return next iterated value or raise\n\ -StopIteration."); +StopIteration.\n\ +the (type, val, tb) signature is deprecated, \n\ +and may be removed in a future version of Python."); + PyDoc_STRVAR(coro_close_doc, "close() -> raise GeneratorExit inside coroutine."); @@ -1500,6 +1513,14 @@ async_gen_aclose(PyAsyncGenObject *o, PyObject *arg) static PyObject * async_gen_athrow(PyAsyncGenObject *o, PyObject *args) { + if (PyTuple_GET_SIZE(args) > 1) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "the (type, exc, tb) signature of athrow() is deprecated, " + "use the single-arg signature instead.", + 1) < 0) { + return NULL; + } + } if (async_gen_init_hooks(o)) { return NULL; } @@ -1537,7 +1558,12 @@ PyDoc_STRVAR(async_asend_doc, "asend(v) -> send 'v' in generator."); PyDoc_STRVAR(async_athrow_doc, -"athrow(typ[,val[,tb]]) -> raise exception in generator."); +"athrow(value)\n\ +athrow(type[,value[,tb]])\n\ +\n\ +raise exception in generator.\n\ +the (type, val, tb) signature is deprecated, \n\ +and may be removed in a future version of Python."); static PyMethodDef async_gen_methods[] = { {"asend", (PyCFunction)async_gen_asend, METH_O, async_asend_doc}, diff --git a/Objects/iterobject.c b/Objects/iterobject.c index 1732a037600c9e..62c36146d64f69 100644 --- a/Objects/iterobject.c +++ b/Objects/iterobject.c @@ -428,8 +428,13 @@ return next yielded value or raise StopIteration."); PyDoc_STRVAR(throw_doc, -"throw(typ[,val[,tb]]) -> raise exception in the wrapped iterator,\n\ -return next yielded value or raise StopIteration."); +"throw(value)\n\ +throw(typ[,val[,tb]])\n\ +\n\ +raise exception in the wrapped iterator, return next yielded value\n\ +or raise StopIteration.\n\ +the (type, val, tb) signature is deprecated, \n\ +and may be removed in a future version of Python."); PyDoc_STRVAR(close_doc, From 8a0ad46585e1ff1be01a5c2f846fc10c4f980c59 Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Fri, 30 Sep 2022 04:59:46 -0400 Subject: [PATCH 06/25] Use SyntaxError invalid range in tutorial introduction example (GH-93031) Use output from a 3.10+ REPL, showing invalid range, for the SyntaxError examples in the tutorial introduction page. Automerge-Triggered-By: GH:iritkatriel --- Doc/tutorial/introduction.rst | 4 ++-- .../2022-05-20-18-42-10.gh-issue-93031.c2RdJe.rst | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Documentation/2022-05-20-18-42-10.gh-issue-93031.c2RdJe.rst diff --git a/Doc/tutorial/introduction.rst b/Doc/tutorial/introduction.rst index 33678f5a64b1f3..ba0f4770529783 100644 --- a/Doc/tutorial/introduction.rst +++ b/Doc/tutorial/introduction.rst @@ -234,12 +234,12 @@ This only works with two literals though, not with variables or expressions:: >>> prefix 'thon' # can't concatenate a variable and a string literal File "", line 1 prefix 'thon' - ^ + ^^^^^^ SyntaxError: invalid syntax >>> ('un' * 3) 'ium' File "", line 1 ('un' * 3) 'ium' - ^ + ^^^^^ SyntaxError: invalid syntax If you want to concatenate variables or a variable and a literal, use ``+``:: diff --git a/Misc/NEWS.d/next/Documentation/2022-05-20-18-42-10.gh-issue-93031.c2RdJe.rst b/Misc/NEWS.d/next/Documentation/2022-05-20-18-42-10.gh-issue-93031.c2RdJe.rst new file mode 100644 index 00000000000000..c46b45d2433cb8 --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2022-05-20-18-42-10.gh-issue-93031.c2RdJe.rst @@ -0,0 +1 @@ +Update tutorial introduction output to use 3.10+ SyntaxError invalid range. From 182755ffe629c7846e0898a1070ccbdae46d142c Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 30 Sep 2022 10:25:00 +0100 Subject: [PATCH 07/25] gh-97649: The Tools directory is no longer installed on Windows (GH-97653) --- ...2-09-29-22-27-04.gh-issue-97649.bI7OQU.rst | 1 + PC/layout/support/options.py | 2 - Tools/msi/bundle/bundle.targets | 1 - Tools/msi/bundle/bundle.wxs | 3 +- Tools/msi/bundle/packagegroups/tools.wxs | 26 ------------- Tools/msi/common.wxs | 6 --- Tools/msi/tools/tools.wixproj | 38 ------------------- Tools/msi/tools/tools.wxs | 17 --------- Tools/msi/tools/tools_en-US.wxl | 5 --- Tools/msi/tools/tools_files.wxs | 20 ---------- 10 files changed, 2 insertions(+), 117 deletions(-) create mode 100644 Misc/NEWS.d/next/Windows/2022-09-29-22-27-04.gh-issue-97649.bI7OQU.rst delete mode 100644 Tools/msi/bundle/packagegroups/tools.wxs delete mode 100644 Tools/msi/tools/tools.wixproj delete mode 100644 Tools/msi/tools/tools.wxs delete mode 100644 Tools/msi/tools/tools_en-US.wxl delete mode 100644 Tools/msi/tools/tools_files.wxs diff --git a/Misc/NEWS.d/next/Windows/2022-09-29-22-27-04.gh-issue-97649.bI7OQU.rst b/Misc/NEWS.d/next/Windows/2022-09-29-22-27-04.gh-issue-97649.bI7OQU.rst new file mode 100644 index 00000000000000..118f9df7c89d2f --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2022-09-29-22-27-04.gh-issue-97649.bI7OQU.rst @@ -0,0 +1 @@ +The ``Tools`` directory is no longer installed on Windows diff --git a/PC/layout/support/options.py b/PC/layout/support/options.py index e8310293159e4c..3d93e892adae8a 100644 --- a/PC/layout/support/options.py +++ b/PC/layout/support/options.py @@ -57,7 +57,6 @@ def public(f): "help": "nuget package", "options": [ "dev", - "tools", "pip", "stable", "distutils", @@ -76,7 +75,6 @@ def public(f): "tcltk", "idle", "tests", - "tools", "venv", "dev", "symbols", diff --git a/Tools/msi/bundle/bundle.targets b/Tools/msi/bundle/bundle.targets index 89a5960a50efe1..9c7410fe514d19 100644 --- a/Tools/msi/bundle/bundle.targets +++ b/Tools/msi/bundle/bundle.targets @@ -71,7 +71,6 @@ - diff --git a/Tools/msi/bundle/bundle.wxs b/Tools/msi/bundle/bundle.wxs index 19e67faf887bc7..d23158c2cae91c 100644 --- a/Tools/msi/bundle/bundle.wxs +++ b/Tools/msi/bundle/bundle.wxs @@ -71,7 +71,7 @@ - + @@ -106,7 +106,6 @@ - diff --git a/Tools/msi/bundle/packagegroups/tools.wxs b/Tools/msi/bundle/packagegroups/tools.wxs deleted file mode 100644 index 1d9ab19f3e090a..00000000000000 --- a/Tools/msi/bundle/packagegroups/tools.wxs +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Tools/msi/common.wxs b/Tools/msi/common.wxs index b819d320ee9481..55cb44860d02c0 100644 --- a/Tools/msi/common.wxs +++ b/Tools/msi/common.wxs @@ -121,12 +121,6 @@ - - - - - - diff --git a/Tools/msi/tools/tools.wixproj b/Tools/msi/tools/tools.wixproj deleted file mode 100644 index 2963048ddb49bd..00000000000000 --- a/Tools/msi/tools/tools.wixproj +++ /dev/null @@ -1,38 +0,0 @@ - - - - {24CBEB95-BC1E-4EA9-AEA9-33834BCCD0EC} - 2.0 - tools - Package - - - - - - - - - - - - $(PySourcePath) - !(bindpath.src) - $(PySourcePath) - - tools_py - true - - - - - diff --git a/Tools/msi/tools/tools.wxs b/Tools/msi/tools/tools.wxs deleted file mode 100644 index c06b3c27f6970a..00000000000000 --- a/Tools/msi/tools/tools.wxs +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/Tools/msi/tools/tools_en-US.wxl b/Tools/msi/tools/tools_en-US.wxl deleted file mode 100644 index a1384177ea264b..00000000000000 --- a/Tools/msi/tools/tools_en-US.wxl +++ /dev/null @@ -1,5 +0,0 @@ - - - Utility Scripts - tools - diff --git a/Tools/msi/tools/tools_files.wxs b/Tools/msi/tools/tools_files.wxs deleted file mode 100644 index 3de6c9291cf676..00000000000000 --- a/Tools/msi/tools/tools_files.wxs +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - From b1a9de028cf3348332bd68a9ebdc8bfefda32af4 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 30 Sep 2022 10:29:31 +0100 Subject: [PATCH 08/25] gh-90989: Install Windows launcher per-user, and clarify some installer text (GH-97655) --- .../2022-09-29-23-08-49.gh-issue-90989.no89Q2.rst | 2 ++ Tools/msi/bundle/Default.wxl | 12 ++++++------ Tools/msi/bundle/bundle.wxs | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Windows/2022-09-29-23-08-49.gh-issue-90989.no89Q2.rst diff --git a/Misc/NEWS.d/next/Windows/2022-09-29-23-08-49.gh-issue-90989.no89Q2.rst b/Misc/NEWS.d/next/Windows/2022-09-29-23-08-49.gh-issue-90989.no89Q2.rst new file mode 100644 index 00000000000000..786b3435042dbd --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2022-09-29-23-08-49.gh-issue-90989.no89Q2.rst @@ -0,0 +1,2 @@ +Made :ref:`launcher` install per-user by default (unless an all users +install already exists), and clarify some text in the installer. diff --git a/Tools/msi/bundle/Default.wxl b/Tools/msi/bundle/Default.wxl index 8b2633fe89f726..e83a540a0e6726 100644 --- a/Tools/msi/bundle/Default.wxl +++ b/Tools/msi/bundle/Default.wxl @@ -79,15 +79,15 @@ Select Customize to review current options. Use Programs and Features to remove the 'py' launcher. Upgrades the global 'py' launcher from the previous version. - Associate &files with Python (requires the py launcher) + Associate &files with Python (requires the 'py' launcher) Create shortcuts for installed applications Add Python to &environment variables - Add &Python [ShortVersion] to PATH + Add &python.exe to PATH Append Python to &environment variables - Append &Python [ShortVersion] to PATH - Install for &all users - for &all users (requires elevation) - Install &launcher for all users (recommended) + Append &python.exe to PATH + Install Python [ShortVersion] for &all users + for &all users (requires admin privileges) + Use admin privi&leges when installing py.exe &Precompile standard library Download debugging &symbols Download debu&g binaries (requires VS 2017 or later) diff --git a/Tools/msi/bundle/bundle.wxs b/Tools/msi/bundle/bundle.wxs index d23158c2cae91c..8b12baae31105e 100644 --- a/Tools/msi/bundle/bundle.wxs +++ b/Tools/msi/bundle/bundle.wxs @@ -28,7 +28,7 @@ - + From cc1e8e0367c90a67d8b24a0cb8c0b75d85ce1094 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 30 Sep 2022 14:58:30 +0200 Subject: [PATCH 09/25] gh-94526: getpath_dirname() no longer encodes the path (#97645) Fix the Python path configuration used to initialized sys.path at Python startup. Paths are no longer encoded to UTF-8/strict to avoid encoding errors if it contains surrogate characters (bytes paths are decoded with the surrogateescape error handler). getpath_basename() and getpath_dirname() functions no longer encode the path to UTF-8/strict, but work directly on Unicode strings. These functions now use PyUnicode_FindChar() and PyUnicode_Substring() on the Unicode path, rather than strrchr() on the encoded bytes string. --- ...2-09-29-15-19-29.gh-issue-94526.wq5m6T.rst | 4 ++++ Modules/getpath.c | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-09-29-15-19-29.gh-issue-94526.wq5m6T.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-09-29-15-19-29.gh-issue-94526.wq5m6T.rst b/Misc/NEWS.d/next/Core and Builtins/2022-09-29-15-19-29.gh-issue-94526.wq5m6T.rst new file mode 100644 index 00000000000000..59e389a64ee07d --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-09-29-15-19-29.gh-issue-94526.wq5m6T.rst @@ -0,0 +1,4 @@ +Fix the Python path configuration used to initialized :data:`sys.path` at +Python startup. Paths are no longer encoded to UTF-8/strict to avoid encoding +errors if it contains surrogate characters (bytes paths are decoded with the +surrogateescape error handler). Patch by Victor Stinner. diff --git a/Modules/getpath.c b/Modules/getpath.c index 94479887cf850a..be704adbde9468 100644 --- a/Modules/getpath.c +++ b/Modules/getpath.c @@ -82,27 +82,32 @@ getpath_abspath(PyObject *Py_UNUSED(self), PyObject *args) static PyObject * getpath_basename(PyObject *Py_UNUSED(self), PyObject *args) { - const char *path; - if (!PyArg_ParseTuple(args, "s", &path)) { + PyObject *path; + if (!PyArg_ParseTuple(args, "U", &path)) { return NULL; } - const char *name = strrchr(path, SEP); - return PyUnicode_FromString(name ? name + 1 : path); + Py_ssize_t end = PyUnicode_GET_LENGTH(path); + Py_ssize_t pos = PyUnicode_FindChar(path, SEP, 0, end, -1); + if (pos < 0) { + return Py_NewRef(path); + } + return PyUnicode_Substring(path, pos + 1, end); } static PyObject * getpath_dirname(PyObject *Py_UNUSED(self), PyObject *args) { - const char *path; - if (!PyArg_ParseTuple(args, "s", &path)) { + PyObject *path; + if (!PyArg_ParseTuple(args, "U", &path)) { return NULL; } - const char *name = strrchr(path, SEP); - if (!name) { + Py_ssize_t end = PyUnicode_GET_LENGTH(path); + Py_ssize_t pos = PyUnicode_FindChar(path, SEP, 0, end, -1); + if (pos < 0) { return PyUnicode_FromStringAndSize(NULL, 0); } - return PyUnicode_FromStringAndSize(path, (name - path)); + return PyUnicode_Substring(path, 0, pos); } From 8fd0e8618c5d995458ed58553f7023789cb310a1 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Fri, 30 Sep 2022 12:44:44 -0400 Subject: [PATCH 10/25] bpo-35675: IDLE - separate config_key window and frame (#11427) bpo-35598: IDLE: Refactor window and frame class Co-authored-by: Terry Jan Reedy --- Lib/idlelib/config_key.py | 137 ++++++++++++--------- Lib/idlelib/configdialog.py | 4 +- Lib/idlelib/idle_test/test_config_key.py | 109 ++++++++++++---- Lib/idlelib/idle_test/test_configdialog.py | 6 +- 4 files changed, 174 insertions(+), 82 deletions(-) diff --git a/Lib/idlelib/config_key.py b/Lib/idlelib/config_key.py index 9ca3a156f4b97f..bb07231cd590b6 100644 --- a/Lib/idlelib/config_key.py +++ b/Lib/idlelib/config_key.py @@ -41,32 +41,22 @@ def translate_key(key, modifiers): return f'Key-{key}' -class GetKeysDialog(Toplevel): +class GetKeysFrame(Frame): # Dialog title for invalid key sequence keyerror_title = 'Key Sequence Error' - def __init__(self, parent, title, action, current_key_sequences, - *, _htest=False, _utest=False): + def __init__(self, parent, action, current_key_sequences): """ parent - parent of this dialog - title - string which is the title of the popup dialog - action - string, the name of the virtual event these keys will be + action - the name of the virtual event these keys will be mapped to - current_key_sequences - list, a list of all key sequence lists + current_key_sequences - a list of all key sequence lists currently mapped to virtual events, for overlap checking - _htest - bool, change box location when running htest - _utest - bool, do not wait when running unittest """ - Toplevel.__init__(self, parent) - self.withdraw() # Hide while setting geometry. - self.configure(borderwidth=5) - self.resizable(height=False, width=False) - self.title(title) - self.transient(parent) - _setup_dialog(self) - self.grab_set() - self.protocol("WM_DELETE_WINDOW", self.cancel) + super().__init__(parent) + self['borderwidth'] = 2 + self['relief'] = 'sunken' self.parent = parent self.action = action self.current_key_sequences = current_key_sequences @@ -82,39 +72,14 @@ def __init__(self, parent, title, action, current_key_sequences, self.modifier_vars.append(variable) self.advanced = False self.create_widgets() - self.update_idletasks() - self.geometry( - "+%d+%d" % ( - parent.winfo_rootx() + - (parent.winfo_width()/2 - self.winfo_reqwidth()/2), - parent.winfo_rooty() + - ((parent.winfo_height()/2 - self.winfo_reqheight()/2) - if not _htest else 150) - ) ) # Center dialog over parent (or below htest box). - if not _utest: - self.deiconify() # Geometry set, unhide. - self.wait_window() def showerror(self, *args, **kwargs): # Make testing easier. Replace in #30751. messagebox.showerror(*args, **kwargs) def create_widgets(self): - self.frame = frame = Frame(self, borderwidth=2, relief='sunken') - frame.pack(side='top', expand=True, fill='both') - - frame_buttons = Frame(self) - frame_buttons.pack(side='bottom', fill='x') - - self.button_ok = Button(frame_buttons, text='OK', - width=8, command=self.ok) - self.button_ok.grid(row=0, column=0, padx=5, pady=5) - self.button_cancel = Button(frame_buttons, text='Cancel', - width=8, command=self.cancel) - self.button_cancel.grid(row=0, column=1, padx=5, pady=5) - # Basic entry key sequence. - self.frame_keyseq_basic = Frame(frame, name='keyseq_basic') + self.frame_keyseq_basic = Frame(self, name='keyseq_basic') self.frame_keyseq_basic.grid(row=0, column=0, sticky='nsew', padx=5, pady=5) basic_title = Label(self.frame_keyseq_basic, @@ -127,7 +92,7 @@ def create_widgets(self): basic_keys.pack(ipadx=5, ipady=5, fill='x') # Basic entry controls. - self.frame_controls_basic = Frame(frame) + self.frame_controls_basic = Frame(self) self.frame_controls_basic.grid(row=1, column=0, sticky='nsew', padx=5) # Basic entry modifiers. @@ -169,7 +134,7 @@ def create_widgets(self): self.button_clear.grid(row=2, column=0, columnspan=4) # Advanced entry key sequence. - self.frame_keyseq_advanced = Frame(frame, name='keyseq_advanced') + self.frame_keyseq_advanced = Frame(self, name='keyseq_advanced') self.frame_keyseq_advanced.grid(row=0, column=0, sticky='nsew', padx=5, pady=5) advanced_title = Label(self.frame_keyseq_advanced, justify='left', @@ -181,7 +146,7 @@ def create_widgets(self): self.advanced_keys.pack(fill='x') # Advanced entry help text. - self.frame_help_advanced = Frame(frame) + self.frame_help_advanced = Frame(self) self.frame_help_advanced.grid(row=1, column=0, sticky='nsew', padx=5) help_advanced = Label(self.frame_help_advanced, justify='left', text="Key bindings are specified using Tkinter keysyms as\n"+ @@ -196,7 +161,7 @@ def create_widgets(self): help_advanced.grid(row=0, column=0, sticky='nsew') # Switch between basic and advanced. - self.button_level = Button(frame, command=self.toggle_level, + self.button_level = Button(self, command=self.toggle_level, text='<< Basic Key Binding Entry') self.button_level.grid(row=2, column=0, stick='ew', padx=5, pady=5) self.toggle_level() @@ -257,7 +222,8 @@ def clear_key_seq(self): variable.set('') self.key_string.set('') - def ok(self, event=None): + def ok(self): + self.result = '' keys = self.key_string.get().strip() if not keys: self.showerror(title=self.keyerror_title, parent=self, @@ -265,13 +231,7 @@ def ok(self, event=None): return if (self.advanced or self.keys_ok(keys)) and self.bind_ok(keys): self.result = keys - self.grab_release() - self.destroy() - - def cancel(self, event=None): - self.result = '' - self.grab_release() - self.destroy() + return def keys_ok(self, keys): """Validity check on user's 'basic' keybinding selection. @@ -319,6 +279,73 @@ def bind_ok(self, keys): return True +class GetKeysWindow(Toplevel): + + def __init__(self, parent, title, action, current_key_sequences, + *, _htest=False, _utest=False): + """ + parent - parent of this dialog + title - string which is the title of the popup dialog + action - string, the name of the virtual event these keys will be + mapped to + current_key_sequences - list, a list of all key sequence lists + currently mapped to virtual events, for overlap checking + _htest - bool, change box location when running htest + _utest - bool, do not wait when running unittest + """ + super().__init__(parent) + self.withdraw() # Hide while setting geometry. + self['borderwidth'] = 5 + self.resizable(height=False, width=False) + # Needed for winfo_reqwidth(). + self.update_idletasks() + # Center dialog over parent (or below htest box). + x = (parent.winfo_rootx() + + (parent.winfo_width()//2 - self.winfo_reqwidth()//2)) + y = (parent.winfo_rooty() + + ((parent.winfo_height()//2 - self.winfo_reqheight()//2) + if not _htest else 150)) + self.geometry(f"+{x}+{y}") + + self.title(title) + self.frame = frame = GetKeysFrame(self, action, current_key_sequences) + self.protocol("WM_DELETE_WINDOW", self.cancel) + frame_buttons = Frame(self) + self.button_ok = Button(frame_buttons, text='OK', + width=8, command=self.ok) + self.button_cancel = Button(frame_buttons, text='Cancel', + width=8, command=self.cancel) + self.button_ok.grid(row=0, column=0, padx=5, pady=5) + self.button_cancel.grid(row=0, column=1, padx=5, pady=5) + frame.pack(side='top', expand=True, fill='both') + frame_buttons.pack(side='bottom', fill='x') + + self.transient(parent) + _setup_dialog(self) + self.grab_set() + if not _utest: + self.deiconify() # Geometry set, unhide. + self.wait_window() + + @property + def result(self): + return self.frame.result + + @result.setter + def result(self, value): + self.frame.result = value + + def ok(self, event=None): + self.frame.ok() + self.grab_release() + self.destroy() + + def cancel(self, event=None): + self.result = '' + self.grab_release() + self.destroy() + + if __name__ == '__main__': from unittest import main main('idlelib.idle_test.test_config_key', verbosity=2, exit=False) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 8e478d743fb767..cda7966d558a51 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -24,7 +24,7 @@ from tkinter import messagebox from idlelib.config import idleConf, ConfigChanges -from idlelib.config_key import GetKeysDialog +from idlelib.config_key import GetKeysWindow from idlelib.dynoption import DynOptionMenu from idlelib import macosx from idlelib.query import SectionName, HelpSource @@ -1397,7 +1397,7 @@ def get_new_keys(self): for event in key_set_changes: current_bindings[event] = key_set_changes[event].split() current_key_sequences = list(current_bindings.values()) - new_keys = GetKeysDialog(self, 'Get New Keys', bind_name, + new_keys = GetKeysWindow(self, 'Get New Keys', bind_name, current_key_sequences).result if new_keys: if self.keyset_source.get(): # Current key set is a built-in. diff --git a/Lib/idlelib/idle_test/test_config_key.py b/Lib/idlelib/idle_test/test_config_key.py index bf66cadf57cd3c..32f878b842b276 100644 --- a/Lib/idlelib/idle_test/test_config_key.py +++ b/Lib/idlelib/idle_test/test_config_key.py @@ -13,15 +13,13 @@ from idlelib.idle_test.mock_idle import Func from idlelib.idle_test.mock_tk import Mbox_func -gkd = config_key.GetKeysDialog - class ValidationTest(unittest.TestCase): "Test validation methods: ok, keys_ok, bind_ok." - class Validator(gkd): + class Validator(config_key.GetKeysFrame): def __init__(self, *args, **kwargs): - config_key.GetKeysDialog.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) class list_keys_final: get = Func() self.list_keys_final = list_keys_final @@ -34,15 +32,14 @@ def setUpClass(cls): cls.root = Tk() cls.root.withdraw() keylist = [[''], ['', '']] - cls.dialog = cls.Validator( - cls.root, 'Title', '<>', keylist, _utest=True) + cls.dialog = cls.Validator(cls.root, '<>', keylist) @classmethod def tearDownClass(cls): - cls.dialog.cancel() + del cls.dialog cls.root.update_idletasks() cls.root.destroy() - del cls.dialog, cls.root + del cls.root def setUp(self): self.dialog.showerror.message = '' @@ -111,14 +108,14 @@ def setUpClass(cls): requires('gui') cls.root = Tk() cls.root.withdraw() - cls.dialog = gkd(cls.root, 'Title', '<>', [], _utest=True) + cls.dialog = config_key.GetKeysFrame(cls.root, '<>', []) @classmethod def tearDownClass(cls): - cls.dialog.cancel() + del cls.dialog cls.root.update_idletasks() cls.root.destroy() - del cls.dialog, cls.root + del cls.root def test_toggle_level(self): dialog = self.dialog @@ -130,7 +127,7 @@ def stackorder(): this can be used to check whether a frame is above or below another one. """ - for index, child in enumerate(dialog.frame.winfo_children()): + for index, child in enumerate(dialog.winfo_children()): if child._name == 'keyseq_basic': basic = index if child._name == 'keyseq_advanced': @@ -161,7 +158,7 @@ def stackorder(): class KeySelectionTest(unittest.TestCase): "Test selecting key on Basic frames." - class Basic(gkd): + class Basic(config_key.GetKeysFrame): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) class list_keys_final: @@ -179,14 +176,14 @@ def setUpClass(cls): requires('gui') cls.root = Tk() cls.root.withdraw() - cls.dialog = cls.Basic(cls.root, 'Title', '<>', [], _utest=True) + cls.dialog = cls.Basic(cls.root, '<>', []) @classmethod def tearDownClass(cls): - cls.dialog.cancel() + del cls.dialog cls.root.update_idletasks() cls.root.destroy() - del cls.dialog, cls.root + del cls.root def setUp(self): self.dialog.clear_key_seq() @@ -206,7 +203,7 @@ def test_get_modifiers(self): dialog.modifier_checkbuttons['foo'].invoke() eq(gm(), ['BAZ']) - @mock.patch.object(gkd, 'get_modifiers') + @mock.patch.object(config_key.GetKeysFrame, 'get_modifiers') def test_build_key_string(self, mock_modifiers): dialog = self.dialog key = dialog.list_keys_final @@ -227,7 +224,7 @@ def test_build_key_string(self, mock_modifiers): dialog.build_key_string() eq(string(), '') - @mock.patch.object(gkd, 'get_modifiers') + @mock.patch.object(config_key.GetKeysFrame, 'get_modifiers') def test_final_key_selected(self, mock_modifiers): dialog = self.dialog key = dialog.list_keys_final @@ -240,7 +237,7 @@ def test_final_key_selected(self, mock_modifiers): eq(string(), '') -class CancelTest(unittest.TestCase): +class CancelWindowTest(unittest.TestCase): "Simulate user clicking [Cancel] button." @classmethod @@ -248,21 +245,89 @@ def setUpClass(cls): requires('gui') cls.root = Tk() cls.root.withdraw() - cls.dialog = gkd(cls.root, 'Title', '<>', [], _utest=True) + cls.dialog = config_key.GetKeysWindow( + cls.root, 'Title', '<>', [], _utest=True) @classmethod def tearDownClass(cls): cls.dialog.cancel() + del cls.dialog cls.root.update_idletasks() cls.root.destroy() - del cls.dialog, cls.root + del cls.root - def test_cancel(self): + @mock.patch.object(config_key.GetKeysFrame, 'ok') + def test_cancel(self, mock_frame_ok): self.assertEqual(self.dialog.winfo_class(), 'Toplevel') self.dialog.button_cancel.invoke() with self.assertRaises(TclError): self.dialog.winfo_class() self.assertEqual(self.dialog.result, '') + mock_frame_ok.assert_not_called() + + +class OKWindowTest(unittest.TestCase): + "Simulate user clicking [OK] button." + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.root.withdraw() + cls.dialog = config_key.GetKeysWindow( + cls.root, 'Title', '<>', [], _utest=True) + + @classmethod + def tearDownClass(cls): + cls.dialog.cancel() + del cls.dialog + cls.root.update_idletasks() + cls.root.destroy() + del cls.root + + @mock.patch.object(config_key.GetKeysFrame, 'ok') + def test_ok(self, mock_frame_ok): + self.assertEqual(self.dialog.winfo_class(), 'Toplevel') + self.dialog.button_ok.invoke() + with self.assertRaises(TclError): + self.dialog.winfo_class() + mock_frame_ok.assert_called() + + +class WindowResultTest(unittest.TestCase): + "Test window result get and set." + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = Tk() + cls.root.withdraw() + cls.dialog = config_key.GetKeysWindow( + cls.root, 'Title', '<>', [], _utest=True) + + @classmethod + def tearDownClass(cls): + cls.dialog.cancel() + del cls.dialog + cls.root.update_idletasks() + cls.root.destroy() + del cls.root + + def test_result(self): + dialog = self.dialog + eq = self.assertEqual + + dialog.result = '' + eq(dialog.result, '') + eq(dialog.frame.result,'') + + dialog.result = 'bar' + eq(dialog.result,'bar') + eq(dialog.frame.result,'bar') + + dialog.frame.result = 'foo' + eq(dialog.result, 'foo') + eq(dialog.frame.result,'foo') class HelperTest(unittest.TestCase): diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py index 3005ce08c9bf43..e5d5b4013fca57 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -954,8 +954,8 @@ def test_set_keys_type(self): def test_get_new_keys(self): eq = self.assertEqual d = self.page - orig_getkeysdialog = configdialog.GetKeysDialog - gkd = configdialog.GetKeysDialog = Func(return_self=True) + orig_getkeysdialog = configdialog.GetKeysWindow + gkd = configdialog.GetKeysWindow = Func(return_self=True) gnkn = d.get_new_keys_name = Func() d.button_new_keys.state(('!disabled',)) @@ -997,7 +997,7 @@ def test_get_new_keys(self): eq(d.keybinding.get(), '') del d.get_new_keys_name - configdialog.GetKeysDialog = orig_getkeysdialog + configdialog.GetKeysWindow = orig_getkeysdialog def test_get_new_keys_name(self): orig_sectionname = configdialog.SectionName From d1d6b31572151e4923db27765be3038e9b973964 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Fri, 30 Sep 2022 10:45:47 -0700 Subject: [PATCH 11/25] gh-87597: Document TimeoutExpired.stdout & .stderr types (#97685) This documents the behavior that has always been the case since timeout support was introduced in Python 3.3. --- Doc/library/subprocess.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst index 43d6ffceee8fc8..dee5bd879db5b5 100644 --- a/Doc/library/subprocess.rst +++ b/Doc/library/subprocess.rst @@ -193,7 +193,10 @@ underlying :class:`Popen` interface can be used directly. .. attribute:: output Output of the child process if it was captured by :func:`run` or - :func:`check_output`. Otherwise, ``None``. + :func:`check_output`. Otherwise, ``None``. This is always + :class:`bytes` when any output was captured regardless of the + ``text=True`` setting. It may remain ``None`` instead of ``b''`` + when no output was observed. .. attribute:: stdout @@ -202,7 +205,9 @@ underlying :class:`Popen` interface can be used directly. .. attribute:: stderr Stderr output of the child process if it was captured by :func:`run`. - Otherwise, ``None``. + Otherwise, ``None``. This is always :class:`bytes` when stderr output + was captured regardless of the ``text=True`` setting. It may remain + ``None`` instead of ``b''`` when no stderr output was observed. .. versionadded:: 3.3 From c6203f8001f92d2141fca83b2e6a4ae5296e2257 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 30 Sep 2022 12:55:40 -0700 Subject: [PATCH 12/25] GH-96827: Don't touch closed loops from executor threads (#96837) * When chaining futures, skip callback if loop closed. * When shutting down an executor, don't wake a closed loop. --- Lib/asyncio/base_events.py | 6 ++++-- Lib/asyncio/futures.py | 2 ++ .../Library/2022-09-30-15-56-20.gh-issue-96827.lzy1iw.rst | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-09-30-15-56-20.gh-issue-96827.lzy1iw.rst diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 9c9d98dbb9c509..2df9dcac8f723f 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -588,9 +588,11 @@ async def shutdown_default_executor(self, timeout=None): def _do_shutdown(self, future): try: self._default_executor.shutdown(wait=True) - self.call_soon_threadsafe(future.set_result, None) + if not self.is_closed(): + self.call_soon_threadsafe(future.set_result, None) except Exception as ex: - self.call_soon_threadsafe(future.set_exception, ex) + if not self.is_closed(): + self.call_soon_threadsafe(future.set_exception, ex) def _check_running(self): if self.is_running(): diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index 4bd9629e9eb62b..39776e3c2cce48 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -404,6 +404,8 @@ def _call_set_state(source): if dest_loop is None or dest_loop is source_loop: _set_state(destination, source) else: + if dest_loop.is_closed(): + return dest_loop.call_soon_threadsafe(_set_state, destination, source) destination.add_done_callback(_call_check_cancel) diff --git a/Misc/NEWS.d/next/Library/2022-09-30-15-56-20.gh-issue-96827.lzy1iw.rst b/Misc/NEWS.d/next/Library/2022-09-30-15-56-20.gh-issue-96827.lzy1iw.rst new file mode 100644 index 00000000000000..159ab32ffbfc34 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-09-30-15-56-20.gh-issue-96827.lzy1iw.rst @@ -0,0 +1 @@ +Avoid spurious tracebacks from :mod:`asyncio` when default executor cleanup is delayed until after the event loop is closed (e.g. as the result of a keyboard interrupt). From 67851dc7e5e5dc7525a51bb594225f03cac17834 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 30 Sep 2022 12:57:09 -0700 Subject: [PATCH 13/25] GH-97592: Fix crash in C remove_done_callback due to evil code (#97660) Evil code could cause fut_callbacks to be cleared when PyObject_RichCompareBool is called. --- Lib/test/test_asyncio/test_futures.py | 15 +++++++++++++++ .../2022-09-29-23-22-24.gh-issue-97592.tpJg_J.rst | 1 + Modules/_asynciomodule.c | 9 +++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-09-29-23-22-24.gh-issue-97592.tpJg_J.rst diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index 11d4273930804f..3dc6b658cfae8d 100644 --- a/Lib/test/test_asyncio/test_futures.py +++ b/Lib/test/test_asyncio/test_futures.py @@ -837,6 +837,21 @@ def __eq__(self, other): fut.remove_done_callback(evil()) + def test_remove_done_callbacks_list_clear(self): + # see https://github.com/python/cpython/issues/97592 for details + + fut = self._new_future() + fut.add_done_callback(str) + + for _ in range(63): + fut.add_done_callback(id) + + class evil: + def __eq__(self, other): + fut.remove_done_callback(other) + + fut.remove_done_callback(evil()) + def test_schedule_callbacks_list_mutation_1(self): # see http://bugs.python.org/issue28963 for details diff --git a/Misc/NEWS.d/next/Library/2022-09-29-23-22-24.gh-issue-97592.tpJg_J.rst b/Misc/NEWS.d/next/Library/2022-09-29-23-22-24.gh-issue-97592.tpJg_J.rst new file mode 100644 index 00000000000000..aa245cf944004e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-09-29-23-22-24.gh-issue-97592.tpJg_J.rst @@ -0,0 +1 @@ +Avoid a crash in the C version of :meth:`asyncio.Future.remove_done_callback` when an evil argument is passed. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 5a5881b873e245..909171150bdd36 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1052,7 +1052,11 @@ _asyncio_Future_remove_done_callback(FutureObj *self, PyObject *fn) return NULL; } - for (i = 0; i < PyList_GET_SIZE(self->fut_callbacks); i++) { + // Beware: PyObject_RichCompareBool below may change fut_callbacks. + // See GH-97592. + for (i = 0; + self->fut_callbacks != NULL && i < PyList_GET_SIZE(self->fut_callbacks); + i++) { int ret; PyObject *item = PyList_GET_ITEM(self->fut_callbacks, i); Py_INCREF(item); @@ -1071,7 +1075,8 @@ _asyncio_Future_remove_done_callback(FutureObj *self, PyObject *fn) } } - if (j == 0) { + // Note: fut_callbacks may have been cleared. + if (j == 0 || self->fut_callbacks == NULL) { Py_CLEAR(self->fut_callbacks); Py_DECREF(newlist); return PyLong_FromSsize_t(len + cleared_callback0); From 4c95e501fefb9e46db3aef4f6f34021017c1dde9 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Fri, 30 Sep 2022 19:32:46 -0600 Subject: [PATCH 14/25] gh-90110: Update the c-analyzer Tool (gh-97695) https://github.com/python/cpython/issues/90110 --- Tools/c-analyzer/cpython/_analyzer.py | 4 ++ Tools/c-analyzer/cpython/globals-to-fix.tsv | 71 +-------------------- 2 files changed, 7 insertions(+), 68 deletions(-) diff --git a/Tools/c-analyzer/cpython/_analyzer.py b/Tools/c-analyzer/cpython/_analyzer.py index 4a11fc99a4064d..cfe5e75f2f4df6 100644 --- a/Tools/c-analyzer/cpython/_analyzer.py +++ b/Tools/c-analyzer/cpython/_analyzer.py @@ -287,6 +287,10 @@ def _is_kwlist(decl): def _has_other_supported_type(decl): + if hasattr(decl, 'file') and decl.file.filename.endswith('.c.h'): + assert 'clinic' in decl.file.filename, (decl,) + if decl.name == '_kwtuple': + return True vartype = str(decl.vartype).split() if vartype[0] == 'struct': vartype = vartype[1:] diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index 83da54fdd28c94..196d62d361b679 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -298,69 +298,6 @@ Objects/setobject.c - _dummy_struct - Objects/setobject.c - _PySet_Dummy - Objects/sliceobject.c - _Py_EllipsisObject - -#----------------------- -# statically initialized - -# argument clinic -Objects/clinic/odictobject.c.h OrderedDict_fromkeys _kwtuple - -Objects/clinic/odictobject.c.h OrderedDict_setdefault _kwtuple - -Objects/clinic/odictobject.c.h OrderedDict_pop _kwtuple - -Objects/clinic/odictobject.c.h OrderedDict_popitem _kwtuple - -Objects/clinic/odictobject.c.h OrderedDict_move_to_end _kwtuple - -Objects/clinic/funcobject.c.h func_new _kwtuple - -Objects/clinic/longobject.c.h long_new _kwtuple - -Objects/clinic/longobject.c.h int_to_bytes _kwtuple - -Objects/clinic/longobject.c.h int_from_bytes _kwtuple - -Objects/clinic/listobject.c.h list_sort _kwtuple - -Objects/clinic/bytearrayobject.c.h bytearray___init__ _kwtuple - -Objects/clinic/bytearrayobject.c.h bytearray_translate _kwtuple - -Objects/clinic/bytearrayobject.c.h bytearray_split _kwtuple - -Objects/clinic/bytearrayobject.c.h bytearray_rsplit _kwtuple - -Objects/clinic/bytearrayobject.c.h bytearray_decode _kwtuple - -Objects/clinic/bytearrayobject.c.h bytearray_splitlines _kwtuple - -Objects/clinic/bytearrayobject.c.h bytearray_hex _kwtuple - -Objects/clinic/memoryobject.c.h memoryview _kwtuple - -Objects/clinic/memoryobject.c.h memoryview_cast _kwtuple - -Objects/clinic/memoryobject.c.h memoryview_tobytes _kwtuple - -Objects/clinic/memoryobject.c.h memoryview_hex _kwtuple - -Objects/clinic/enumobject.c.h enum_new _kwtuple - -Objects/clinic/structseq.c.h structseq_new _kwtuple - -Objects/clinic/descrobject.c.h mappingproxy_new _kwtuple - -Objects/clinic/descrobject.c.h property_init _kwtuple - -Objects/clinic/codeobject.c.h code_replace _kwtuple - -Objects/clinic/codeobject.c.h code__varname_from_oparg _kwtuple - -Objects/clinic/moduleobject.c.h module___init__ _kwtuple - -Objects/clinic/bytesobject.c.h bytes_split _kwtuple - -Objects/clinic/bytesobject.c.h bytes_rsplit _kwtuple - -Objects/clinic/bytesobject.c.h bytes_translate _kwtuple - -Objects/clinic/bytesobject.c.h bytes_decode _kwtuple - -Objects/clinic/bytesobject.c.h bytes_splitlines _kwtuple - -Objects/clinic/bytesobject.c.h bytes_hex _kwtuple - -Objects/clinic/bytesobject.c.h bytes_new _kwtuple - -Objects/clinic/unicodeobject.c.h unicode_encode _kwtuple - -Objects/clinic/unicodeobject.c.h unicode_expandtabs _kwtuple - -Objects/clinic/unicodeobject.c.h unicode_split _kwtuple - -Objects/clinic/unicodeobject.c.h unicode_rsplit _kwtuple - -Objects/clinic/unicodeobject.c.h unicode_splitlines _kwtuple - -Objects/clinic/unicodeobject.c.h unicode_new _kwtuple - -Objects/clinic/complexobject.c.h complex_new _kwtuple - -Python/clinic/traceback.c.h tb_new _kwtuple - -Python/clinic/_warnings.c.h warnings_warn _kwtuple - -Python/clinic/_warnings.c.h warnings_warn_explicit _kwtuple - -Python/clinic/bltinmodule.c.h builtin___import__ _kwtuple - -Python/clinic/bltinmodule.c.h builtin_compile _kwtuple - -Python/clinic/bltinmodule.c.h builtin_exec _kwtuple - -Python/clinic/bltinmodule.c.h builtin_pow _kwtuple - -Python/clinic/bltinmodule.c.h builtin_print _kwtuple - -Python/clinic/bltinmodule.c.h builtin_round _kwtuple - -Python/clinic/bltinmodule.c.h builtin_sum _kwtuple - -Python/clinic/import.c.h _imp_find_frozen _kwtuple - -Python/clinic/import.c.h _imp_source_hash _kwtuple - -Python/clinic/Python-tokenize.c.h tokenizeriter_new _kwtuple - -Python/clinic/sysmodule.c.h sys_addaudithook _kwtuple - -Python/clinic/sysmodule.c.h sys_set_coroutine_origin_tracking_depth _kwtuple - -Python/clinic/sysmodule.c.h sys_set_int_max_str_digits _kwtuple - - #----------------------- # cached - initialized once @@ -440,9 +377,9 @@ Python/initconfig.c - _Py_StandardStreamErrors - # lazy Objects/floatobject.c - double_format - Objects/floatobject.c - float_format - -Objects/longobject.c PyLong_FromString log_base_BASE - -Objects/longobject.c PyLong_FromString convwidth_base - -Objects/longobject.c PyLong_FromString convmultmax_base - +Objects/longobject.c long_from_non_binary_base log_base_BASE - +Objects/longobject.c long_from_non_binary_base convwidth_base - +Objects/longobject.c long_from_non_binary_base convmultmax_base - Objects/perf_trampoline.c - perf_map_file - Objects/unicodeobject.c - ucnhash_capi - Parser/action_helpers.c _PyPegen_dummy_name cache - @@ -568,7 +505,6 @@ Modules/_io/stringio.c - PyStringIO_Type - Modules/_io/textio.c - PyIncrementalNewlineDecoder_Type - Modules/_io/textio.c - PyTextIOBase_Type - Modules/_io/textio.c - PyTextIOWrapper_Type - -# XXX This should have been found by the analyzer but wasn't: Modules/_io/winconsoleio.c - PyWindowsConsoleIO_Type - Modules/_testcapi/vectorcall.c - MethodDescriptorBase_Type - Modules/_testcapi/vectorcall.c - MethodDescriptorDerived_Type - @@ -601,7 +537,6 @@ Modules/itertoolsmodule.c - ziplongest_type - # statically initializd pointer to static type # XXX should be const? -# XXX This should have been found by the analyzer but wasn't: Modules/_io/winconsoleio.c - _PyWindowsConsoleIO_Type - # initialized once From fb39e7fdfd844b98f3657b192e30876a2499776e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 1 Oct 2022 19:42:36 +0200 Subject: [PATCH 15/25] gh-90908: Document asyncio.Task.cancelling() and asyncio.Task.uncancel() (#95253) Co-authored-by: Thomas Grainger --- Doc/library/asyncio-task.rst | 202 ++++++++++++++++++---------- Lib/asyncio/tasks.py | 4 +- Lib/test/test_asyncio/test_tasks.py | 128 +++++++++++++++++- 3 files changed, 254 insertions(+), 80 deletions(-) diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 221197ea40ffd9..ade969220ea701 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -294,11 +294,13 @@ perform clean-up logic. In case :exc:`asyncio.CancelledError` is explicitly caught, it should generally be propagated when clean-up is complete. Most code can safely ignore :exc:`asyncio.CancelledError`. -Important asyncio components, like :class:`asyncio.TaskGroup` and the -:func:`asyncio.timeout` context manager, are implemented using cancellation -internally and might misbehave if a coroutine swallows -:exc:`asyncio.CancelledError`. +The asyncio components that enable structured concurrency, like +:class:`asyncio.TaskGroup` and :func:`asyncio.timeout`, +are implemented using cancellation internally and might misbehave if +a coroutine swallows :exc:`asyncio.CancelledError`. Similarly, user code +should not call :meth:`uncancel `. +.. _taskgroups: Task Groups =========== @@ -1003,76 +1005,6 @@ Task Object Deprecation warning is emitted if *loop* is not specified and there is no running event loop. - .. method:: cancel(msg=None) - - Request the Task to be cancelled. - - This arranges for a :exc:`CancelledError` exception to be thrown - into the wrapped coroutine on the next cycle of the event loop. - - The coroutine then has a chance to clean up or even deny the - request by suppressing the exception with a :keyword:`try` ... - ... ``except CancelledError`` ... :keyword:`finally` block. - Therefore, unlike :meth:`Future.cancel`, :meth:`Task.cancel` does - not guarantee that the Task will be cancelled, although - suppressing cancellation completely is not common and is actively - discouraged. - - .. versionchanged:: 3.9 - Added the *msg* parameter. - - .. deprecated-removed:: 3.11 3.14 - *msg* parameter is ambiguous when multiple :meth:`cancel` - are called with different cancellation messages. - The argument will be removed. - - .. _asyncio_example_task_cancel: - - The following example illustrates how coroutines can intercept - the cancellation request:: - - async def cancel_me(): - print('cancel_me(): before sleep') - - try: - # Wait for 1 hour - await asyncio.sleep(3600) - except asyncio.CancelledError: - print('cancel_me(): cancel sleep') - raise - finally: - print('cancel_me(): after sleep') - - async def main(): - # Create a "cancel_me" Task - task = asyncio.create_task(cancel_me()) - - # Wait for 1 second - await asyncio.sleep(1) - - task.cancel() - try: - await task - except asyncio.CancelledError: - print("main(): cancel_me is cancelled now") - - asyncio.run(main()) - - # Expected output: - # - # cancel_me(): before sleep - # cancel_me(): cancel sleep - # cancel_me(): after sleep - # main(): cancel_me is cancelled now - - .. method:: cancelled() - - Return ``True`` if the Task is *cancelled*. - - The Task is *cancelled* when the cancellation was requested with - :meth:`cancel` and the wrapped coroutine propagated the - :exc:`CancelledError` exception thrown into it. - .. method:: done() Return ``True`` if the Task is *done*. @@ -1186,3 +1118,125 @@ Task Object in the :func:`repr` output of a task object. .. versionadded:: 3.8 + + .. method:: cancel(msg=None) + + Request the Task to be cancelled. + + This arranges for a :exc:`CancelledError` exception to be thrown + into the wrapped coroutine on the next cycle of the event loop. + + The coroutine then has a chance to clean up or even deny the + request by suppressing the exception with a :keyword:`try` ... + ... ``except CancelledError`` ... :keyword:`finally` block. + Therefore, unlike :meth:`Future.cancel`, :meth:`Task.cancel` does + not guarantee that the Task will be cancelled, although + suppressing cancellation completely is not common and is actively + discouraged. + + .. versionchanged:: 3.9 + Added the *msg* parameter. + + .. deprecated-removed:: 3.11 3.14 + *msg* parameter is ambiguous when multiple :meth:`cancel` + are called with different cancellation messages. + The argument will be removed. + + .. _asyncio_example_task_cancel: + + The following example illustrates how coroutines can intercept + the cancellation request:: + + async def cancel_me(): + print('cancel_me(): before sleep') + + try: + # Wait for 1 hour + await asyncio.sleep(3600) + except asyncio.CancelledError: + print('cancel_me(): cancel sleep') + raise + finally: + print('cancel_me(): after sleep') + + async def main(): + # Create a "cancel_me" Task + task = asyncio.create_task(cancel_me()) + + # Wait for 1 second + await asyncio.sleep(1) + + task.cancel() + try: + await task + except asyncio.CancelledError: + print("main(): cancel_me is cancelled now") + + asyncio.run(main()) + + # Expected output: + # + # cancel_me(): before sleep + # cancel_me(): cancel sleep + # cancel_me(): after sleep + # main(): cancel_me is cancelled now + + .. method:: cancelled() + + Return ``True`` if the Task is *cancelled*. + + The Task is *cancelled* when the cancellation was requested with + :meth:`cancel` and the wrapped coroutine propagated the + :exc:`CancelledError` exception thrown into it. + + .. method:: uncancel() + + Decrement the count of cancellation requests to this Task. + + Returns the remaining number of cancellation requests. + + Note that once execution of a cancelled task completed, further + calls to :meth:`uncancel` are ineffective. + + .. versionadded:: 3.11 + + This method is used by asyncio's internals and isn't expected to be + used by end-user code. In particular, if a Task gets successfully + uncancelled, this allows for elements of structured concurrency like + :ref:`taskgroups` and :func:`asyncio.timeout` to continue running, + isolating cancellation to the respective structured block. + For example:: + + async def make_request_with_timeout(): + try: + async with asyncio.timeout(1): + # Structured block affected by the timeout: + await make_request() + await make_another_request() + except TimeoutError: + log("There was a timeout") + # Outer code not affected by the timeout: + await unrelated_code() + + While the block with ``make_request()`` and ``make_another_request()`` + might get cancelled due to the timeout, ``unrelated_code()`` should + continue running even in case of the timeout. This is implemented + with :meth:`uncancel`. :class:`TaskGroup` context managers use + :func:`uncancel` in a similar fashion. + + .. method:: cancelling() + + Return the number of pending cancellation requests to this Task, i.e., + the number of calls to :meth:`cancel` less the number of + :meth:`uncancel` calls. + + Note that if this number is greater than zero but the Task is + still executing, :meth:`cancelled` will still return ``False``. + This is because this number can be lowered by calling :meth:`uncancel`, + which can lead to the task not being cancelled after all if the + cancellation requests go down to zero. + + This method is used by asyncio's internals and isn't expected to be + used by end-user code. See :meth:`uncancel` for more details. + + .. versionadded:: 3.11 diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 56a355cbdc70c8..e48da0f2008829 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -243,8 +243,8 @@ def cancelling(self): def uncancel(self): """Decrement the task's count of cancellation requests. - This should be used by tasks that catch CancelledError - and wish to continue indefinitely until they are cancelled again. + This should be called by the party that called `cancel()` on the task + beforehand. Returns the remaining number of cancellation requests. """ diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index de735ba77aae96..04bdf648313148 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -521,7 +521,7 @@ async def task(): finally: loop.close() - def test_uncancel(self): + def test_uncancel_basic(self): loop = asyncio.new_event_loop() async def task(): @@ -534,17 +534,137 @@ async def task(): try: t = self.new_task(loop, task()) loop.run_until_complete(asyncio.sleep(0.01)) - self.assertTrue(t.cancel()) # Cancel first sleep + + # Cancel first sleep + self.assertTrue(t.cancel()) self.assertIn(" cancelling ", repr(t)) + self.assertEqual(t.cancelling(), 1) + self.assertFalse(t.cancelled()) # Task is still not complete loop.run_until_complete(asyncio.sleep(0.01)) - self.assertNotIn(" cancelling ", repr(t)) # after .uncancel() - self.assertTrue(t.cancel()) # Cancel second sleep + # after .uncancel() + self.assertNotIn(" cancelling ", repr(t)) + self.assertEqual(t.cancelling(), 0) + self.assertFalse(t.cancelled()) # Task is still not complete + + # Cancel second sleep + self.assertTrue(t.cancel()) + self.assertEqual(t.cancelling(), 1) + self.assertFalse(t.cancelled()) # Task is still not complete with self.assertRaises(asyncio.CancelledError): loop.run_until_complete(t) + self.assertTrue(t.cancelled()) # Finally, task complete + self.assertTrue(t.done()) + + # uncancel is no longer effective after the task is complete + t.uncancel() + self.assertTrue(t.cancelled()) + self.assertTrue(t.done()) finally: loop.close() + def test_uncancel_structured_blocks(self): + # This test recreates the following high-level structure using uncancel():: + # + # async def make_request_with_timeout(): + # try: + # async with asyncio.timeout(1): + # # Structured block affected by the timeout: + # await make_request() + # await make_another_request() + # except TimeoutError: + # pass # There was a timeout + # # Outer code not affected by the timeout: + # await unrelated_code() + + loop = asyncio.new_event_loop() + + async def make_request_with_timeout(*, sleep: float, timeout: float): + task = asyncio.current_task() + loop = task.get_loop() + + timed_out = False + structured_block_finished = False + outer_code_reached = False + + def on_timeout(): + nonlocal timed_out + timed_out = True + task.cancel() + + timeout_handle = loop.call_later(timeout, on_timeout) + try: + try: + # Structured block affected by the timeout + await asyncio.sleep(sleep) + structured_block_finished = True + finally: + timeout_handle.cancel() + if ( + timed_out + and task.uncancel() == 0 + and sys.exc_info()[0] is asyncio.CancelledError + ): + # Note the five rules that are needed here to satisfy proper + # uncancellation: + # + # 1. handle uncancellation in a `finally:` block to allow for + # plain returns; + # 2. our `timed_out` flag is set, meaning that it was our event + # that triggered the need to uncancel the task, regardless of + # what exception is raised; + # 3. we can call `uncancel()` because *we* called `cancel()` + # before; + # 4. we call `uncancel()` but we only continue converting the + # CancelledError to TimeoutError if `uncancel()` caused the + # cancellation request count go down to 0. We need to look + # at the counter vs having a simple boolean flag because our + # code might have been nested (think multiple timeouts). See + # commit 7fce1063b6e5a366f8504e039a8ccdd6944625cd for + # details. + # 5. we only convert CancelledError to TimeoutError; for other + # exceptions raised due to the cancellation (like + # a ConnectionLostError from a database client), simply + # propagate them. + # + # Those checks need to take place in this exact order to make + # sure the `cancelling()` counter always stays in sync. + # + # Additionally, the original stimulus to `cancel()` the task + # needs to be unscheduled to avoid re-cancelling the task later. + # Here we do it by cancelling `timeout_handle` in the `finally:` + # block. + raise TimeoutError + except TimeoutError: + self.assertTrue(timed_out) + + # Outer code not affected by the timeout: + outer_code_reached = True + await asyncio.sleep(0) + return timed_out, structured_block_finished, outer_code_reached + + # Test which timed out. + t1 = self.new_task(loop, make_request_with_timeout(sleep=10.0, timeout=0.1)) + timed_out, structured_block_finished, outer_code_reached = ( + loop.run_until_complete(t1) + ) + self.assertTrue(timed_out) + self.assertFalse(structured_block_finished) # it was cancelled + self.assertTrue(outer_code_reached) # task got uncancelled after leaving + # the structured block and continued until + # completion + self.assertEqual(t1.cancelling(), 0) # no pending cancellation of the outer task + + # Test which did not time out. + t2 = self.new_task(loop, make_request_with_timeout(sleep=0, timeout=10.0)) + timed_out, structured_block_finished, outer_code_reached = ( + loop.run_until_complete(t2) + ) + self.assertFalse(timed_out) + self.assertTrue(structured_block_finished) + self.assertTrue(outer_code_reached) + self.assertEqual(t2.cancelling(), 0) + def test_cancel(self): def gen(): From 0bb7e7171f97910a5df43737fa7e45f2d34e09c7 Mon Sep 17 00:00:00 2001 From: Will Hawkins <8715530+hawkinsw@users.noreply.github.com> Date: Sat, 1 Oct 2022 19:41:06 -0400 Subject: [PATCH 16/25] Fix capitalization of Unix in documentation (#96913) --- Doc/library/email.compat32-message.rst | 4 ++-- Doc/library/email.generator.rst | 4 ++-- Doc/library/multiprocessing.rst | 4 ++-- Doc/library/os.path.rst | 2 +- Doc/library/test.rst | 2 +- Doc/whatsnew/3.3.rst | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/library/email.compat32-message.rst b/Doc/library/email.compat32-message.rst index c68e773b1688aa..4eaa9d588ca35e 100644 --- a/Doc/library/email.compat32-message.rst +++ b/Doc/library/email.compat32-message.rst @@ -83,7 +83,7 @@ Here are the methods of the :class:`Message` class: Note that this method is provided as a convenience and may not always format the message the way you want. For example, by default it does not do the mangling of lines that begin with ``From`` that is - required by the unix mbox format. For more flexibility, instantiate a + required by the Unix mbox format. For more flexibility, instantiate a :class:`~email.generator.Generator` instance and use its :meth:`~email.generator.Generator.flatten` method directly. For example:: @@ -125,7 +125,7 @@ Here are the methods of the :class:`Message` class: Note that this method is provided as a convenience and may not always format the message the way you want. For example, by default it does not do the mangling of lines that begin with ``From`` that is - required by the unix mbox format. For more flexibility, instantiate a + required by the Unix mbox format. For more flexibility, instantiate a :class:`~email.generator.BytesGenerator` instance and use its :meth:`~email.generator.BytesGenerator.flatten` method directly. For example:: diff --git a/Doc/library/email.generator.rst b/Doc/library/email.generator.rst index 2d9bae6a7ee57b..34ad7b7f200af3 100644 --- a/Doc/library/email.generator.rst +++ b/Doc/library/email.generator.rst @@ -55,7 +55,7 @@ To accommodate reproducible processing of SMIME-signed messages defaults to the value of the :attr:`~email.policy.Policy.mangle_from_` setting of the *policy* (which is ``True`` for the :data:`~email.policy.compat32` policy and ``False`` for all others). - *mangle_from_* is intended for use when messages are stored in unix mbox + *mangle_from_* is intended for use when messages are stored in Unix mbox format (see :mod:`mailbox` and `WHY THE CONTENT-LENGTH FORMAT IS BAD `_). @@ -156,7 +156,7 @@ to be using :class:`BytesGenerator`, and not :class:`Generator`. defaults to the value of the :attr:`~email.policy.Policy.mangle_from_` setting of the *policy* (which is ``True`` for the :data:`~email.policy.compat32` policy and ``False`` for all others). - *mangle_from_* is intended for use when messages are stored in unix mbox + *mangle_from_* is intended for use when messages are stored in Unix mbox format (see :mod:`mailbox` and `WHY THE CONTENT-LENGTH FORMAT IS BAD `_). diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index caf24a35fdfc84..d74fe92f20d0cf 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -144,8 +144,8 @@ to start a process. These *start methods* are subprocess. See :issue:`33725`. .. versionchanged:: 3.4 - *spawn* added on all unix platforms, and *forkserver* added for - some unix platforms. + *spawn* added on all Unix platforms, and *forkserver* added for + some Unix platforms. Child processes no longer inherit all of the parents inheritable handles on Windows. diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 7c35f3cafd12e7..6d52a03ba95704 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -16,7 +16,7 @@ files see :func:`open`, and for accessing the filesystem see the :mod:`os` module. The path parameters can be passed as strings, or bytes, or any object implementing the :class:`os.PathLike` protocol. -Unlike a unix shell, Python does not do any *automatic* path expansions. +Unlike a Unix shell, Python does not do any *automatic* path expansions. Functions such as :func:`expanduser` and :func:`expandvars` can be invoked explicitly when an application desires shell-like path expansion. (See also the :mod:`glob` module.) diff --git a/Doc/library/test.rst b/Doc/library/test.rst index 53bcd7c178f947..8199a27d7d9c4e 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -1128,7 +1128,7 @@ The :mod:`test.support.socket_helper` module provides support for socket tests. .. function:: bind_unix_socket(sock, addr) - Bind a unix socket, raising :exc:`unittest.SkipTest` if + Bind a Unix socket, raising :exc:`unittest.SkipTest` if :exc:`PermissionError` is raised. diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst index 1b5b6831e4305c..2d78f81798f283 100644 --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -1898,7 +1898,7 @@ socket family on OS X. (Contributed by Michael Goderbauer in :issue:`13777`.) * New function :func:`~socket.sethostname` allows the hostname to be set - on unix systems if the calling process has sufficient privileges. + on Unix systems if the calling process has sufficient privileges. (Contributed by Ross Lagerwall in :issue:`10866`.) From 5ffc011d121d88446205e42ecde34ce962dcf7fe Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sat, 1 Oct 2022 17:55:40 -0700 Subject: [PATCH 17/25] gh-95588: Drop the safety claim from `ast.literal_eval` docs. (#95919) It was never really safe and this claim conflicts directly with the big warning in the docs about it being able to crash the interpreter. --- Doc/library/ast.rst | 24 ++++++++++++------- Lib/ast.py | 4 +++- ...2-08-12-01-12-52.gh-issue-95588.PA0FI7.rst | 6 +++++ 3 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Documentation/2022-08-12-01-12-52.gh-issue-95588.PA0FI7.rst diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 0349130d29227f..0811b3fa0e7842 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -1991,20 +1991,28 @@ and classes for traversing abstract syntax trees: .. function:: literal_eval(node_or_string) - Safely evaluate an expression node or a string containing a Python literal or + Evaluate an expression node or a string containing only a Python literal or container display. The string or node provided may only consist of the following Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, ``None`` and ``Ellipsis``. - This can be used for safely evaluating strings containing Python values from - untrusted sources without the need to parse the values oneself. It is not - capable of evaluating arbitrarily complex expressions, for example involving - operators or indexing. + This can be used for evaluating strings containing Python values without the + need to parse the values oneself. It is not capable of evaluating + arbitrarily complex expressions, for example involving operators or + indexing. + + This function had been documented as "safe" in the past without defining + what that meant. That was misleading. This is specifically designed not to + execute Python code, unlike the more general :func:`eval`. There is no + namespace, no name lookups, or ability to call out. But it is not free from + attack: A relatively small input can lead to memory exhaustion or to C stack + exhaustion, crashing the process. There is also the possibility for + excessive CPU consumption denial of service on some inputs. Calling it on + untrusted data is thus not recommended. .. warning:: - It is possible to crash the Python interpreter with a - sufficiently large/complex string due to stack depth limitations - in Python's AST compiler. + It is possible to crash the Python interpreter due to stack depth + limitations in Python's AST compiler. It can raise :exc:`ValueError`, :exc:`TypeError`, :exc:`SyntaxError`, :exc:`MemoryError` and :exc:`RecursionError` depending on the malformed diff --git a/Lib/ast.py b/Lib/ast.py index 8adb61fed45388..1a94e9368c161a 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -54,10 +54,12 @@ def parse(source, filename='', mode='exec', *, def literal_eval(node_or_string): """ - Safely evaluate an expression node or a string containing a Python + Evaluate an expression node or a string containing only a Python expression. The string or node provided may only consist of the following Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None. + + Caution: A complex expression can overflow the C stack and cause a crash. """ if isinstance(node_or_string, str): node_or_string = parse(node_or_string.lstrip(" \t"), mode='eval') diff --git a/Misc/NEWS.d/next/Documentation/2022-08-12-01-12-52.gh-issue-95588.PA0FI7.rst b/Misc/NEWS.d/next/Documentation/2022-08-12-01-12-52.gh-issue-95588.PA0FI7.rst new file mode 100644 index 00000000000000..c070bbc19517fc --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2022-08-12-01-12-52.gh-issue-95588.PA0FI7.rst @@ -0,0 +1,6 @@ +Clarified the conflicting advice given in the :mod:`ast` documentation about +:func:`ast.literal_eval` being "safe" for use on untrusted input while at +the same time warning that it can crash the process. The latter statement is +true and is deemed unfixable without a large amount of work unsuitable for a +bugfix. So we keep the warning and no longer claim that ``literal_eval`` is +safe. From e401b65cf1dd4cee3f938506903fecca53437f53 Mon Sep 17 00:00:00 2001 From: Ofey Chan Date: Sun, 2 Oct 2022 11:57:17 +0800 Subject: [PATCH 18/25] gh-97591: In `Exception.__setstate__()` acquire strong references before calling `tp_hash` slot (#97700) --- Lib/test/test_baseexception.py | 25 +++++++++++++++++++ ...2-10-01-08-55-09.gh-issue-97591.pw6kkH.rst | 2 ++ Objects/exceptions.c | 8 +++++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-10-01-08-55-09.gh-issue-97591.pw6kkH.rst diff --git a/Lib/test/test_baseexception.py b/Lib/test/test_baseexception.py index 0061b3fa8e6555..4c3cf0b964ae56 100644 --- a/Lib/test/test_baseexception.py +++ b/Lib/test/test_baseexception.py @@ -114,6 +114,31 @@ def test_interface_no_arg(self): [repr(exc), exc.__class__.__name__ + '()']) self.interface_test_driver(results) + def test_setstate_refcount_no_crash(self): + # gh-97591: Acquire strong reference before calling tp_hash slot + # in PyObject_SetAttr. + import gc + d = {} + class HashThisKeyWillClearTheDict(str): + def __hash__(self) -> int: + d.clear() + return super().__hash__() + class Value(str): + pass + exc = Exception() + + d[HashThisKeyWillClearTheDict()] = Value() # refcount of Value() is 1 now + + # Exception.__setstate__ should aquire a strong reference of key and + # value in the dict. Otherwise, Value()'s refcount would go below + # zero in the tp_hash call in PyObject_SetAttr(), and it would cause + # crash in GC. + exc.__setstate__(d) # __hash__() is called again here, clearing the dict. + + # This GC would crash if the refcount of Value() goes below zero. + gc.collect() + + class UsageTests(unittest.TestCase): """Test usage of exceptions""" diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-10-01-08-55-09.gh-issue-97591.pw6kkH.rst b/Misc/NEWS.d/next/Core and Builtins/2022-10-01-08-55-09.gh-issue-97591.pw6kkH.rst new file mode 100644 index 00000000000000..d3a5867db7fce2 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-10-01-08-55-09.gh-issue-97591.pw6kkH.rst @@ -0,0 +1,2 @@ +Fixed a missing incref/decref pair in `Exception.__setstate__()`. +Patch by Ofey Chan. diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 3703fdcda4dbe9..80e98bb4ffa43a 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -167,8 +167,14 @@ BaseException_setstate(PyObject *self, PyObject *state) return NULL; } while (PyDict_Next(state, &i, &d_key, &d_value)) { - if (PyObject_SetAttr(self, d_key, d_value) < 0) + Py_INCREF(d_key); + Py_INCREF(d_value); + int res = PyObject_SetAttr(self, d_key, d_value); + Py_DECREF(d_value); + Py_DECREF(d_key); + if (res < 0) { return NULL; + } } } Py_RETURN_NONE; From 879e866792945010b85839e45e76ab6aa2cc6088 Mon Sep 17 00:00:00 2001 From: "C.A.M. Gerlach" Date: Sun, 2 Oct 2022 00:12:56 -0500 Subject: [PATCH 19/25] gh-95975: Move except/*/finally ref labels to more precise locations (#95976) * gh-95975: Move except/*/finally ref labels to more precise locations * Add section headers to fix :keyword: role and aid navigation * Move see also to the introduction rather than a particular subsection * Fix other minor Sphinx syntax issues with except Co-authored-by: Ezio Melotti * Suppress redundant link to same section for except too * Don't link try/except/else/finally keywords if in the same section * Format try/except/finally as keywords in modified sections Co-authored-by: Ezio Melotti --- Doc/reference/compound_stmts.rst | 121 +++++++++++++++++++------------ 1 file changed, 76 insertions(+), 45 deletions(-) diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index 751c7c2dbcf257..d914686c0a1ad9 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -199,10 +199,8 @@ returns the list ``[0, 1, 2]``. .. versionchanged:: 3.11 Starred elements are now allowed in the expression list. + .. _try: -.. _except: -.. _except_star: -.. _finally: The :keyword:`!try` statement ============================= @@ -215,7 +213,7 @@ The :keyword:`!try` statement keyword: as single: : (colon); compound statement -The :keyword:`try` statement specifies exception handlers and/or cleanup code +The :keyword:`!try` statement specifies exception handlers and/or cleanup code for a group of statements: .. productionlist:: python-grammar @@ -231,40 +229,56 @@ for a group of statements: try3_stmt: "try" ":" `suite` : "finally" ":" `suite` +Additional information on exceptions can be found in section :ref:`exceptions`, +and information on using the :keyword:`raise` statement to generate exceptions +may be found in section :ref:`raise`. -The :keyword:`except` clause(s) specify one or more exception handlers. When no + +.. _except: + +:keyword:`!except` clause +------------------------- + +The :keyword:`!except` clause(s) specify one or more exception handlers. When no exception occurs in the :keyword:`try` clause, no exception handler is executed. When an exception occurs in the :keyword:`!try` suite, a search for an exception -handler is started. This search inspects the except clauses in turn until one -is found that matches the exception. An expression-less except clause, if -present, must be last; it matches any exception. For an except clause with an -expression, that expression is evaluated, and the clause matches the exception +handler is started. This search inspects the :keyword:`!except` clauses in turn +until one is found that matches the exception. +An expression-less :keyword:`!except` clause, if present, must be last; +it matches any exception. +For an :keyword:`!except` clause with an expression, +that expression is evaluated, and the clause matches the exception if the resulting object is "compatible" with the exception. An object is compatible with an exception if the object is the class or a :term:`non-virtual base class ` of the exception object, or a tuple containing an item that is the class or a non-virtual base class of the exception object. -If no except clause matches the exception, the search for an exception handler +If no :keyword:`!except` clause matches the exception, +the search for an exception handler continues in the surrounding code and on the invocation stack. [#]_ -If the evaluation of an expression in the header of an except clause raises an -exception, the original search for a handler is canceled and a search starts for +If the evaluation of an expression +in the header of an :keyword:`!except` clause raises an exception, +the original search for a handler is canceled and a search starts for the new exception in the surrounding code and on the call stack (it is treated as if the entire :keyword:`try` statement raised the exception). .. index:: single: as; except clause -When a matching except clause is found, the exception is assigned to the target -specified after the :keyword:`!as` keyword in that except clause, if present, and -the except clause's suite is executed. All except clauses must have an -executable block. When the end of this block is reached, execution continues -normally after the entire try statement. (This means that if two nested -handlers exist for the same exception, and the exception occurs in the try -clause of the inner handler, the outer handler will not handle the exception.) +When a matching :keyword:`!except` clause is found, +the exception is assigned to the target +specified after the :keyword:`!as` keyword in that :keyword:`!except` clause, +if present, and the :keyword:`!except` clause's suite is executed. +All :keyword:`!except` clauses must have an executable block. +When the end of this block is reached, execution continues +normally after the entire :keyword:`try` statement. +(This means that if two nested handlers exist for the same exception, +and the exception occurs in the :keyword:`!try` clause of the inner handler, +the outer handler will not handle the exception.) When an exception has been assigned using ``as target``, it is cleared at the -end of the except clause. This is as if :: +end of the :keyword:`!except` clause. This is as if :: except E as N: foo @@ -278,7 +292,8 @@ was translated to :: del N This means the exception must be assigned to a different name to be able to -refer to it after the except clause. Exceptions are cleared because with the +refer to it after the :keyword:`!except` clause. +Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs. @@ -286,7 +301,8 @@ keeping all locals in that frame alive until the next garbage collection occurs. module: sys object: traceback -Before an except clause's suite is executed, details about the exception are +Before an :keyword:`!except` clause's suite is executed, +details about the exception are stored in the :mod:`sys` module and can be accessed via :func:`sys.exc_info`. :func:`sys.exc_info` returns a 3-tuple consisting of the exception class, the exception instance and a traceback object (see section :ref:`types`) identifying @@ -312,17 +328,24 @@ when leaving an exception handler:: >>> print(sys.exc_info()) (None, None, None) + .. index:: keyword: except_star -The :keyword:`except*` clause(s) are used for handling -:exc:`ExceptionGroup`\ s. The exception type for matching is interpreted as in +.. _except_star: + +:keyword:`!except*` clause +-------------------------- + +The :keyword:`!except*` clause(s) are used for handling +:exc:`ExceptionGroup`\s. The exception type for matching is interpreted as in the case of :keyword:`except`, but in the case of exception groups we can have partial matches when the type matches some of the exceptions in the group. -This means that multiple except* clauses can execute, each handling part of -the exception group. Each clause executes once and handles an exception group +This means that multiple :keyword:`!except*` clauses can execute, +each handling part of the exception group. +Each clause executes once and handles an exception group of all matching exceptions. Each exception in the group is handled by at most -one except* clause, the first that matches it. :: +one :keyword:`!except*` clause, the first that matches it. :: >>> try: ... raise ExceptionGroup("eg", @@ -342,15 +365,16 @@ one except* clause, the first that matches it. :: +------------------------------------ >>> - Any remaining exceptions that were not handled by any except* clause - are re-raised at the end, combined into an exception group along with - all exceptions that were raised from within except* clauses. + Any remaining exceptions that were not handled by any :keyword:`!except*` + clause are re-raised at the end, combined into an exception group along with + all exceptions that were raised from within :keyword:`!except*` clauses. - An except* clause must have a matching type, and this type cannot be a - subclass of :exc:`BaseExceptionGroup`. It is not possible to mix except - and except* in the same :keyword:`try`. :keyword:`break`, - :keyword:`continue` and :keyword:`return` cannot appear in an except* - clause. + An :keyword:`!except*` clause must have a matching type, + and this type cannot be a subclass of :exc:`BaseExceptionGroup`. + It is not possible to mix :keyword:`except` and :keyword:`!except*` + in the same :keyword:`try`. + :keyword:`break`, :keyword:`continue` and :keyword:`return` + cannot appear in an :keyword:`!except*` clause. .. index:: @@ -359,17 +383,28 @@ one except* clause, the first that matches it. :: statement: break statement: continue +.. _except_else: + +:keyword:`!else` clause +----------------------- + The optional :keyword:`!else` clause is executed if the control flow leaves the :keyword:`try` suite, no exception was raised, and no :keyword:`return`, :keyword:`continue`, or :keyword:`break` statement was executed. Exceptions in the :keyword:`!else` clause are not handled by the preceding :keyword:`except` clauses. + .. index:: keyword: finally -If :keyword:`finally` is present, it specifies a 'cleanup' handler. The +.. _finally: + +:keyword:`!finally` clause +-------------------------- + +If :keyword:`!finally` is present, it specifies a 'cleanup' handler. The :keyword:`try` clause is executed, including any :keyword:`except` and -:keyword:`!else` clauses. If an exception occurs in any of the clauses and is +:keyword:`else` clauses. If an exception occurs in any of the clauses and is not handled, the exception is temporarily saved. The :keyword:`!finally` clause is executed. If there is a saved exception it is re-raised at the end of the :keyword:`!finally` clause. If the :keyword:`!finally` clause raises another @@ -387,7 +422,7 @@ or :keyword:`continue` statement, the saved exception is discarded:: 42 The exception information is not available to the program during execution of -the :keyword:`finally` clause. +the :keyword:`!finally` clause. .. index:: statement: return @@ -396,10 +431,10 @@ the :keyword:`finally` clause. When a :keyword:`return`, :keyword:`break` or :keyword:`continue` statement is executed in the :keyword:`try` suite of a :keyword:`!try`...\ :keyword:`!finally` -statement, the :keyword:`finally` clause is also executed 'on the way out.' +statement, the :keyword:`!finally` clause is also executed 'on the way out.' The return value of a function is determined by the last :keyword:`return` -statement executed. Since the :keyword:`finally` clause always executes, a +statement executed. Since the :keyword:`!finally` clause always executes, a :keyword:`!return` statement executed in the :keyword:`!finally` clause will always be the last one executed:: @@ -412,13 +447,9 @@ always be the last one executed:: >>> foo() 'finally' -Additional information on exceptions can be found in section :ref:`exceptions`, -and information on using the :keyword:`raise` statement to generate exceptions -may be found in section :ref:`raise`. - .. versionchanged:: 3.8 Prior to Python 3.8, a :keyword:`continue` statement was illegal in the - :keyword:`finally` clause due to a problem with the implementation. + :keyword:`!finally` clause due to a problem with the implementation. .. _with: From 299dd419216e91dafcbea16f0818ab973c11550d Mon Sep 17 00:00:00 2001 From: "C.A.M. Gerlach" Date: Sun, 2 Oct 2022 00:20:17 -0500 Subject: [PATCH 20/25] gh-97607: Fix content parsing in the impl-detail reST directive (#97652) * Don't parse content as arg in the impl-detail directive This does not change the (untranslated) output, but ensures that the doctree node metadata is correct. which fixes gh-97607 with the text not being translated. It also simplifies the code and logic and makes it consistant with the docutils built-in directives. * Remove unused branch from impl-detail directive handling no-content case This is not used anywhere in the docs and lacks a clear use case, and is more likely a mistake which is now flagged at build time. This simplifies the logic from two code paths to one, and makes the behavior consistant with similar built-in directives (e.g. the various admonition types). * Further simplify impl-detail reST directive code --- Doc/tools/extensions/pyspecific.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index da15abdf637260..8c3aa47ad1c74b 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -100,33 +100,24 @@ def source_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): class ImplementationDetail(Directive): has_content = True - required_arguments = 0 - optional_arguments = 1 final_argument_whitespace = True # This text is copied to templates/dummy.html label_text = 'CPython implementation detail:' def run(self): + self.assert_has_content() pnode = nodes.compound(classes=['impl-detail']) label = translators['sphinx'].gettext(self.label_text) content = self.content add_text = nodes.strong(label, label) - if self.arguments: - n, m = self.state.inline_text(self.arguments[0], self.lineno) - pnode.append(nodes.paragraph('', '', *(n + m))) self.state.nested_parse(content, self.content_offset, pnode) - if pnode.children and isinstance(pnode[0], nodes.paragraph): - content = nodes.inline(pnode[0].rawsource, translatable=True) - content.source = pnode[0].source - content.line = pnode[0].line - content += pnode[0].children - pnode[0].replace_self(nodes.paragraph('', '', content, - translatable=False)) - pnode[0].insert(0, add_text) - pnode[0].insert(1, nodes.Text(' ')) - else: - pnode.insert(0, nodes.paragraph('', '', add_text)) + content = nodes.inline(pnode[0].rawsource, translatable=True) + content.source = pnode[0].source + content.line = pnode[0].line + content += pnode[0].children + pnode[0].replace_self(nodes.paragraph( + '', '', add_text, nodes.Text(' '), content, translatable=False)) return [pnode] From 551b707774a261101b9f2dbfa9c1130a3f161d09 Mon Sep 17 00:00:00 2001 From: Vinay Sajip Date: Sun, 2 Oct 2022 14:26:14 +0100 Subject: [PATCH 21/25] =?UTF-8?q?[docs]=20Update=20logging=20cookbook=20wi?= =?UTF-8?q?th=20recipe=20for=20using=20a=20logger=20like=20an=20output?= =?UTF-8?q?=E2=80=A6=20(GH-97730)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/howto/logging-cookbook.rst | 76 ++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/Doc/howto/logging-cookbook.rst b/Doc/howto/logging-cookbook.rst index 5b079744df12ed..ff7ba0789608ff 100644 --- a/Doc/howto/logging-cookbook.rst +++ b/Doc/howto/logging-cookbook.rst @@ -3428,6 +3428,82 @@ the above handler, you'd pass structured data using something like this:: i = 1 logger.debug('Message %d', i, extra=extra) +How to treat a logger like an output stream +------------------------------------------- + +Sometimes, you need to interface to a third-party API which expects a file-like +object to write to, but you want to direct the API's output to a logger. You +can do this using a class which wraps a logger with a file-like API. +Here's a short script illustrating such a class: + +.. code-block:: python + + import logging + + class LoggerWriter: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message != '\n': # avoid printing bare newlines, if you like + self.logger.log(self.level, message) + + def flush(self): + # doesn't actually do anything, but might be expected of a file-like + # object - so optional depending on your situation + pass + + def close(self): + # doesn't actually do anything, but might be expected of a file-like + # object - so optional depending on your situation. You might want + # to set a flag so that later calls to write raise an exception + pass + + def main(): + logging.basicConfig(level=logging.DEBUG) + logger = logging.getLogger('demo') + info_fp = LoggerWriter(logger, logging.INFO) + debug_fp = LoggerWriter(logger, logging.DEBUG) + print('An INFO message', file=info_fp) + print('A DEBUG message', file=debug_fp) + + if __name__ == "__main__": + main() + +When this script is run, it prints + +.. code-block:: text + + INFO:demo:An INFO message + DEBUG:demo:A DEBUG message + +You could also use ``LoggerWriter`` to redirect ``sys.stdout`` and +``sys.stderr`` by doing something like this: + +.. code-block:: python + + import sys + + sys.stdout = LoggerWriter(logger, logging.INFO) + sys.stderr = LoggerWriter(logger, logging.WARNING) + +You should do this *after* configuring logging for your needs. In the above +example, the :func:`~logging.basicConfig` call does this (using the +``sys.stderr`` value *before* it is overwritten by a ``LoggerWriter`` +instance). Then, you'd get this kind of result: + +.. code-block:: pycon + + >>> print('Foo') + INFO:demo:Foo + >>> print('Bar', file=sys.stderr) + WARNING:demo:Bar + >>> + +Of course, these above examples show output according to the format used by +:func:`~logging.basicConfig`, but you can use a different formatter when you +configure logging. .. patterns-to-avoid: From b37f2cd5a9bf6a4bb89cbe6fa0ae5d744c978388 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 2 Oct 2022 22:34:26 +0300 Subject: [PATCH 22/25] Refactor tests. --- Lib/test/test_shutil.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index a74e63cb3712ac..6789fe4cc72e3a 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1568,16 +1568,30 @@ def test_tarfile_root_owner(self): finally: archive.close() + def test_make_archive_cwd_default(self): + current_dir = os.getcwd() + def archiver(base_name, base_dir, **kw): + self.assertNotIn('root_dir', kw) + self.assertEqual(base_name, 'basename') + self.assertEqual(os.getcwd(), current_dir) + raise RuntimeError() + + register_archive_format('xxx', archiver, [], 'xxx file') + try: + with no_chdir: + with self.assertRaises(RuntimeError): + make_archive('basename', 'xxx') + self.assertEqual(os.getcwd(), current_dir) + finally: + unregister_archive_format('xxx') + def test_make_archive_cwd(self): current_dir = os.getcwd() + root_dir = self.mkdtemp() def archiver(base_name, base_dir, **kw): self.assertNotIn('root_dir', kw) - if root_dir is None: - self.assertEqual(base_name, 'basename') - self.assertEqual(os.getcwd(), current_dir) - else: - self.assertEqual(base_name, os.path.join(current_dir, 'basename')) - self.assertEqual(os.getcwd(), root_dir) + self.assertEqual(base_name, os.path.join(current_dir, 'basename')) + self.assertEqual(os.getcwd(), root_dir) raise RuntimeError() dirs = [] def _chdir(path): @@ -1586,14 +1600,6 @@ def _chdir(path): register_archive_format('xxx', archiver, [], 'xxx file') try: - root_dir = None - with support.swap_attr(os, 'chdir', _chdir) as orig_chdir: - with self.assertRaises(RuntimeError): - make_archive('basename', 'xxx') - self.assertEqual(os.getcwd(), current_dir) - self.assertEqual(dirs, []) - - root_dir = self.mkdtemp() with support.swap_attr(os, 'chdir', _chdir) as orig_chdir: with self.assertRaises(RuntimeError): make_archive('basename', 'xxx', root_dir=root_dir) @@ -1611,18 +1617,13 @@ def archiver(base_name, base_dir, **kw): self.assertEqual(os.getcwd(), current_dir) raise RuntimeError() archiver.supports_root_dir = True - dirs = [] - def _chdir(path): - dirs.append(path) - orig_chdir(path) register_archive_format('xxx', archiver, [], 'xxx file') try: - with support.swap_attr(os, 'chdir', _chdir) as orig_chdir: + with no_chdir: with self.assertRaises(RuntimeError): make_archive('basename', 'xxx', root_dir=root_dir) self.assertEqual(os.getcwd(), current_dir) - self.assertEqual(dirs, []) finally: unregister_archive_format('xxx') From 9980c8cc4de371886e626a0fbb578aa80b6ac9d9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 5 Oct 2022 00:11:33 +0300 Subject: [PATCH 23/25] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Éric --- Doc/library/shutil.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index ffa3934df949c7..89f68182ef8073 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -578,7 +578,7 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules. with :func:`register_archive_format` do not support the *root_dir* argument. In this case it temporarily changes the current working directory of the process - to perform archiving. + to *root_dir* to perform archiving. .. versionchanged:: 3.8 The modern pax (POSIX.1-2001) format is now used instead of @@ -615,10 +615,10 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules. Further arguments are passed as keyword arguments: *owner*, *group*, *dry_run* and *logger* (as passed in :func:`make_archive`). - If *function* has the *supports_root_dir* attribute set to ``True``, + If *function* has the custom attribute `function.supports_root_dir` set to ``True``, the *root_dir* argument is passed as a keyword argument. Otherwise the current working directory of the process is temporarily - changed before calling *function*. + changed to *root_dir* before calling *function*. In this case :func:`make_archive` is not thread-safe. If given, *extra_args* is a sequence of ``(name, value)`` pairs that will be @@ -628,7 +628,7 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules. list of archivers. Defaults to an empty string. .. versionchanged:: 3.12 - Added support of functions supporting the *root_dir* argument. + Added support for functions supporting the *root_dir* argument. .. function:: unregister_archive_format(name) From 5160cb754b7a35a99f8af896f55c4db055aa94ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric?= Date: Tue, 4 Oct 2022 17:24:07 -0400 Subject: [PATCH 24/25] fix markup --- Doc/library/shutil.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 89f68182ef8073..b33dbe21b1fa19 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -615,7 +615,7 @@ provided. They rely on the :mod:`zipfile` and :mod:`tarfile` modules. Further arguments are passed as keyword arguments: *owner*, *group*, *dry_run* and *logger* (as passed in :func:`make_archive`). - If *function* has the custom attribute `function.supports_root_dir` set to ``True``, + If *function* has the custom attribute ``function.supports_root_dir`` set to ``True``, the *root_dir* argument is passed as a keyword argument. Otherwise the current working directory of the process is temporarily changed to *root_dir* before calling *function*. From 701f8960ebcaa5528849fba655991fd72fe0ef7d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 5 Oct 2022 11:45:37 +0300 Subject: [PATCH 25/25] Update Doc/whatsnew/3.12.rst --- Doc/whatsnew/3.12.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 952a0a491e6e0f..7b293ef12f338b 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -133,7 +133,7 @@ shutil * :func:`shutil.make_archive` now passes the *root_dir* argument to custom archivers which support it. In this case it no longer temporarily changes the current working directory - of the process to perform archiving. + of the process to *root_dir* to perform archiving. (Contributed by Serhiy Storchaka in :gh:`74696`.)