diff --git a/Doc/howto/logging-cookbook.rst b/Doc/howto/logging-cookbook.rst index 2ff221cf258eda5..687a0c516f67173 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 72cc311e30d669c..5007fa046b9c792 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/curses.rst b/Doc/library/curses.rst index bcf2ef47d48898a..f9ffd4d376e0f5b 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) diff --git a/Doc/library/logging.config.rst b/Doc/library/logging.config.rst index 3842c1e9d477d29..29a001f69661ae9 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 bfe017a5c8fe1c7..6a401a1af7eba38 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:: diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index fde3cef63bc6e90..00a80010543be17 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. diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 8204c762cce8a2b..26121e3af31cb90 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 530114161701b5a..7bdb4a9993f4e33 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/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 000000000000000..daa9746938d3b6b --- /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/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 000000000000000..61d0ed69857f3fd --- /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. diff --git a/Tools/unicode/makeunicodedata.py b/Tools/unicode/makeunicodedata.py index ab453686d4dfb1a..edb5775eeb1bb66 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 9740338fef9d3d4..60052aae3b62983 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 = []