From eee6589c17d0ffae32105a0ff2173798b9bd995c Mon Sep 17 00:00:00 2001 From: rimchoi Date: Sat, 16 Aug 2025 02:41:09 +0900 Subject: [PATCH 1/4] gh-137777: Disband the 'Program Frameworks' chapter (#137796) --- Doc/library/cmdlinelibs.rst | 3 ++- Doc/library/frameworks.rst | 19 +++++++------------ Doc/library/index.rst | 1 - Doc/library/tk.rst | 3 ++- Doc/library/unix.rst | 3 ++- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Doc/library/cmdlinelibs.rst b/Doc/library/cmdlinelibs.rst index 085d31af7bca1f..32f8c2c9f4ae32 100644 --- a/Doc/library/cmdlinelibs.rst +++ b/Doc/library/cmdlinelibs.rst @@ -1,7 +1,7 @@ .. _cmdlinelibs: ******************************** -Command Line Interface Libraries +Command-line interface libraries ******************************** The modules described in this chapter assist with implementing @@ -19,3 +19,4 @@ Here's an overview: curses.rst curses.ascii.rst curses.panel.rst + cmd.rst diff --git a/Doc/library/frameworks.rst b/Doc/library/frameworks.rst index 15ceeec9c255ed..f8e2f6bb18cb1c 100644 --- a/Doc/library/frameworks.rst +++ b/Doc/library/frameworks.rst @@ -1,18 +1,13 @@ +:orphan: + .. _frameworks: ****************** -Program Frameworks +Program frameworks ****************** -The modules described in this chapter are frameworks that will largely dictate -the structure of your program. Currently the modules described here are all -oriented toward writing command-line interfaces. - -The full list of modules described in this chapter is: - - -.. toctree:: +This chapter is no longer maintained, and the modules it contained have been moved to their respective topical documentation. - turtle.rst - cmd.rst - shlex.rst +* :mod:`cmd` — :doc:`Command Line Interface Libraries <./cmdlinelibs>` +* :mod:`shlex` — :doc:`Unix Specific Services <./unix>` +* :mod:`turtle` — :doc:`Graphical User Interfaces with Tk <./tk>` diff --git a/Doc/library/index.rst b/Doc/library/index.rst index 44b218948d07e1..163e1679c65ef8 100644 --- a/Doc/library/index.rst +++ b/Doc/library/index.rst @@ -63,7 +63,6 @@ the `Python Package Index `_. internet.rst mm.rst i18n.rst - frameworks.rst tk.rst development.rst debug.rst diff --git a/Doc/library/tk.rst b/Doc/library/tk.rst index 0593f8b73ea545..fa3c7e910ce21f 100644 --- a/Doc/library/tk.rst +++ b/Doc/library/tk.rst @@ -1,7 +1,7 @@ .. _tkinter: ********************************* -Graphical User Interfaces with Tk +Graphical user interfaces with Tk ********************************* .. index:: @@ -39,6 +39,7 @@ alternative `GUI frameworks and tools Date: Sat, 16 Aug 2025 02:00:43 +0800 Subject: [PATCH 2/4] gh-131178: Add tests for `site` command-line interface (GH-133582) --- Lib/test/test_site.py | 104 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py index d0e3294263557e..39c451fbbbba41 100644 --- a/Lib/test/test_site.py +++ b/Lib/test/test_site.py @@ -13,6 +13,7 @@ from test.support import socket_helper from test.support import captured_stderr from test.support.os_helper import TESTFN, EnvironmentVarGuard +from test.support.script_helper import spawn_python, kill_python import ast import builtins import glob @@ -25,6 +26,7 @@ import sys import sysconfig import tempfile +from textwrap import dedent import urllib.error import urllib.request from unittest import mock @@ -803,5 +805,107 @@ def test_underpth_dll_file(self): self.assertTrue(rc, "sys.path is incorrect") +class CommandLineTests(unittest.TestCase): + def exists(self, path): + if path is not None and os.path.isdir(path): + return "exists" + else: + return "doesn't exist" + + def get_excepted_output(self, *args): + if len(args) == 0: + user_base = site.getuserbase() + user_site = site.getusersitepackages() + output = io.StringIO() + output.write("sys.path = [\n") + for dir in sys.path: + output.write(" %r,\n" % (dir,)) + output.write("]\n") + output.write(f"USER_BASE: {user_base} ({self.exists(user_base)})\n") + output.write(f"USER_SITE: {user_site} ({self.exists(user_site)})\n") + output.write(f"ENABLE_USER_SITE: {site.ENABLE_USER_SITE}\n") + return 0, dedent(output.getvalue()).strip() + + buffer = [] + if '--user-base' in args: + buffer.append(site.getuserbase()) + if '--user-site' in args: + buffer.append(site.getusersitepackages()) + + if buffer: + return_code = 3 + if site.ENABLE_USER_SITE: + return_code = 0 + elif site.ENABLE_USER_SITE is False: + return_code = 1 + elif site.ENABLE_USER_SITE is None: + return_code = 2 + output = os.pathsep.join(buffer) + return return_code, os.path.normpath(dedent(output).strip()) + else: + return 10, None + + def invoke_command_line(self, *args): + args = ["-m", "site", *args] + + with EnvironmentVarGuard() as env: + env["PYTHONUTF8"] = "1" + env["PYTHONIOENCODING"] = "utf-8" + proc = spawn_python(*args, text=True, env=env, + encoding='utf-8', errors='replace') + + output = kill_python(proc) + return_code = proc.returncode + return return_code, os.path.normpath(dedent(output).strip()) + + @support.requires_subprocess() + def test_no_args(self): + return_code, output = self.invoke_command_line() + excepted_return_code, _ = self.get_excepted_output() + self.assertEqual(return_code, excepted_return_code) + lines = output.splitlines() + self.assertEqual(lines[0], "sys.path = [") + self.assertEqual(lines[-4], "]") + excepted_base = f"USER_BASE: '{site.getuserbase()}'" +\ + f" ({self.exists(site.getuserbase())})" + self.assertEqual(lines[-3], excepted_base) + excepted_site = f"USER_SITE: '{site.getusersitepackages()}'" +\ + f" ({self.exists(site.getusersitepackages())})" + self.assertEqual(lines[-2], excepted_site) + self.assertEqual(lines[-1], f"ENABLE_USER_SITE: {site.ENABLE_USER_SITE}") + + @support.requires_subprocess() + def test_unknown_args(self): + return_code, output = self.invoke_command_line("--unknown-arg") + excepted_return_code, _ = self.get_excepted_output("--unknown-arg") + self.assertEqual(return_code, excepted_return_code) + self.assertIn('[--user-base] [--user-site]', output) + + @support.requires_subprocess() + def test_base_arg(self): + return_code, output = self.invoke_command_line("--user-base") + excepted = self.get_excepted_output("--user-base") + excepted_return_code, excepted_output = excepted + self.assertEqual(return_code, excepted_return_code) + self.assertEqual(output, excepted_output) + + @support.requires_subprocess() + def test_site_arg(self): + return_code, output = self.invoke_command_line("--user-site") + excepted = self.get_excepted_output("--user-site") + excepted_return_code, excepted_output = excepted + self.assertEqual(return_code, excepted_return_code) + self.assertEqual(output, excepted_output) + + @support.requires_subprocess() + def test_both_args(self): + return_code, output = self.invoke_command_line("--user-base", + "--user-site") + excepted = self.get_excepted_output("--user-base", "--user-site") + excepted_return_code, excepted_output = excepted + self.assertEqual(return_code, excepted_return_code) + self.assertEqual(output, excepted_output) + + if __name__ == "__main__": unittest.main() From d86c2257a69a8d6c650c0db470499463131a569f Mon Sep 17 00:00:00 2001 From: Nick Burns Date: Fri, 15 Aug 2025 13:47:46 -0700 Subject: [PATCH 3/4] gh-92936: update `http.cookies` docs post GH-113663 (#137566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add versionchanged and example with quotes in cookie value * update whatsnew with http.cookies change * Update Doc/library/http.cookies.rst Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * Update Doc/whatsnew/3.15.rst Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * spelling, quote * demonstrate json * Update Doc/library/http.cookies.rst Co-authored-by: Senthil Kumaran * Apply suggestions from code review Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> * shorter description --------- Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Senthil Kumaran Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- Doc/library/http.cookies.rst | 11 ++++++++++- Doc/whatsnew/3.15.rst | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst index 46efc45c5e7d96..9e7648ef6d8345 100644 --- a/Doc/library/http.cookies.rst +++ b/Doc/library/http.cookies.rst @@ -28,8 +28,10 @@ The character set, :data:`string.ascii_letters`, :data:`string.digits` and in a cookie name (as :attr:`~Morsel.key`). .. versionchanged:: 3.3 - Allowed ':' as a valid cookie name character. + Allowed '``:``' as a valid cookie name character. +.. versionchanged:: next + Allowed '``"``' as a valid cookie value character. .. note:: @@ -314,3 +316,10 @@ The following example demonstrates how to use the :mod:`http.cookies` module. >>> print(C) Set-Cookie: number=7 Set-Cookie: string=seven + >>> import json + >>> C = cookies.SimpleCookie() + >>> C.load(f'cookies=7; mixins="{json.dumps({"chips": "dark chocolate"})}"; state=gooey') + >>> print(C) + Set-Cookie: cookies=7 + Set-Cookie: mixins="{"chips": "dark chocolate"}" + Set-Cookie: state=gooey diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 6c5ab1bb1a1078..252d8966b7450f 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -267,6 +267,13 @@ http.client (Contributed by Alexander Enrique Urieles Nieto in :gh:`131724`.) +http.cookies +------------ + +* Allow '``"``' double quotes in cookie values. + (Contributed by Nick Burns and Senthil Kumaran in :gh:`92936`.) + + math ---- From ec4021c6d73407fd4d22ee1a4f49d68835ef0770 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 15 Aug 2025 14:19:23 -0700 Subject: [PATCH 4/4] gh-120492: Sync importlib_metadata 8.2.0 (#124033) * Sync with importlib_metadata 8.2.0 Removes deprecated behaviors, including support for `PackageMetadata.__getitem__` returning None for missing keys and Distribution subclasses not implementing abstract methods. Prioritizes valid dists to invalid dists when retrieving by name (python/cpython/#120492). Adds SimplePath to `importlib.metadata.__all__`. * Add blurb --- Lib/importlib/metadata/__init__.py | 38 +++---- Lib/importlib/metadata/_adapters.py | 22 ++--- Lib/importlib/metadata/_itertools.py | 98 +++++++++++++++++++ Lib/test/test_importlib/metadata/test_api.py | 17 +--- Lib/test/test_importlib/metadata/test_main.py | 38 ++++--- ...-09-13-09-43-15.gh-issue-120492.Mm6CJ6.rst | 2 + ...4-09-13-09-46-47.gh-issue-91216.LuOsF4.rst | 2 + ...-09-13-09-48-25.gh-issue-124033.WNudS0.rst | 1 + 8 files changed, 154 insertions(+), 64 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-09-13-09-43-15.gh-issue-120492.Mm6CJ6.rst create mode 100644 Misc/NEWS.d/next/Library/2024-09-13-09-46-47.gh-issue-91216.LuOsF4.rst create mode 100644 Misc/NEWS.d/next/Library/2024-09-13-09-48-25.gh-issue-124033.WNudS0.rst diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index 8ce62dd864fc27..b59587e80165e5 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -12,7 +12,6 @@ import zipfile import operator import textwrap -import warnings import functools import itertools import posixpath @@ -21,7 +20,7 @@ from . import _meta from ._collections import FreezableDefaultDict, Pair from ._functools import method_cache, pass_none -from ._itertools import always_iterable, unique_everseen +from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath from contextlib import suppress @@ -35,6 +34,7 @@ 'DistributionFinder', 'PackageMetadata', 'PackageNotFoundError', + 'SimplePath', 'distribution', 'distributions', 'entry_points', @@ -329,27 +329,7 @@ def __repr__(self) -> str: return f'' -class DeprecatedNonAbstract: - # Required until Python 3.14 - def __new__(cls, *args, **kwargs): - all_names = { - name for subclass in inspect.getmro(cls) for name in vars(subclass) - } - abstract = { - name - for name in all_names - if getattr(getattr(cls, name), '__isabstractmethod__', False) - } - if abstract: - warnings.warn( - f"Unimplemented abstract methods {abstract}", - DeprecationWarning, - stacklevel=2, - ) - return super().__new__(cls) - - -class Distribution(DeprecatedNonAbstract): +class Distribution(metaclass=abc.ABCMeta): """ An abstract Python distribution package. @@ -404,7 +384,7 @@ def from_name(cls, name: str) -> Distribution: if not name: raise ValueError("A distribution name is required.") try: - return next(iter(cls.discover(name=name))) + return next(iter(cls._prefer_valid(cls.discover(name=name)))) except StopIteration: raise PackageNotFoundError(name) @@ -428,6 +408,16 @@ def discover( resolver(context) for resolver in cls._discover_resolvers() ) + @staticmethod + def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]: + """ + Prefer (move to the front) distributions that have metadata. + + Ref python/importlib_resources#489. + """ + buckets = bucket(dists, lambda dist: bool(dist.metadata)) + return itertools.chain(buckets[True], buckets[False]) + @staticmethod def at(path: str | os.PathLike[str]) -> Distribution: """Return a Distribution for the indicated metadata path. diff --git a/Lib/importlib/metadata/_adapters.py b/Lib/importlib/metadata/_adapters.py index 591168808953ba..6223263ed53f22 100644 --- a/Lib/importlib/metadata/_adapters.py +++ b/Lib/importlib/metadata/_adapters.py @@ -1,5 +1,3 @@ -import functools -import warnings import re import textwrap import email.message @@ -7,15 +5,6 @@ from ._text import FoldedCase -# Do not remove prior to 2024-01-01 or Python 3.14 -_warn = functools.partial( - warnings.warn, - "Implicit None on return values is deprecated and will raise KeyErrors.", - DeprecationWarning, - stacklevel=2, -) - - class Message(email.message.Message): multiple_use_keys = set( map( @@ -52,12 +41,17 @@ def __iter__(self): def __getitem__(self, item): """ - Warn users that a ``KeyError`` can be expected when a - missing key is supplied. Ref python/importlib_metadata#371. + Override parent behavior to typical dict behavior. + + ``email.message.Message`` will emit None values for missing + keys. Typical mappings, including this ``Message``, will raise + a key error for missing keys. + + Ref python/importlib_metadata#371. """ res = super().__getitem__(item) if res is None: - _warn() + raise KeyError(item) return res def _repair_headers(self): diff --git a/Lib/importlib/metadata/_itertools.py b/Lib/importlib/metadata/_itertools.py index d4ca9b9140e3f0..79d37198ce7aff 100644 --- a/Lib/importlib/metadata/_itertools.py +++ b/Lib/importlib/metadata/_itertools.py @@ -1,3 +1,4 @@ +from collections import defaultdict, deque from itertools import filterfalse @@ -71,3 +72,100 @@ def always_iterable(obj, base_type=(str, bytes)): return iter(obj) except TypeError: return iter((obj,)) + + +# Copied from more_itertools 10.3 +class bucket: + """Wrap *iterable* and return an object that buckets the iterable into + child iterables based on a *key* function. + + >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] + >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character + >>> sorted(list(s)) # Get the keys + ['a', 'b', 'c'] + >>> a_iterable = s['a'] + >>> next(a_iterable) + 'a1' + >>> next(a_iterable) + 'a2' + >>> list(s['b']) + ['b1', 'b2', 'b3'] + + The original iterable will be advanced and its items will be cached until + they are used by the child iterables. This may require significant storage. + + By default, attempting to select a bucket to which no items belong will + exhaust the iterable and cache all values. + If you specify a *validator* function, selected buckets will instead be + checked against it. + + >>> from itertools import count + >>> it = count(1, 2) # Infinite sequence of odd numbers + >>> key = lambda x: x % 10 # Bucket by last digit + >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only + >>> s = bucket(it, key=key, validator=validator) + >>> 2 in s + False + >>> list(s[2]) + [] + + """ + + def __init__(self, iterable, key, validator=None): + self._it = iter(iterable) + self._key = key + self._cache = defaultdict(deque) + self._validator = validator or (lambda x: True) + + def __contains__(self, value): + if not self._validator(value): + return False + + try: + item = next(self[value]) + except StopIteration: + return False + else: + self._cache[value].appendleft(item) + + return True + + def _get_values(self, value): + """ + Helper to yield items from the parent iterator that match *value*. + Items that don't match are stored in the local cache as they + are encountered. + """ + while True: + # If we've cached some items that match the target value, emit + # the first one and evict it from the cache. + if self._cache[value]: + yield self._cache[value].popleft() + # Otherwise we need to advance the parent iterator to search for + # a matching item, caching the rest. + else: + while True: + try: + item = next(self._it) + except StopIteration: + return + item_value = self._key(item) + if item_value == value: + yield item + break + elif self._validator(item_value): + self._cache[item_value].append(item) + + def __iter__(self): + for item in self._it: + item_value = self._key(item) + if self._validator(item_value): + self._cache[item_value].append(item) + + yield from self._cache.keys() + + def __getitem__(self, value): + if not self._validator(value): + return iter(()) + + return self._get_values(value) diff --git a/Lib/test/test_importlib/metadata/test_api.py b/Lib/test/test_importlib/metadata/test_api.py index 2256e0c502e46f..813febf269593b 100644 --- a/Lib/test/test_importlib/metadata/test_api.py +++ b/Lib/test/test_importlib/metadata/test_api.py @@ -1,9 +1,7 @@ import re import textwrap import unittest -import warnings import importlib -import contextlib from . import fixtures from importlib.metadata import ( @@ -18,13 +16,6 @@ ) -@contextlib.contextmanager -def suppress_known_deprecation(): - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('default', category=DeprecationWarning) - yield ctx - - class APITests( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoToplevel, @@ -153,13 +144,13 @@ def test_metadata_for_this_package(self): classifiers = md.get_all('Classifier') assert 'Topic :: Software Development :: Libraries' in classifiers - def test_missing_key_legacy(self): + def test_missing_key(self): """ - Requesting a missing key will still return None, but warn. + Requesting a missing key raises KeyError. """ md = metadata('distinfo-pkg') - with suppress_known_deprecation(): - assert md['does-not-exist'] is None + with self.assertRaises(KeyError): + md['does-not-exist'] def test_get_key(self): """ diff --git a/Lib/test/test_importlib/metadata/test_main.py b/Lib/test/test_importlib/metadata/test_main.py index e4218076f8cb0e..a0bc8222d5ba24 100644 --- a/Lib/test/test_importlib/metadata/test_main.py +++ b/Lib/test/test_importlib/metadata/test_main.py @@ -1,10 +1,8 @@ import re import pickle import unittest -import warnings import importlib import importlib.metadata -import contextlib from test.support import os_helper try: @@ -13,7 +11,6 @@ from .stubs import fake_filesystem_unittest as ffs from . import fixtures -from ._context import suppress from ._path import Symlink from importlib.metadata import ( Distribution, @@ -28,13 +25,6 @@ ) -@contextlib.contextmanager -def suppress_known_deprecation(): - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('default', category=DeprecationWarning) - yield ctx - - class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): version_pattern = r'\d+\.\d+(\.\d)?' @@ -59,9 +49,6 @@ def test_package_not_found_mentions_metadata(self): assert "metadata" in str(ctx.exception) - # expected to fail until ABC is enforced - @suppress(AssertionError) - @suppress_known_deprecation() def test_abc_enforced(self): with self.assertRaises(TypeError): type('DistributionSubclass', (Distribution,), {})() @@ -146,6 +133,31 @@ def test_unique_distributions(self): assert len(after) == len(before) +class InvalidMetadataTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): + @staticmethod + def make_pkg(name, files=dict(METADATA="VERSION: 1.0")): + """ + Create metadata for a dist-info package with name and files. + """ + return { + f'{name}.dist-info': files, + } + + def test_valid_dists_preferred(self): + """ + Dists with metadata should be preferred when discovered by name. + + Ref python/importlib_metadata#489. + """ + # create three dists with the valid one in the middle (lexicographically) + # such that on most file systems, the valid one is never naturally first. + fixtures.build_files(self.make_pkg('foo-4.0', files={}), self.site_dir) + fixtures.build_files(self.make_pkg('foo-4.1'), self.site_dir) + fixtures.build_files(self.make_pkg('foo-4.2', files={}), self.site_dir) + dist = Distribution.from_name('foo') + assert dist.version == "1.0" + + class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod def pkg_with_non_ascii_description(site_dir): diff --git a/Misc/NEWS.d/next/Library/2024-09-13-09-43-15.gh-issue-120492.Mm6CJ6.rst b/Misc/NEWS.d/next/Library/2024-09-13-09-43-15.gh-issue-120492.Mm6CJ6.rst new file mode 100644 index 00000000000000..a9652b9fcfc354 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-13-09-43-15.gh-issue-120492.Mm6CJ6.rst @@ -0,0 +1,2 @@ +``importlib.metadata`` now prioritizes valid dists to invalid dists when +retrieving by name. diff --git a/Misc/NEWS.d/next/Library/2024-09-13-09-46-47.gh-issue-91216.LuOsF4.rst b/Misc/NEWS.d/next/Library/2024-09-13-09-46-47.gh-issue-91216.LuOsF4.rst new file mode 100644 index 00000000000000..bb90588b2e7a77 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-13-09-46-47.gh-issue-91216.LuOsF4.rst @@ -0,0 +1,2 @@ +``importlib.metadata`` now raises a ``KeyError`` instead of returning +``None`` when a key is missing from the metadata. diff --git a/Misc/NEWS.d/next/Library/2024-09-13-09-48-25.gh-issue-124033.WNudS0.rst b/Misc/NEWS.d/next/Library/2024-09-13-09-48-25.gh-issue-124033.WNudS0.rst new file mode 100644 index 00000000000000..f422ab01a5f113 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-13-09-48-25.gh-issue-124033.WNudS0.rst @@ -0,0 +1 @@ +``SimplePath`` is now presented in ``importlib.metadata.__all__``.