From 791b5731d0c999d671e5a74a4c6557ddfa2a72a7 Mon Sep 17 00:00:00 2001 From: stevens Date: Tue, 11 Aug 2026 02:19:26 +0800 Subject: [PATCH 1/8] Update logging.config.listen() docstrings to mention dictConfig() (GH-155479) logging.config.listen() docstring had omitted dictConfig() support. --- Lib/logging/config.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/logging/config.py b/Lib/logging/config.py index fab91e663a0f6aa..f566de5750dbf55 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -934,8 +934,9 @@ def listen(port=DEFAULT_LOGGING_CONFIG_PORT, verify=None): Start up a socket server on the specified port, and listen for new configurations. - These will be sent as a file suitable for processing by fileConfig(). - Returns a Thread object on which you can call start() to start the server, + These will be sent as a file suitable for processing by dictConfig() or + fileConfig(). Returns a Thread object on which you can call start() to + start the server, and which you can join() when appropriate. To stop the server, call stopListening(). @@ -953,8 +954,8 @@ class ConfigStreamHandler(StreamRequestHandler): """ Handler for a logging configuration request. - It expects a completely new logging configuration and uses fileConfig - to install it. + It expects a completely new logging configuration and uses dictConfig + or fileConfig to install it. """ def handle(self): """ @@ -962,7 +963,7 @@ def handle(self): Each request is expected to be a 4-byte length, packed using struct.pack(">L", n), followed by the config file. - Uses fileConfig() to do the grunt work. + Uses dictConfig() or fileConfig() to do the grunt work. """ try: conn = self.connection From 433c842b39a9b930b7e2d6aafeca722fd68a3323 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 10 Aug 2026 20:24:48 +0200 Subject: [PATCH 2/8] gh-155491: Fix subprocess test_encoding_warning() (#155492) The test fails if run using PYTHONWARNINGS=error environment variable. Use make_clean_env() function to not inherit PYTHON environment variables. --- Lib/test/test_subprocess.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index 4cea07b3d2c7745..fc94b9a972828c1 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -2051,8 +2051,9 @@ def test_encoding_warning(self): run("echo hello", shell=True, text=True) check_output("echo hello", shell=True, text=True) """) + env = support.make_clean_env() cp = subprocess.run([sys.executable, "-Xwarn_default_encoding", "-c", code], - capture_output=True) + capture_output=True, env=env) lines = cp.stderr.splitlines() self.assertEqual(len(lines), 2, lines) self.assertStartsWith(lines[0], b":2: EncodingWarning: ") From 38235c0e8034467b9589748161d9f8ebe74d6471 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 10 Aug 2026 20:28:46 +0200 Subject: [PATCH 3/8] gh-155358: Use named attributes with test.support.script_helper (#155367) Replace assert_python_ok() result: * proc[0] => proc.rc * proc[1] => proc.out * proc[2] => proc.err --- Lib/test/test_calendar.py | 3 ++- Lib/test/test_hash.py | 4 ++-- Lib/test/test_os/test_os.py | 4 ++-- Lib/test/test_script_helper.py | 2 +- Lib/test/test_utf8_mode.py | 8 ++++---- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_calendar.py b/Lib/test/test_calendar.py index 8646cfcad58cea8..15cce2b30da5768 100644 --- a/Lib/test/test_calendar.py +++ b/Lib/test/test_calendar.py @@ -1108,7 +1108,8 @@ def run_cli_ok(self, *args): return stdout.buffer.read() def run_cmd_ok(self, *args): - return assert_python_ok('-m', 'calendar', *args)[1] + proc = assert_python_ok('-m', 'calendar', *args) + return proc.out def assertCLIFails(self, *args): with self.captured_stderr_with_buffer() as stderr: diff --git a/Lib/test/test_hash.py b/Lib/test/test_hash.py index cf9db66a29ae110..63b745f7a9f52fb 100644 --- a/Lib/test/test_hash.py +++ b/Lib/test/test_hash.py @@ -182,10 +182,10 @@ def get_hash(self, repr_, seed=None): env['PYTHONHASHSEED'] = str(seed) else: env.pop('PYTHONHASHSEED', None) - out = assert_python_ok( + proc = assert_python_ok( '-c', self.get_hash_command(repr_), **env) - stdout = out[1].strip() + stdout = proc.out.strip() return int(stdout) def test_randomized_hash(self): diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 328a0dbeb99f8fa..bcf83a314f1a6eb 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2459,8 +2459,8 @@ def get_urandom_subprocess(self, count): 'data = os.urandom(%s)' % count, 'sys.stdout.buffer.write(data)', 'sys.stdout.buffer.flush()')) - out = assert_python_ok('-c', code) - stdout = out[1] + proc = assert_python_ok('-c', code) + stdout = proc.out self.assertEqual(len(stdout), count) return stdout diff --git a/Lib/test/test_script_helper.py b/Lib/test/test_script_helper.py index eeea6c4842b4881..e65b3efdcd0a701 100644 --- a/Lib/test/test_script_helper.py +++ b/Lib/test/test_script_helper.py @@ -12,7 +12,7 @@ class TestScriptHelper(unittest.TestCase): def test_assert_python_ok(self): t = script_helper.assert_python_ok('-c', 'import sys; sys.exit(0)') - self.assertEqual(0, t[0], 'return code was not 0') + self.assertEqual(0, t.rc, 'return code was not 0') def test_assert_python_failure(self): # I didn't import the sys module so this child will fail. diff --git a/Lib/test/test_utf8_mode.py b/Lib/test/test_utf8_mode.py index b8e49440c9f7da6..6cd156b7e9293ac 100644 --- a/Lib/test/test_utf8_mode.py +++ b/Lib/test/test_utf8_mode.py @@ -29,11 +29,11 @@ def posix_locale(self): def get_output(self, *args, failure=False, **kw): kw = dict(self.DEFAULT_ENV, **kw) if failure: - out = assert_python_failure(*args, **kw) - out = out[2] + proc = assert_python_failure(*args, **kw) + out = proc.err else: - out = assert_python_ok(*args, **kw) - out = out[1] + proc = assert_python_ok(*args, **kw) + out = proc.out return out.decode().rstrip("\n\r") @unittest.skipIf(MS_WINDOWS, 'Windows has no POSIX locale') From f8cfa0cd593a0034628e36bd5b5e082af6541a78 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 10 Aug 2026 20:30:40 +0200 Subject: [PATCH 4/8] gh-155358: Use named attributes with pwd and grp modules (#155362) * Replace pwd[0] with pwd.pw_name * Replace pwd[2] with pwd.pw_uid * Replace grp[0] with grp.gr_name * Replace grp[2] with grp.gr_gid * Replace pwd[3] with pwd.pw_gid --- Doc/library/os.rst | 2 +- Lib/getpass.py | 2 +- Lib/http/server.py | 2 +- Lib/netrc.py | 2 +- Lib/shutil.py | 4 ++-- Lib/tarfile.py | 8 ++++---- Lib/test/support/smtpd.py | 2 +- Lib/test/test_getpass.py | 5 ++++- Lib/test/test_os/test_posix.py | 4 ++-- Lib/test/test_pwd.py | 2 +- Lib/test/test_shutil.py | 12 ++++++------ Lib/test/test_tarfile.py | 4 ++-- Tools/c-analyzer/c_common/fsutil.py | 2 +- 13 files changed, 27 insertions(+), 24 deletions(-) diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 0a4a02c45b533bd..fceadde7df6bf48 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -452,7 +452,7 @@ process and user. process. For most purposes, it is more useful to use :func:`getpass.getuser` since the latter checks the environment variables :envvar:`LOGNAME` or :envvar:`USERNAME` to find out who the user is, and - falls back to ``pwd.getpwuid(os.getuid())[0]`` to get the login name of the + falls back to ``pwd.getpwuid(os.getuid()).pw_name`` to get the login name of the current real user id. .. availability:: Unix, Windows, not WASI. diff --git a/Lib/getpass.py b/Lib/getpass.py index cfbd63dded6cc19..b9eec4c57abc97c 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -428,7 +428,7 @@ def getuser(): try: import pwd - return pwd.getpwuid(os.getuid())[0] + return pwd.getpwuid(os.getuid()).pw_name except (ImportError, KeyError) as e: raise OSError('No username set in the environment') from e diff --git a/Lib/http/server.py b/Lib/http/server.py index 095b5744bd12fc6..a74773bf8a12d47 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -1013,7 +1013,7 @@ def nobody_uid(): except ImportError: return -1 try: - nobody = pwd.getpwnam('nobody')[2] + nobody = pwd.getpwnam('nobody').pw_uid except KeyError: nobody = 1 + max(x[2] for x in pwd.getpwall()) return nobody diff --git a/Lib/netrc.py b/Lib/netrc.py index a28ea297df894b6..e9b5538d2c4399d 100644 --- a/Lib/netrc.py +++ b/Lib/netrc.py @@ -15,7 +15,7 @@ def _can_security_check(): def _getpwuid(uid): try: import pwd - return pwd.getpwuid(uid)[0] + return pwd.getpwuid(uid).pw_name except (ImportError, LookupError): return f'uid {uid}' diff --git a/Lib/shutil.py b/Lib/shutil.py index 94617ec296f5087..ce6969d6a4bf5a9 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -983,7 +983,7 @@ def _get_gid(name): except KeyError: result = None if result is not None: - return result[2] + return result.gr_gid return None def _get_uid(name): @@ -1001,7 +1001,7 @@ def _get_uid(name): except KeyError: result = None if result is not None: - return result[2] + return result.pw_uid return None def _make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0, diff --git a/Lib/tarfile.py b/Lib/tarfile.py index d12bd15aa2d2319..dc5c3a59744cbc4 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2282,14 +2282,14 @@ def gettarinfo(self, name=None, arcname=None, fileobj=None): if pwd: if tarinfo.uid not in self._unames: try: - self._unames[tarinfo.uid] = pwd.getpwuid(tarinfo.uid)[0] + self._unames[tarinfo.uid] = pwd.getpwuid(tarinfo.uid).pw_name except KeyError: self._unames[tarinfo.uid] = '' tarinfo.uname = self._unames[tarinfo.uid] if grp: if tarinfo.gid not in self._gnames: try: - self._gnames[tarinfo.gid] = grp.getgrgid(tarinfo.gid)[0] + self._gnames[tarinfo.gid] = grp.getgrgid(tarinfo.gid).gr_name except KeyError: self._gnames[tarinfo.gid] = '' tarinfo.gname = self._gnames[tarinfo.gid] @@ -2837,12 +2837,12 @@ def chown(self, tarinfo, targetpath, numeric_owner): if not numeric_owner: try: if grp and tarinfo.gname: - g = grp.getgrnam(tarinfo.gname)[2] + g = grp.getgrnam(tarinfo.gname).gr_gid except KeyError: pass try: if pwd and tarinfo.uname: - u = pwd.getpwnam(tarinfo.uname)[2] + u = pwd.getpwnam(tarinfo.uname).pw_uid except KeyError: pass if g is None: diff --git a/Lib/test/support/smtpd.py b/Lib/test/support/smtpd.py index 6537679db9ad24f..9800332a27f86cf 100755 --- a/Lib/test/support/smtpd.py +++ b/Lib/test/support/smtpd.py @@ -862,7 +862,7 @@ def parseargs(): except ImportError: print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr) sys.exit(1) - nobody = pwd.getpwnam('nobody')[2] + nobody = pwd.getpwnam('nobody').pw_uid try: os.setuid(nobody) except PermissionError: diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 272414a62048561..23f8a328506c6ee 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -39,10 +39,13 @@ def test_username_falls_back_to_pwd(self, environ): expected_name = 'some_name' environ.get.return_value = None if pwd: + class User: + pass with mock.patch('os.getuid') as uid, \ mock.patch('pwd.getpwuid') as getpw: uid.return_value = 42 - getpw.return_value = [expected_name] + getpw.return_value = User() + getpw.return_value.pw_name = expected_name self.assertEqual(expected_name, getpass.getuser()) getpw.assert_called_once_with(42) diff --git a/Lib/test/test_os/test_posix.py b/Lib/test/test_os/test_posix.py index f3d67027ad37277..1cc8b5d7b1c6165 100644 --- a/Lib/test/test_os/test_posix.py +++ b/Lib/test/test_os/test_posix.py @@ -1322,8 +1322,8 @@ def _create_and_do_getcwd(dirname, current_path_length = 0): @unittest.skipUnless(hasattr(pwd, 'getpwuid'), "test needs pwd.getpwuid()") @unittest.skipUnless(hasattr(os, 'getuid'), "test needs os.getuid()") def test_getgrouplist(self): - user = pwd.getpwuid(os.getuid())[0] - group = pwd.getpwuid(os.getuid())[3] + user = pwd.getpwuid(os.getuid()).pw_name + group = pwd.getpwuid(os.getuid()).pw_gid self.assertIn(group, posix.getgrouplist(user, group)) diff --git a/Lib/test/test_pwd.py b/Lib/test/test_pwd.py index bdf57776c82be13..82acce85f1db572 100644 --- a/Lib/test/test_pwd.py +++ b/Lib/test/test_pwd.py @@ -50,7 +50,7 @@ def test_values_extended(self): # check whether the entry returned by getpwuid() # for each uid is among those from getpwall() for this uid for e in entries: - if not e[0] or e[0] == '+': + if not e.pw_name or e.pw_name == '+': continue # skip NIS entries etc. self.assertIn(pwd.getpwnam(e.pw_name), entriesbyname[e.pw_name]) self.assertIn(pwd.getpwuid(e.pw_uid), entriesbyuid[e.pw_uid]) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index ed5d15ecc7ddad6..d6b3b6a642bee1e 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1999,8 +1999,8 @@ def test_make_archive_owner_group(self): # testing make_archive with owner and group, with various combinations # this works even if there's not gid/uid support if UID_GID_SUPPORT: - group = grp.getgrgid(0)[0] - owner = pwd.getpwuid(0)[0] + group = grp.getgrgid(0).gr_name + owner = pwd.getpwuid(0).pw_name else: group = owner = 'root' @@ -2027,8 +2027,8 @@ def test_make_archive_owner_group(self): def test_tarfile_root_owner(self): root_dir, base_dir = self._create_files() base_name = os.path.join(self.mkdtemp(), 'archive') - group = grp.getgrgid(0)[0] - owner = pwd.getpwuid(0)[0] + group = grp.getgrgid(0).gr_name + owner = pwd.getpwuid(0).pw_name with os_helper.change_cwd(root_dir), no_chdir: archive_name = make_archive(base_name, 'gztar', root_dir, 'dist', owner=owner, group=group) @@ -2433,8 +2433,8 @@ def check_chown(path, uid=None, gid=None): check_chown(dirname, gid=gid) try: - user = pwd.getpwuid(uid)[0] - group = grp.getgrgid(gid)[0] + user = pwd.getpwuid(uid).pw_name + group = grp.getgrgid(gid).gr_name except KeyError: # On some systems uid/gid cannot be resolved. pass diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index c86bcb79eb85d89..5fa97e2ac226c43 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -3351,12 +3351,12 @@ def root_is_uid_gid_0(): except ImportError: return False try: - if pwd.getpwuid(0)[0] != 'root': + if pwd.getpwuid(0).pw_name != 'root': return False except KeyError: # On Cygwin, there is no root user (uid 0) return False - if grp.getgrgid(0)[0] != 'root': + if grp.getgrgid(0).gr_name != 'root': return False return True diff --git a/Tools/c-analyzer/c_common/fsutil.py b/Tools/c-analyzer/c_common/fsutil.py index a8cf8d0537e40db..eb9b74d552ece00 100644 --- a/Tools/c-analyzer/c_common/fsutil.py +++ b/Tools/c-analyzer/c_common/fsutil.py @@ -411,7 +411,7 @@ def _get_user_info(user): if user is None: uid = os.geteuid() #username = os.getlogin() - username = pwd.getpwuid(uid)[0] + username = pwd.getpwuid(uid).pw_name gid = os.getgid() groups = os.getgroups() else: From 2e519ddf413018582436ca1600994887ef2345ea Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 10 Aug 2026 20:31:53 +0200 Subject: [PATCH 5/8] gh-155358: Use named attributes with os module (#155366) Replace os.stat() result: * st[1] => st.st_ino * st[6] => st.st_size * st[ST_MTIME] => st_mtime: change int type to float --- Lib/http/server.py | 2 +- Lib/test/test_os/test_posix.py | 4 ++-- Tools/c-analyzer/distutils/dep_util.py | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Lib/http/server.py b/Lib/http/server.py index a74773bf8a12d47..2fdfc78725ea4b7 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -857,7 +857,7 @@ def send_head(self): self.send_response(HTTPStatus.OK) self.send_header("Content-type", ctype) - self.send_header("Content-Length", str(fs[6])) + self.send_header("Content-Length", str(fs.st_size)) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self._send_extra_response_headers() diff --git a/Lib/test/test_os/test_posix.py b/Lib/test/test_os/test_posix.py index 1cc8b5d7b1c6165..814f945aac7453c 100644 --- a/Lib/test/test_os/test_posix.py +++ b/Lib/test/test_os/test_posix.py @@ -1823,8 +1823,8 @@ def test_link_dir_fd(self): self.skipTest('posix.link(): %s' % e) self.addCleanup(posix.unlink, fulllinkname) # should have same inodes - self.assertEqual(posix.stat(fullname)[1], - posix.stat(fulllinkname)[1]) + self.assertEqual(posix.stat(fullname).st_ino, + posix.stat(fulllinkname).st_ino) @unittest.skipUnless(os.mkdir in os.supports_dir_fd, "test needs dir_fd support in os.mkdir()") def test_mkdir_dir_fd(self): diff --git a/Tools/c-analyzer/distutils/dep_util.py b/Tools/c-analyzer/distutils/dep_util.py index 318c830f2eab3e3..a97f925d9ee4187 100644 --- a/Tools/c-analyzer/distutils/dep_util.py +++ b/Tools/c-analyzer/distutils/dep_util.py @@ -20,9 +20,8 @@ def newer (source, target): if not os.path.exists(target): return 1 - from stat import ST_MTIME - mtime1 = os.stat(source)[ST_MTIME] - mtime2 = os.stat(target)[ST_MTIME] + mtime1 = os.stat(source).st_mtime + mtime2 = os.stat(target).st_mtime return mtime1 > mtime2 From 7c906a1164573f627427213a600cabe73d7f846d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 10 Aug 2026 21:36:15 +0300 Subject: [PATCH 6/8] gh-76595: PyCapsule_Import() now imports submodules if needed (GH-6898) A submodule not imported by its package is now imported if needed. Errors raised during importing the module or looking up an attribute are now propagated instead of being replaced with generic ImportError or AttributeError. Co-Authored-By: Claude Fable 5 --- Doc/c-api/capsule.rst | 19 +++--- Lib/test/test_capi/test_capsule.py | 61 ++++++++----------- .../2018-05-16-13-47-18.bpo-32414.NODPbj.rst | 3 + Objects/capsule.c | 51 ++++++++-------- 4 files changed, 63 insertions(+), 71 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2018-05-16-13-47-18.bpo-32414.NODPbj.rst diff --git a/Doc/c-api/capsule.rst b/Doc/c-api/capsule.rst index 03a848d68ed7aba..e3ffe494c0b6ef9 100644 --- a/Doc/c-api/capsule.rst +++ b/Doc/c-api/capsule.rst @@ -108,25 +108,20 @@ Refer to :ref:`using-capsules` for more information on using these objects. Import a pointer to a C object from a capsule attribute in a module. The *name* parameter should specify the full name to the attribute, as in - ``module.attribute``. The *name* stored in the capsule must match this - string exactly. - - This function splits *name* on the ``.`` character, and imports the first - element. It then processes further elements using attribute lookups. + ``package.module.attribute``. + Modules are imported if needed, + other components are looked up as attributes. + The *name* stored in the capsule must match this string exactly. Return the capsule's internal *pointer* on success. On failure, set an exception and return ``NULL``. - .. note:: - - If *name* points to an attribute of some submodule or subpackage, this - submodule or subpackage must be previously imported using other means - (for example, by using :c:func:`PyImport_ImportModule`) for the - attribute lookups to succeed. - .. versionchanged:: 3.3 *no_block* has no effect anymore. + .. versionchanged:: next + Submodules are now imported if needed. + .. c:function:: int PyCapsule_IsValid(PyObject *capsule, const char *name) diff --git a/Lib/test/test_capi/test_capsule.py b/Lib/test/test_capi/test_capsule.py index 981caf3fad426bd..871b7b88571c349 100644 --- a/Lib/test/test_capi/test_capsule.py +++ b/Lib/test/test_capi/test_capsule.py @@ -93,12 +93,11 @@ def test_non_ascii_module_name(self): self.check_import(f'{name}.capsule') def test_submodule(self): - # Only the first component is imported; a submodule not imported - # by its package is not found. - self.assertRaises(AttributeError, - _testlimitedcapi.PyCapsule_Import, 'capsule_pkg.sub.capsule') - # It is found after explicit import. - importlib.import_module('capsule_pkg.sub') + # A submodule not imported by its package is imported if needed. + self.assertNotIn('capsule_pkg.sub', sys.modules) + self.check_import('capsule_pkg.sub.capsule') + self.assertIn('capsule_pkg.sub', sys.modules) + # It is also found if already imported. self.check_import('capsule_pkg.sub.capsule') # A submodule imported by its package is found. self.check_import('capsule_autopkg.sub.capsule') @@ -106,36 +105,34 @@ def test_submodule(self): def test_invalid_name(self): pycapsule_import = _testlimitedcapi.PyCapsule_Import # Non-existing module. - self.assertRaisesRegex(ImportError, - 'PyCapsule_Import could not import module "capsule_nonexistent"', + self.assertRaisesRegex(ModuleNotFoundError, + "No module named 'capsule_nonexistent'", pycapsule_import, 'capsule_nonexistent.capsule') # Non-UTF-8 module name. - self.assertRaisesRegex(ImportError, - 'PyCapsule_Import could not import module', - pycapsule_import, b'\xff\xfe.capsule') + self.assertRaises(UnicodeDecodeError, + pycapsule_import, b'\xff\xfe.capsule') # Empty module name. - self.assertRaisesRegex(ImportError, - 'PyCapsule_Import could not import module ""', - pycapsule_import, '.capsule_mod.capsule') + self.assertRaisesRegex(ValueError, 'Empty module name', + pycapsule_import, '.capsule_mod.capsule') # Empty name. - self.assertRaisesRegex(ImportError, - 'PyCapsule_Import could not import module ""', - pycapsule_import, '') + self.assertRaisesRegex(AttributeError, 'is not valid', + pycapsule_import, '') # Only a dot. - self.assertRaisesRegex(ImportError, - 'PyCapsule_Import could not import module ""', - pycapsule_import, '.') + self.assertRaisesRegex(ValueError, 'Empty module name', + pycapsule_import, '.') # Non-existing attribute. - self.assertRaises(AttributeError, - pycapsule_import, 'capsule_mod.nonexistent') + self.assertRaisesRegex(AttributeError, 'is not valid', + pycapsule_import, 'capsule_mod.nonexistent') # Empty attribute name. - self.assertRaises(AttributeError, pycapsule_import, 'capsule_mod.') + self.assertRaisesRegex(AttributeError, 'is not valid', + pycapsule_import, 'capsule_mod.') # Consecutive dots. - self.assertRaises(AttributeError, - pycapsule_import, 'capsule_mod..capsule') + self.assertRaisesRegex(ModuleNotFoundError, + "No module named 'capsule_mod.'", + pycapsule_import, 'capsule_mod..capsule') # Attribute of an object which is not a module. - self.assertRaises(AttributeError, - pycapsule_import, 'capsule_mod.not_capsule.capsule') + self.assertRaisesRegex(AttributeError, 'is not valid', + pycapsule_import, 'capsule_mod.not_capsule.capsule') # No attribute name. self.assertRaisesRegex(AttributeError, 'is not valid', pycapsule_import, 'capsule_mod') @@ -162,13 +159,9 @@ def test_invalid_capsule(self): pycapsule_import, 'capsule_mod.nullname') def test_error_from_import(self): - # The exception raised during importing the module is replaced - # with generic ImportError. - with self.assertRaises(ImportError) as cm: - _testlimitedcapi.PyCapsule_Import('capsule_broken.capsule') - self.assertEqual(str(cm.exception), - 'PyCapsule_Import could not import ' - 'module "capsule_broken"') + # The exception raised during importing the module is propagated. + self.assertRaises(ZeroDivisionError, + _testlimitedcapi.PyCapsule_Import, 'capsule_broken.capsule') def test_error_from_attribute_lookup(self): self.assertRaises(FloatingPointError, diff --git a/Misc/NEWS.d/next/C_API/2018-05-16-13-47-18.bpo-32414.NODPbj.rst b/Misc/NEWS.d/next/C_API/2018-05-16-13-47-18.bpo-32414.NODPbj.rst new file mode 100644 index 000000000000000..17f7a096ca8a384 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2018-05-16-13-47-18.bpo-32414.NODPbj.rst @@ -0,0 +1,3 @@ +:c:func:`PyCapsule_Import` now imports submodules if needed. Previously +names like ``package.module.attribute`` worked only if ``package.module`` +was already imported. diff --git a/Objects/capsule.c b/Objects/capsule.c index 16ae65905ef5ac0..bd90cb25f78862e 100644 --- a/Objects/capsule.c +++ b/Objects/capsule.c @@ -4,6 +4,7 @@ #include "pycore_capsule.h" // export _PyCapsule_SetTraverse() #include "pycore_gc.h" // _PyObject_GC_IS_TRACKED() #include "pycore_object.h" // _PyObject_GC_TRACK() +#include "pycore_pymem.h" // _PyMem_Strdup() /* Internal structure of PyCapsule */ @@ -227,58 +228,58 @@ _PyCapsule_SetTraverse(PyObject *op, traverseproc traverse_func, inquiry clear_f void * -PyCapsule_Import(const char *name, int no_block) +PyCapsule_Import(const char *name, int Py_UNUSED(no_block)) { PyObject *object = NULL; void *return_value = NULL; - char *trace; - size_t name_length = (strlen(name) + 1) * sizeof(char); - char *name_dup = (char *)PyMem_Malloc(name_length); + char *name_dup = _PyMem_Strdup(name); if (!name_dup) { return PyErr_NoMemory(); } - memcpy(name_dup, name, name_length); - - trace = name_dup; - while (trace) { + char *trace = name_dup; + while (1) { char *dot = strchr(trace, '.'); if (dot) { - *dot++ = '\0'; + *dot = '\0'; } - - if (object == NULL) { - object = PyImport_ImportModule(trace); - if (!object) { - PyErr_Format(PyExc_ImportError, "PyCapsule_Import could not import module \"%s\"", trace); + if (object) { + PyObject *attr; + if (PyObject_GetOptionalAttrString(object, trace, &attr) < 0) { + Py_CLEAR(object); + break; } - } else { - PyObject *object2 = PyObject_GetAttrString(object, trace); - Py_SETREF(object, object2); + Py_SETREF(object, attr); } - if (!object) { - goto EXIT; + if (!dot) { + // We are done + break; } - trace = dot; + if (!object) { + object = PyImport_ImportModule(name_dup); + if (!object) { + break; + } + } + *dot = '.'; + trace = dot + 1; } /* compare attribute name to module.name by hand */ if (PyCapsule_IsValid(object, name)) { PyCapsule *capsule = (PyCapsule *)object; return_value = capsule->pointer; - } else { + } + else if (!PyErr_Occurred()) { PyErr_Format(PyExc_AttributeError, "PyCapsule_Import \"%s\" is not valid", name); } -EXIT: Py_XDECREF(object); - if (name_dup) { - PyMem_Free(name_dup); - } + PyMem_Free(name_dup); return return_value; } From 7571c41da7505e6b551dae305b285af2a9139771 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Mon, 10 Aug 2026 21:15:45 +0200 Subject: [PATCH 7/8] gh-114905: Remove redundant assignment in ssl._create_unverified_context() (GH-103625) --- Lib/ssl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/ssl.py b/Lib/ssl.py index 3c0361330d7e951..db66c59c05cc542 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -757,8 +757,6 @@ def _create_unverified_context(protocol=None, *, cert_reqs=CERT_NONE, context.check_hostname = check_hostname if cert_reqs is not None: context.verify_mode = cert_reqs - if check_hostname: - context.check_hostname = True if keyfile and not certfile: raise ValueError("certfile must be specified") From 219768ff531fc0686de623139562ee9f9537df98 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 10 Aug 2026 21:44:50 +0200 Subject: [PATCH 8/8] gh-155358: Add test.support.check_immutable_type() (#155510) Check that sys types are immutable: * type(sys.flags) * type(sys.get_asyncgen_hooks()) * type(sys.getwindowsversion()) * type(sys.hash_info) * type(sys.version_info) --- Lib/test/support/__init__.py | 6 ++++++ Lib/test/test_decimal.py | 4 ++-- Lib/test/test_itertools.py | 3 +-- Lib/test/test_sys.py | 10 ++++++++-- Lib/test/test_xml_etree_c.py | 3 +-- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 897c210f4767e1d..c1460b806806f81 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -3465,3 +3465,9 @@ def skip_on_low_desktop_heap_memory_subprocess(returncode): if returncode == STATUS_DLL_INIT_FAILED: raise unittest.SkipTest('gh-150436: DLL init failed, likely because ' 'of low desktop heap memory') + + +def check_immutable_type(testcase, type): + regex = r'cannot set .* attribute of immutable type' + with testcase.assertRaisesRegex(TypeError, regex): + setattr(type, 'custom_attr', 123) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index b8c09c7f43e3e3b..a0ba5a8351aebdf 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -32,6 +32,7 @@ import unittest import numbers import locale +from test import support from test.support import (is_resource_enabled, requires_IEEE_754, requires_docstrings, check_disallow_instantiation) @@ -5806,8 +5807,7 @@ def test_c_immutable_types(self): ) for tp in types: with self.subTest(tp=tp): - with self.assertRaisesRegex(TypeError, "immutable"): - tp.foo = 1 + support.check_immutable_type(self, tp) def test_c_disallow_instantiation(self): ContextManager = type(C.localcontext()) diff --git a/Lib/test/test_itertools.py b/Lib/test/test_itertools.py index cf579d4da4e0dfb..d47f9acf019dca6 100644 --- a/Lib/test/test_itertools.py +++ b/Lib/test/test_itertools.py @@ -1541,8 +1541,7 @@ def test_immutable_types(self): ) for tp in dataset: with self.subTest(tp=tp): - with self.assertRaisesRegex(TypeError, "immutable"): - tp.foobar = 1 + support.check_immutable_type(self, tp) class TestExamples(unittest.TestCase): diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index dab03ef06a8b8e5..f2adce532595e70 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -425,6 +425,7 @@ def test_getwindowsversion(self): self.assertEqual(v[2], v.build) self.assertEqual(v[3], v.platform) self.assertEqual(v[4], v.service_pack) + support.check_immutable_type(self, type(v)) # This is how platform.py calls it. Make sure tuple # still has 5 elements @@ -690,6 +691,7 @@ def test_attributes(self): self.assertEqual(algo, 0) self.assertGreaterEqual(sys.hash_info.cutoff, 0) self.assertLess(sys.hash_info.cutoff, 8) + support.check_immutable_type(self, type(sys.hash_info)) self.assertIsInstance(sys.maxsize, int) self.assertIsInstance(sys.maxunicode, int) @@ -893,11 +895,13 @@ def assert_raise_on_new_sys_type(self, sys_attr): # sys.flags, sys.version_info, and sys.getwindowsversion. support.check_disallow_instantiation(self, type(sys_attr), sys_attr) - def test_sys_flags_no_instantiation(self): + def test_sys_flags_type(self): self.assert_raise_on_new_sys_type(sys.flags) + support.check_immutable_type(self, type(sys.flags)) - def test_sys_version_info_no_instantiation(self): + def test_sys_version_info_type(self): self.assert_raise_on_new_sys_type(sys.version_info) + support.check_immutable_type(self, type(sys.version_info)) def test_sys_getwindowsversion_no_instantiation(self): # Skip if not being run on Windows. @@ -1954,6 +1958,7 @@ def test_asyncgen_hooks(self): cur = sys.get_asyncgen_hooks() self.assertIsNone(cur.firstiter) self.assertIsNone(cur.finalizer) + support.check_immutable_type(self, type(cur)) # gh-118473 with self.assertRaises(TypeError): @@ -1997,6 +2002,7 @@ def write(self, s): self.assertEqual(out, b"") self.assertEqual(err, b"") + @test.support.support_remote_exec_only @test.support.cpython_only class TestRemoteExec(unittest.TestCase): diff --git a/Lib/test/test_xml_etree_c.py b/Lib/test/test_xml_etree_c.py index 270b9d6da8e7b9e..2a18396afb60881 100644 --- a/Lib/test/test_xml_etree_c.py +++ b/Lib/test/test_xml_etree_c.py @@ -194,8 +194,7 @@ def test_immutable_types(self): ) for tp in dataset: with self.subTest(tp=tp): - with self.assertRaisesRegex(TypeError, "immutable"): - tp.foo = 1 + support.check_immutable_type(self, tp) @support.cpython_only def test_disallow_instantiation(self):