From 4de00a42661a1c280e06d2d5d71c4f69eda41562 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 9 Sep 2026 16:21:06 +0300 Subject: [PATCH 1/5] gh-157170: Document the scope of global curses settings (GH-157207) use_env() and filter() affect every screen created afterwards, slk_init() affects only the screen created next, and set_escdelay() and set_tabsize() may apply to all screens. Also add missing cross-references to newterm(), window.refresh() and doupdate(). --- Doc/library/curses.rst | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index bcf2ef47d48898..f9ffd4d376e0f5 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -326,9 +326,10 @@ Input options The curses library does "line-breakout optimization" by looking for typeahead periodically while updating the screen. If input is found, and it is coming - from a tty, the current update is postponed until refresh or doupdate is called - again, allowing faster response to commands typed in advance. This function - allows specifying a different file descriptor for typeahead checking. + from a tty, the current update is postponed until :meth:`~window.refresh` or + :func:`doupdate` is called again, allowing faster response to commands typed + in advance. This function allows specifying a different file descriptor for + typeahead checking. .. function:: is_cbreak() @@ -702,7 +703,8 @@ labels. (eight labels). Where the underlying curses library supports them, ``2`` gives 4-4-4 (twelve labels) and ``3`` gives 4-4-4 with an index line. - Must be called before :func:`initscr` or :func:`newterm`. + Must be called before :func:`initscr` or :func:`newterm`, + and affects only the screen created next. .. versionadded:: next @@ -1067,8 +1069,10 @@ Utilities .. function:: filter() - The :func:`.filter` routine, if used, must be called before :func:`initscr` is - called. The effect is that, during the initialization, :envvar:`LINES` is set to ``1``; the + The :func:`.filter` routine, if used, must be called before :func:`initscr` + or :func:`newterm` is called, + and affects every screen created afterwards. + The effect is that, during the initialization, :envvar:`LINES` is set to ``1``; the capabilities ``clear``, ``cup``, ``cud``, ``cud1``, ``cuu1``, ``cuu``, ``vpa`` are disabled; and the ``home`` string is set to the value of ``cr``. The effect is that the cursor is confined to the current line, and so are screen updates. This may be used for enabling @@ -1087,8 +1091,10 @@ Utilities .. function:: use_env(flag) - If used, this function should be called before :func:`initscr` or newterm are - called. When *flag* is ``False``, the values of lines and columns specified in the + If used, this function should be called before :func:`initscr` or + :func:`newterm` are called, + and affects every screen created afterwards. + When *flag* is ``False``, the values of lines and columns specified in the terminfo database will be used, even if environment variables :envvar:`LINES` and :envvar:`COLUMNS` (used by default) are set, or if curses is running in a window (in which case default behavior would be to use the window size if @@ -1106,6 +1112,9 @@ Utilities to distinguish between an individual escape character entered on the keyboard from escape sequences sent by cursor and function keys. + Depending on the curses library, the setting may apply to all screens, + not only to the current one. + .. versionadded:: 3.9 .. function:: get_tabsize() @@ -1119,6 +1128,9 @@ Utilities Sets the number of columns used by the curses library when converting a tab character to spaces as it adds the tab to a window. + Depending on the curses library, the setting may apply to all screens, + not only to the current one, and creating a screen may reset it. + .. versionadded:: 3.9 .. function:: napms(ms) From c42790b34f634051750e5da340d17c7da19e4784 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 9 Sep 2026 15:57:53 +0200 Subject: [PATCH 2/5] gh-155292: Skip updating unicodedata with mismatched interpreter (GH-157066) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mkstringprep uses things like str.lower(), so it generates the wrong result if run in an interpreter with a different Unicode data version than the target. This means that updating the Unicode version is a two-step process: run makeunicodedata.py, then compile, then run mkstringprep.py. The two steps can (and should) be combined when re-running regen-unicodedata to verify that the data is up to date. The GH-155292 fix only considered that case. Change makeunicodedata.py to only run mkstringprep.py when the current interpreter is up to it. Otherwise, show a reminder. As an extra complication, download the input (RFC 3454) in the "first step", since an out-of-date stringprep.py's freshness assertion may prevent downloads. Co-authored-by: Maurycy Pawłowski-Wieroński --- ...-09-07-15-13-19.gh-issue-155292._WNxL7.rst | 3 ++ Tools/unicode/makeunicodedata.py | 37 ++++++++++++++++--- Tools/unicode/mkstringprep.py | 14 +------ 3 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/Build/2026-09-07-15-13-19.gh-issue-155292._WNxL7.rst diff --git a/Misc/NEWS.d/next/Build/2026-09-07-15-13-19.gh-issue-155292._WNxL7.rst b/Misc/NEWS.d/next/Build/2026-09-07-15-13-19.gh-issue-155292._WNxL7.rst new file mode 100644 index 00000000000000..daa9746938d3b6 --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-09-07-15-13-19.gh-issue-155292._WNxL7.rst @@ -0,0 +1,3 @@ +In ``makeunicodedata.py``, the script for updating Unicode data, skip +generating ``stringprep.py`` if the current interpreter's Unicode data +version does not match the target version. diff --git a/Tools/unicode/makeunicodedata.py b/Tools/unicode/makeunicodedata.py index ab453686d4dfb1..edb5775eeb1bb6 100644 --- a/Tools/unicode/makeunicodedata.py +++ b/Tools/unicode/makeunicodedata.py @@ -40,6 +40,9 @@ SCRIPT = os.path.normpath(sys.argv[0]) VERSION = "3.3" +# Local cache location +DATA_DIR = os.path.join('Tools', 'unicode', 'data') + # The Unicode Database # -------------------- # When changing UCD version please update @@ -816,10 +819,31 @@ def makeunicodename(unicode, trace): def makestringprep(): FILE = "Lib/stringprep.py" + RFC_LOCAL = os.path.join(DATA_DIR, "rfc3454.txt") + RFC_URL = "https://www.rfc-editor.org/rfc/rfc3454.txt" + print("--- Preparing", FILE, "...") + # mkstringprep expects a local copy of RFC 3454. Download it now. + # (stringprep is used for URL handling, and if it's not matched + # with the compiled unicodedata, downloads would fail.) + if not os.path.exists(RFC_LOCAL): + download_data(RFC_LOCAL, RFC_URL) + MKSTRINGPREP = "Tools/unicode/mkstringprep.py" + # mkstringprep needs to be run with a Python version that has "its" + # unicode data, since it uses str.lower() and similar. + import unicodedata + if unicodedata.unidata_version != UNIDATA_VERSION: + print() + print("!! Skipping mkstringprep -- mismatched Unicode version !!") + print() + print("Please compile CPython with the updated Unicode database,") + print("then use that interpreter to run:") + print(f" python {MKSTRINGPREP} > {FILE}") + return + with open(FILE, "w") as f: f.truncate() subprocess.check_call([sys.executable, MKSTRINGPREP], stdout=f) @@ -929,19 +953,15 @@ class Difference(Exception):pass normalization_changes)) -DATA_DIR = os.path.join('Tools', 'unicode', 'data') - def open_data(template, version): local = os.path.join(DATA_DIR, template % ('-'+version,)) if not os.path.exists(local): - import urllib.request if version == '3.2.0': # irregular url structure url = ('https://www.unicode.org/Public/3.2-Update/'+template) % ('-'+version,) else: url = ('https://www.unicode.org/Public/%s/ucd/'+template) % (version, '') - os.makedirs(os.path.dirname(local), exist_ok=True) - urllib.request.urlretrieve(url, filename=local) + download_data(local, url) if local.endswith('.txt'): return open(local, encoding='utf-8') else: @@ -949,6 +969,13 @@ def open_data(template, version): return open(local, 'rb') +def download_data(local, url): + import urllib.request + os.makedirs(os.path.dirname(local), exist_ok=True) + print(f'Downloading {url} to {local}') + urllib.request.urlretrieve(url, filename=local) + + def expand_range(char_range: str) -> Iterator[int]: ''' Parses ranges of code points, as described in UAX #44: diff --git a/Tools/unicode/mkstringprep.py b/Tools/unicode/mkstringprep.py index 9740338fef9d3d..60052aae3b6298 100644 --- a/Tools/unicode/mkstringprep.py +++ b/Tools/unicode/mkstringprep.py @@ -1,11 +1,7 @@ import re -import os import unicodedata as unicodedata_current from unicodedata import ucd_3_2_0 as unicodedata_320 -FILENAME = "Tools/unicode/data/rfc3454.txt" -URL = "https://www.rfc-editor.org/rfc/rfc3454.txt" - def gen_category(cats): for i in range(0, 0x110000): if unicodedata_320.category(chr(i)) in cats: @@ -52,15 +48,7 @@ def compact_set(l): ############## Read the tables in the RFC ####################### -try: - data_file = open(FILENAME, encoding='utf-8') -except FileNotFoundError: - import urllib.request - os.makedirs(os.path.dirname(FILENAME), exist_ok=True) - urllib.request.urlretrieve(URL, filename=FILENAME) - data_file = open(FILENAME, encoding='utf-8') - -with data_file: +with open("Tools/unicode/data/rfc3454.txt", encoding='utf-8') as data_file: data = data_file.readlines() tables = [] From 2723799c7f357b6ba38a5cbbe96885a0a5fedacf Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 9 Sep 2026 11:33:53 -0300 Subject: [PATCH 3/5] gh-156837: Refer to yield expressions in generator function definitions (GH-156863) --- Doc/reference/datamodel.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index fde3cef63bc6e9..00a80010543be1 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -798,12 +798,12 @@ Generator functions single: generator; function single: generator; iterator -A function or method which uses the :keyword:`yield` statement (see section -:ref:`yield`) is called a :dfn:`generator function`. Such a function, when +A function or method which contains a :keyword:`yield` expression (see section +:ref:`yieldexpr`) is called a :dfn:`generator function`. Such a function, when called, always returns an :term:`iterator` object which can be used to execute the body of the function: calling the iterator's :meth:`iterator.__next__` method will cause the function to execute until -it provides a value using the :keyword:`!yield` statement. When the +it provides a value using the :keyword:`!yield` expression. When the function executes a :keyword:`return` statement or falls off the end, a :exc:`StopIteration` exception is raised and the iterator will have reached the end of the set of values to be returned. @@ -830,7 +830,7 @@ Asynchronous generator functions single: asynchronous generator; asynchronous iterator A function or method which is defined using :keyword:`async def` and -which uses the :keyword:`yield` statement is called a +which contains a :keyword:`yield` expression is called a :dfn:`asynchronous generator function`. Such a function, when called, returns an :term:`asynchronous iterator` object which can be used in an :keyword:`async for` statement to execute the body of the function. From 9f90a8659aa86bc203b7628611c79508d30e36b6 Mon Sep 17 00:00:00 2001 From: Willy Bruns Date: Wed, 9 Sep 2026 14:58:41 +0000 Subject: [PATCH 4/5] gh-156233: Fix typos and code snippet errors in the docs (#156475) --- Doc/howto/logging-cookbook.rst | 2 +- Doc/howto/mro.rst | 2 +- Doc/library/logging.config.rst | 2 +- Doc/library/pdb.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/howto/logging-cookbook.rst b/Doc/howto/logging-cookbook.rst index 2ff221cf258eda..687a0c516f6717 100644 --- a/Doc/howto/logging-cookbook.rst +++ b/Doc/howto/logging-cookbook.rst @@ -3920,7 +3920,7 @@ subclassed handler which looks something like this:: You'll need to be familiar with RFC 5424 to fully understand the above code, and it may be that you have slightly different needs (e.g. for how you pass structural data -to the log). Nevertheless, the above should be adaptable to your speciric needs. With +to the log). Nevertheless, the above should be adaptable to your specific needs. With the above handler, you'd pass structured data using something like this:: sd = { diff --git a/Doc/howto/mro.rst b/Doc/howto/mro.rst index 72cc311e30d669..5007fa046b9c79 100644 --- a/Doc/howto/mro.rst +++ b/Doc/howto/mro.rst @@ -189,7 +189,7 @@ prescription: the tail of any of the other lists, then add it to the linearization of C and remove it from the lists in the merge, otherwise look at the head of the next list and take it, if it is a good head. Then repeat - the operation until all the class are removed or it is impossible to + the operation until all the classes are removed or it is impossible to find good heads. In this case, it is impossible to construct the merge, Python 2.3 will refuse to create the class C and will raise an exception.* diff --git a/Doc/library/logging.config.rst b/Doc/library/logging.config.rst index 3842c1e9d477d2..29a001f69661ae 100644 --- a/Doc/library/logging.config.rst +++ b/Doc/library/logging.config.rst @@ -555,7 +555,7 @@ following configuration:: 'bar' : 'baz', 'spam' : 99.9, 'answer' : 42, - '.' { + '.' : { 'foo': 'bar', 'baz': 'bozz' } diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index bfe017a5c8fe1c..6a401a1af7eba3 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -834,7 +834,7 @@ can be overridden by the local file. def inner(x): 1 / x - out() + out() calling ``pdb.pm()`` will allow to move between exceptions:: From 52ffffe0a23bf0f4a57ee00377c5aeb965b3a29a Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:13:48 +0200 Subject: [PATCH 5/5] gh-156924: Try reifying lazy imports in `ForwarRef.evaluate()` (#156940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jelle Zijlstra Co-authored-by: Petr Viktorin Co-authored-by: Bartosz Sławecki --- Lib/annotationlib.py | 21 +++- Lib/test/test_annotationlib.py | 112 ++++++++++++++++++ ...-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst | 6 + 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 8204c762cce8a2..26121e3af31cb9 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -191,12 +191,27 @@ def evaluate( arg = self.__forward_arg__ if arg.isidentifier() and not keyword.iskeyword(arg): + resolved = _sentinel if arg in locals: - return locals[arg] + resolved = locals[arg] elif arg in globals: - return globals[arg] + resolved = globals[arg] elif hasattr(builtins, arg): - return getattr(builtins, arg) + resolved = getattr(builtins, arg) + + if resolved is not _sentinel: + if isinstance(resolved, types.LazyImportType): + # We try reifying the lazy object. If this fails, we propagate + # the error in the VALUE format and leave the + # ForwardRef unresolved in the FORWARDREF format. + try: + return resolved.resolve() + except Exception: + if not is_forwardref_format: + raise + return self + else: + return resolved elif is_forwardref_format: return self else: diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 530114161701b5..7bdb4a9993f4e3 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -2179,6 +2179,118 @@ def test_evaluate_forwardref_format(self): support.EqualToForwardRef('"a" + 1'), ) + def test_evaluate_lazy_import(self): + ns = {} + exec( + textwrap.dedent( + """ + lazy from test.test_lazy_import.data.basic2 import x + lazy from test.test_lazy_import.data.broken_module import y + + class A: + a: x + + class B: + b: y + """ + ), + ns, + ) + self.addCleanup( + import_helper.unload, "test.test_lazy_import.data.basic2" + ) + self.addCleanup( + import_helper.unload, "test.test_lazy_import.data.broken_module" + ) + self.assertIs(type(ns["x"]), types.LazyImportType) + self.assertIs(type(ns["y"]), types.LazyImportType) + + # The lazy import resolves successfully: + for format in (Format.VALUE, Format.FORWARDREF): + with self.subTest(format=format): + self.assertEqual( + ForwardRef("x").evaluate(globals=ns, format=format), 42 + ) + self.assertEqual( + ForwardRef("x").evaluate(locals=ns, format=format), 42 + ) + self.assertEqual( + get_annotations(ns["A"], format=Format.FORWARDREF), {"a": 42} + ) + + # The lazy import fails to resolve: + fr = ForwardRef("y") + with self.assertRaisesRegex(ValueError, "always fails to import"): + fr.evaluate(globals=ns, format=Format.VALUE) + with self.assertRaisesRegex(ValueError, "always fails to import"): + fr.evaluate(locals=ns, format=Format.VALUE) + self.assertIs(fr.evaluate(globals=ns, format=Format.FORWARDREF), fr) + self.assertIs(fr.evaluate(locals=ns, format=Format.FORWARDREF), fr) + + annos = get_annotations(ns["B"], format=Format.FORWARDREF) + self.assertEqual( + annos, + {"b": support.EqualToForwardRef("y", is_class=True, owner=ns["B"])}, + ) + with self.assertRaisesRegex(ValueError, "always fails to import"): + annos["b"].evaluate(format=Format.VALUE) + with self.assertRaisesRegex(ValueError, "always fails to import"): + get_annotations(ns["B"], format=Format.VALUE) + + def test_get_annotations_lazy_import(self): + ns = {} + exec( + textwrap.dedent( + """ + lazy from test.test_lazy_import.data.basic2 import x + lazy from test.test_lazy_import.data.broken_module import y + + class A: + a: object.fail + b: x + + class B: + a: object.fail + b: y + """ + ), + ns, + ) + self.addCleanup( + import_helper.unload, "test.test_lazy_import.data.basic2" + ) + self.addCleanup( + import_helper.unload, "test.test_lazy_import.data.broken_module" + ) + self.assertIs(type(ns["x"]), types.LazyImportType) + self.assertIs(type(ns["y"]), types.LazyImportType) + + annos = get_annotations(ns["A"], format=Format.FORWARDREF) + self.assertEqual( + annos, + { + "a": support.EqualToForwardRef( + "object.fail", is_class=True, owner=ns["A"] + ), + "b": 42, + }, + ) + + annos = get_annotations(ns["B"], format=Format.FORWARDREF) + self.assertEqual( + annos, + { + "a": support.EqualToForwardRef( + "object.fail", is_class=True, owner=ns["B"] + ), + "b": support.EqualToForwardRef( + "y", is_class=True, owner=ns["B"] + ), + }, + ) + with self.assertRaisesRegex(ValueError, "always fails to import"): + annos["b"].evaluate(format=Format.VALUE) + def test_evaluate_notimplemented_format(self): class C: x: alias diff --git a/Misc/NEWS.d/next/Library/2026-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst b/Misc/NEWS.d/next/Library/2026-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst new file mode 100644 index 00000000000000..61d0ed69857f3f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst @@ -0,0 +1,6 @@ +:meth:`annotationlib.ForwardRef.evaluate` now resolves :ref:`lazy import +` proxies found in the namespace. With +:attr:`~annotationlib.Format.FORWARDREF`, a lazy import that fails to resolve +now results in a :class:`~annotationlib.ForwardRef` instead of the +:class:`lazy import proxy `, and with :attr:`~annotationlib.Format.VALUE` the underlying exception is +propagated.