From 89d3b2010cf848cb72d438cd2b269a542cb189d5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 7 Aug 2026 17:49:03 +0200 Subject: [PATCH 1/8] gh-155358: [WIP] Deprecate tuple API of structseq objects --- Include/internal/pycore_interp_structs.h | 12 ++ Include/internal/pycore_pylifecycle.h | 2 +- Include/internal/pycore_structseq.h | 3 +- Lib/http/server.py | 2 +- Lib/test/test_grp.py | 14 ++- Lib/test/test_os/test_os.py | 22 ++-- Lib/test/test_os/test_posix.py | 4 +- Lib/test/test_pwd.py | 22 ++-- Lib/test/test_structseq.py | 9 +- Lib/test/test_sys.py | 18 ++- Lib/test/test_time.py | 3 +- Modules/_cursesmodule.c | 3 +- Modules/_interpchannelsmodule.c | 3 +- Modules/_lsprof.c | 5 +- Modules/_threadmodule.c | 3 +- Modules/grpmodule.c | 3 +- Modules/posixmodule.c | 15 +-- Modules/pwdmodule.c | 3 +- Modules/resource.c | 4 +- Modules/signalmodule.c | 3 +- Objects/structseq.c | 60 +++++++++- Python/pylifecycle.c | 19 ++-- Python/sysmodule.c | 138 ++++++++++++----------- 23 files changed, 241 insertions(+), 129 deletions(-) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 0623adce693d46..5b8f65357bbc1c 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -823,6 +823,17 @@ typedef _Py_CODEUNIT *(*_PyJitEntryFuncPtr)(struct _PyExecutorObject *exec, _PyI #define _PyInterpreterGuard_GUARDS_NOT_ALLOWED UINTPTR_MAX +typedef struct { + PyTypeObject *async_gen_hooks_type; + PyTypeObject *flags_type; +#if defined(MS_WINDOWS) + PyTypeObject *windows_version_type; +#endif +#ifdef __EMSCRIPTEN__ + PyTypeObject *emscripten_info_type; +#endif +} _PySys_State; + /* PyInterpreterState holds the global state for one of the runtime's interpreters. Typically the initial (main) interpreter is the only one. @@ -899,6 +910,7 @@ struct _is { // Dictionary of the sys module PyObject *sysdict; + _PySys_State sys_state; // Dictionary of the builtins module PyObject *builtins; diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h index ab627c28c1fa5e..8c88806e0486bd 100644 --- a/Include/internal/pycore_pylifecycle.h +++ b/Include/internal/pycore_pylifecycle.h @@ -32,7 +32,7 @@ extern PyStatus _PySys_Create( extern PyStatus _PySys_ReadPreinitWarnOptions(PyWideStringList *options); extern PyStatus _PySys_ReadPreinitXOptions(PyConfig *config); extern int _PySys_UpdateConfig(PyThreadState *tstate); -extern void _PySys_FiniTypes(PyInterpreterState *interp); +extern void _PySys_Fini(PyInterpreterState *interp); extern int _PyBuiltins_AddExceptions(PyObject * bltinmod); extern PyStatus _Py_HashRandomization_Init(const PyConfig *); diff --git a/Include/internal/pycore_structseq.h b/Include/internal/pycore_structseq.h index 5cff165627502b..b9abae018e67c0 100644 --- a/Include/internal/pycore_structseq.h +++ b/Include/internal/pycore_structseq.h @@ -14,7 +14,8 @@ extern "C" { // Export for '_curses' shared extension PyAPI_FUNC(PyTypeObject*) _PyStructSequence_NewType( PyStructSequence_Desc *desc, - unsigned long tp_flags); + unsigned long tp_flags, + int deprecate_tuple_api); extern int _PyStructSequence_InitBuiltinWithFlags( PyInterpreterState *interp, diff --git a/Lib/http/server.py b/Lib/http/server.py index 095b5744bd12fc..6af70ed75c13e6 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_grp.py b/Lib/test/test_grp.py index f08b9328a9ff04..94ffa85fb40bd5 100644 --- a/Lib/test/test_grp.py +++ b/Lib/test/test_grp.py @@ -4,6 +4,7 @@ import string import sys import unittest +import warnings from test.support import import_helper @@ -14,16 +15,19 @@ class GroupDatabaseTestCase(unittest.TestCase): def check_value(self, value): # check that a grp tuple has the entries and # attributes promised by the docs - self.assertEqual(len(value), 4) - self.assertEqual(value[0], value.gr_name) self.assertIsInstance(value.gr_name, str) - self.assertEqual(value[1], value.gr_passwd) self.assertIsInstance(value.gr_passwd, str) - self.assertEqual(value[2], value.gr_gid) self.assertIsInstance(value.gr_gid, int) - self.assertEqual(value[3], value.gr_mem) self.assertIsInstance(value.gr_mem, list) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(len(value), 4) + self.assertEqual(value[0], value.gr_name) + self.assertEqual(value[1], value.gr_passwd) + self.assertEqual(value[2], value.gr_gid) + self.assertEqual(value[3], value.gr_mem) + def test_values(self): entries = grp.getgrall() diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 328a0dbeb99f8f..c7efa12668be00 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -665,7 +665,9 @@ def check_stat_attributes(self, fname): result = os.stat(fname) # Make sure direct access works - self.assertEqual(result[stat.ST_SIZE], 3) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(result[stat.ST_SIZE], 3) self.assertEqual(result.st_size, 3) # Make sure all the attributes are there @@ -677,8 +679,10 @@ def check_stat_attributes(self, fname): def trunc(x): return int(x) else: def trunc(x): return x - self.assertEqual(trunc(getattr(result, attr)), - result[getattr(stat, name)]) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(trunc(getattr(result, attr)), + result[getattr(stat, name)]) self.assertIn(attr, members) time_attributes = ['st_atime', 'st_mtime', 'st_ctime'] @@ -692,7 +696,9 @@ def trunc(x): return x self.check_timestamp_agreement(result, time_attributes) try: - result[200] + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + result[200] self.fail("No exception raised") except IndexError: pass @@ -1055,9 +1061,11 @@ def support_subsecond(self, filename): # Heuristic to check if the filesystem supports timestamp with # subsecond resolution: check if float and int timestamps are different st = os.stat(filename) - return ((st.st_atime != st[7]) - or (st.st_mtime != st[8]) - or (st.st_ctime != st[9])) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + return ((st.st_atime != st[7]) + or (st.st_mtime != st[8]) + or (st.st_ctime != st[9])) def support_atime(self, filename): # Heuristic to check if the filesystem stores the access time. diff --git a/Lib/test/test_os/test_posix.py b/Lib/test/test_os/test_posix.py index 8743b0bf0bc493..413c797d637e58 100644 --- a/Lib/test/test_os/test_posix.py +++ b/Lib/test/test_os/test_posix.py @@ -1816,8 +1816,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/Lib/test/test_pwd.py b/Lib/test/test_pwd.py index bdf57776c82be1..c2a69792384a52 100644 --- a/Lib/test/test_pwd.py +++ b/Lib/test/test_pwd.py @@ -2,6 +2,7 @@ import string import sys import unittest +import warnings from test.support import import_helper pwd = import_helper.import_module('pwd') @@ -13,22 +14,25 @@ def test_values(self): entries = pwd.getpwall() for e in entries: - self.assertEqual(len(e), 7) - self.assertEqual(e[0], e.pw_name) self.assertIsInstance(e.pw_name, str) - self.assertEqual(e[1], e.pw_passwd) self.assertIsInstance(e.pw_passwd, str) - self.assertEqual(e[2], e.pw_uid) self.assertIsInstance(e.pw_uid, int) - self.assertEqual(e[3], e.pw_gid) self.assertIsInstance(e.pw_gid, int) - self.assertEqual(e[4], e.pw_gecos) self.assertIn(type(e.pw_gecos), (str, type(None))) - self.assertEqual(e[5], e.pw_dir) self.assertIsInstance(e.pw_dir, str) - self.assertEqual(e[6], e.pw_shell) self.assertIsInstance(e.pw_shell, str) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(len(e), 7) + self.assertEqual(e[0], e.pw_name) + self.assertEqual(e[1], e.pw_passwd) + self.assertEqual(e[2], e.pw_uid) + self.assertEqual(e[3], e.pw_gid) + self.assertEqual(e[4], e.pw_gecos) + self.assertEqual(e[5], e.pw_dir) + self.assertEqual(e[6], e.pw_shell) + # The following won't work, because of duplicate entries # for one uid # self.assertEqual(pwd.getpwuid(e.pw_uid), e) @@ -50,7 +54,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_structseq.py b/Lib/test/test_structseq.py index 74506fc54de50e..ee90952745c81f 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -6,6 +6,7 @@ import textwrap import time import unittest +import warnings from test.support import script_helper @@ -228,7 +229,9 @@ def test_copying_with_unnamed_fields(self): self.assertEqual(r2.st_mode, r.st_mode) self.assertEqual(r2.st_atime, r.st_atime) self.assertEqual(r2.st_atime_ns, r.st_atime_ns) - self.assertIs(r2[0], r[0]) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertIs(r2[0], r[0]) self.assertIs(r2.st_mode, r.st_mode) self.assertIs(r2.st_atime, r.st_atime) self.assertIs(r2.st_atime_ns, r.st_atime_ns) @@ -239,7 +242,9 @@ def test_copying_with_unnamed_fields(self): self.assertEqual(r3.st_mode, r.st_mode) self.assertEqual(r3.st_atime, r.st_atime) self.assertEqual(r3.st_atime_ns, r.st_atime_ns) - self.assertIsNot(r3[0], r[0]) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertIsNot(r3[0], r[0]) self.assertIsNot(r3.st_mode, r.st_mode) self.assertIsNot(r3.st_atime, r.st_atime) self.assertIsNot(r3.st_atime_ns, r.st_atime_ns) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index dab03ef06a8b8e..c0321e998b5537 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -874,8 +874,10 @@ def test_sys_flags_indexable_attributes(self): attr_type = bool if attr in ("dev_mode", "safe_path") else int self.assertEqual(type(getattr(sys.flags, attr)), attr_type, attr) attr_value = getattr(sys.flags, attr) - self.assertEqual(sys.flags[attr_idx], attr_value, - msg=f"sys.flags .{attr} vs [{attr_idx}]") + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(sys.flags[attr_idx], attr_value, + msg=f"sys.flags .{attr} vs [{attr_idx}]") self.assertTrue(repr(sys.flags)) self.assertEqual(len(sys.flags), 18, msg="Do not increase, see GH-122575") @@ -1965,16 +1967,20 @@ def test_asyncgen_hooks(self): sys.set_asyncgen_hooks(firstiter=firstiter) hooks = sys.get_asyncgen_hooks() self.assertIs(hooks.firstiter, firstiter) - self.assertIs(hooks[0], firstiter) self.assertIs(hooks.finalizer, None) - self.assertIs(hooks[1], None) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertIs(hooks[0], firstiter) + self.assertIs(hooks[1], None) sys.set_asyncgen_hooks(finalizer=finalizer) hooks = sys.get_asyncgen_hooks() self.assertIs(hooks.firstiter, firstiter) - self.assertIs(hooks[0], firstiter) self.assertIs(hooks.finalizer, finalizer) - self.assertIs(hooks[1], finalizer) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertIs(hooks[0], firstiter) + self.assertIs(hooks[1], finalizer) sys.set_asyncgen_hooks(*old) cur = sys.get_asyncgen_hooks() diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 985984b55123ce..f30b377f6dbdaf 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -815,7 +815,8 @@ def test_localtime_timezone(self): # Try and make UNIX times from the localtime and a 9-tuple # created from the localtime. Test to see that the times are # the same. - t = time.mktime(lt); t9 = time.mktime(lt[:9]) + t = time.mktime(lt) + t9 = time.mktime(lt[:9]) self.assertEqual(t, t9) # Make localtimes from the UNIX times and compare them to diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 07e924b0fc564b..e2e776fa90ce32 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -9025,7 +9025,8 @@ cursesmodule_exec(PyObject *module) /* ncurses_version */ PyTypeObject *version_type; version_type = _PyStructSequence_NewType(&ncurses_version_desc, - Py_TPFLAGS_DISALLOW_INSTANTIATION); + Py_TPFLAGS_DISALLOW_INSTANTIATION, + 0); if (version_type == NULL) { return -1; } diff --git a/Modules/_interpchannelsmodule.c b/Modules/_interpchannelsmodule.c index 7b31b1f0c85d26..b86c1850169b63 100644 --- a/Modules/_interpchannelsmodule.c +++ b/Modules/_interpchannelsmodule.c @@ -9,6 +9,7 @@ #include "pycore_crossinterp.h" // _PyXIData_t #include "pycore_interp.h" // _PyInterpreterState_LookUpID() #include "pycore_pystate.h" // _PyInterpreterState_GetIDObject() +#include "pycore_structseq.h" // _PyStructSequence_NewType() #ifdef MS_WINDOWS #ifndef WIN32_LEAN_AND_MEAN @@ -3576,7 +3577,7 @@ module_exec(PyObject *mod) /* Add other types */ // ChannelInfo - state->ChannelInfoType = PyStructSequence_NewType(&channel_info_desc); + state->ChannelInfoType = _PyStructSequence_NewType(&channel_info_desc, 0, 1); if (state->ChannelInfoType == NULL) { goto error; } diff --git a/Modules/_lsprof.c b/Modules/_lsprof.c index 4e50ca64f59af2..7a9f14d3dcef62 100644 --- a/Modules/_lsprof.c +++ b/Modules/_lsprof.c @@ -6,6 +6,7 @@ #include "pycore_call.h" // _PyObject_CallNoArgs() #include "pycore_ceval.h" // _PyEval_SetProfile() #include "pycore_pystate.h" // _PyThreadState_GET() +#include "pycore_structseq.h" // _PyStructSequence_NewType() #include "pycore_time.h" // _PyTime_FromSecondsObject() #include "pycore_typeobject.h" // _PyType_GetModuleState() #include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString() @@ -1105,7 +1106,7 @@ _lsprof_exec(PyObject *module) return -1; } - state->stats_entry_type = PyStructSequence_NewType(&profiler_entry_desc); + state->stats_entry_type = _PyStructSequence_NewType(&profiler_entry_desc, 0, 1); if (state->stats_entry_type == NULL) { return -1; } @@ -1113,7 +1114,7 @@ _lsprof_exec(PyObject *module) return -1; } - state->stats_subentry_type = PyStructSequence_NewType(&profiler_subentry_desc); + state->stats_subentry_type = _PyStructSequence_NewType(&profiler_subentry_desc, 0, 1); if (state->stats_subentry_type == NULL) { return -1; } diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 199e4ac3db723b..32fbfdf41da1d3 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -10,6 +10,7 @@ #include "pycore_object_deferred.h" // _PyObject_SetDeferredRefcount() #include "pycore_pylifecycle.h" #include "pycore_pystate.h" // _PyThreadState_SetCurrent() +#include "pycore_structseq.h" // _PyStructSequence_NewType() #include "pycore_time.h" // _PyTime_FromSeconds() #include "pycore_tuple.h" // _PyTuple_FromPairSteal #include "pycore_weakref.h" // _PyWeakref_GET_REF() @@ -2755,7 +2756,7 @@ thread_module_exec(PyObject *module) } // _ExceptHookArgs type - state->excepthook_type = PyStructSequence_NewType(&ExceptHookArgs_desc); + state->excepthook_type = _PyStructSequence_NewType(&ExceptHookArgs_desc, 0, 1); if (state->excepthook_type == NULL) { return -1; } diff --git a/Modules/grpmodule.c b/Modules/grpmodule.c index 32ead259803614..2989efd3c1606a 100644 --- a/Modules/grpmodule.c +++ b/Modules/grpmodule.c @@ -6,6 +6,7 @@ #endif #include "Python.h" +#include "pycore_structseq.h" // _PyStructSequence_NewType() #include "posixmodule.h" #include // ERANGE @@ -359,7 +360,7 @@ grpmodule_exec(PyObject *module) { grpmodulestate *state = get_grp_state(module); - state->StructGrpType = PyStructSequence_NewType(&struct_group_type_desc); + state->StructGrpType = _PyStructSequence_NewType(&struct_group_type_desc, 0, 1); if (state->StructGrpType == NULL) { return -1; } diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index db65d586244065..2599aef0351f12 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -27,6 +27,7 @@ #include "pycore_pylifecycle.h" // _PyOS_URandom() #include "pycore_pystate.h" // _PyInterpreterState_GET() #include "pycore_signal.h" // Py_NSIG +#include "pycore_structseq.h" // _PyStructSequence_NewType() #include "pycore_time.h" // _PyLong_FromTime_t() #include "pycore_tuple.h" // _PyTuple_FromPairSteal #include "pycore_typeobject.h" // _PyType_AddMethod() @@ -18936,7 +18937,7 @@ posixmodule_exec(PyObject *m) } #if defined(HAVE_WAITID) - state->WaitidResultType = (PyObject *)PyStructSequence_NewType(&waitid_result_desc); + state->WaitidResultType = (PyObject *)_PyStructSequence_NewType(&waitid_result_desc, 0, 1); if (PyModule_AddObjectRef(m, "waitid_result", state->WaitidResultType) < 0) { return -1; } @@ -18945,20 +18946,20 @@ posixmodule_exec(PyObject *m) stat_result_desc.fields[7].name = PyStructSequence_UnnamedField; stat_result_desc.fields[8].name = PyStructSequence_UnnamedField; stat_result_desc.fields[9].name = PyStructSequence_UnnamedField; - state->StatResultType = (PyObject *)PyStructSequence_NewType(&stat_result_desc); + state->StatResultType = (PyObject *)_PyStructSequence_NewType(&stat_result_desc, 0, 1); if (PyModule_AddObjectRef(m, "stat_result", state->StatResultType) < 0) { return -1; } state->statresult_new_orig = ((PyTypeObject *)state->StatResultType)->tp_new; ((PyTypeObject *)state->StatResultType)->tp_new = statresult_new; - state->StatVFSResultType = (PyObject *)PyStructSequence_NewType(&statvfs_result_desc); + state->StatVFSResultType = (PyObject *)_PyStructSequence_NewType(&statvfs_result_desc, 0, 1); if (PyModule_AddObjectRef(m, "statvfs_result", state->StatVFSResultType) < 0) { return -1; } #if defined(HAVE_SCHED_SETPARAM) || defined(HAVE_SCHED_SETSCHEDULER) || defined(POSIX_SPAWN_SETSCHEDULER) || defined(POSIX_SPAWN_SETSCHEDPARAM) - state->SchedParamType = (PyObject *)PyStructSequence_NewType(&sched_param_desc); + state->SchedParamType = (PyObject *)_PyStructSequence_NewType(&sched_param_desc, 0, 1); if (PyModule_AddObjectRef(m, "sched_param", state->SchedParamType) < 0) { return -1; } @@ -18972,7 +18973,7 @@ posixmodule_exec(PyObject *m) #endif /* initialize TerminalSize_info */ - state->TerminalSizeType = (PyObject *)PyStructSequence_NewType(&TerminalSize_desc); + state->TerminalSizeType = (PyObject *)_PyStructSequence_NewType(&TerminalSize_desc, 0, 1); if (PyModule_AddObjectRef(m, "terminal_size", state->TerminalSizeType) < 0) { return -1; } @@ -18989,12 +18990,12 @@ posixmodule_exec(PyObject *m) return -1; } - state->TimesResultType = (PyObject *)PyStructSequence_NewType(×_result_desc); + state->TimesResultType = (PyObject *)_PyStructSequence_NewType(×_result_desc, 0, 1); if (PyModule_AddObjectRef(m, "times_result", state->TimesResultType) < 0) { return -1; } - state->UnameResultType = (PyObject *)PyStructSequence_NewType(&uname_result_desc); + state->UnameResultType = (PyObject *)_PyStructSequence_NewType(&uname_result_desc, 0, 1); if (PyModule_AddObjectRef(m, "uname_result", state->UnameResultType) < 0) { return -1; } diff --git a/Modules/pwdmodule.c b/Modules/pwdmodule.c index 4a2b33f8700d10..b73ee083032458 100644 --- a/Modules/pwdmodule.c +++ b/Modules/pwdmodule.c @@ -3,6 +3,7 @@ #include "Python.h" #include "posixmodule.h" +#include "pycore_structseq.h" // _PyStructSequence_NewType() #include // ERANGE #include // getpwuid() @@ -361,7 +362,7 @@ pwdmodule_exec(PyObject *module) { pwdmodulestate *state = get_pwd_state(module); - state->StructPwdType = PyStructSequence_NewType(&struct_pwd_type_desc); + state->StructPwdType = _PyStructSequence_NewType(&struct_pwd_type_desc, 0, 1); if (state->StructPwdType == NULL) { return -1; } diff --git a/Modules/resource.c b/Modules/resource.c index 9bf8d2782766cc..e34236b16a2da5 100644 --- a/Modules/resource.c +++ b/Modules/resource.c @@ -3,6 +3,8 @@ #endif #include "Python.h" +#include "pycore_structseq.h" // _PyStructSequence_NewType() + #include // errno #include #include // getrusage() @@ -405,7 +407,7 @@ resource_exec(PyObject *module) return -1; } - state->StructRUsageType = PyStructSequence_NewType(&struct_rusage_desc); + state->StructRUsageType = _PyStructSequence_NewType(&struct_rusage_desc, 0, 1); if (state->StructRUsageType == NULL) { return -1; } diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c index 8456239dee202d..f04cfca767a0c3 100644 --- a/Modules/signalmodule.c +++ b/Modules/signalmodule.c @@ -13,6 +13,7 @@ #include "pycore_pyerrors.h" // _PyErr_SetString() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_signal.h" // _Py_RestoreSignals() +#include "pycore_structseq.h" // _PyStructSequence_NewType() #include "pycore_time.h" // _PyTime_FromSecondsObject() #include "pycore_tuple.h" // _PyTuple_FromPairSteal @@ -1647,7 +1648,7 @@ signal_module_exec(PyObject *m) #endif #if defined(HAVE_SIGWAITINFO) || defined(HAVE_SIGTIMEDWAIT) - modstate->siginfo_type = PyStructSequence_NewType(&struct_siginfo_desc); + modstate->siginfo_type = _PyStructSequence_NewType(&struct_siginfo_desc, 0, 1); if (modstate->siginfo_type == NULL) { return -1; } diff --git a/Objects/structseq.c b/Objects/structseq.c index 9130fe6a133b1e..5090c87aac2a72 100644 --- a/Objects/structseq.c +++ b/Objects/structseq.c @@ -741,12 +741,54 @@ _PyStructSequence_FiniBuiltin(PyInterpreterState *interp, PyTypeObject *type) } +static int +structseq_deprecation(void) +{ + return PyErr_WarnEx(PyExc_DeprecationWarning, + "tuple API is deprecated, use named attributes", + 1); +} + + +static Py_ssize_t +structseq_length(PyObject *self) +{ + if (structseq_deprecation() < 0) { + return -1; + } + + return PyTuple_Type.tp_as_sequence->sq_length(self); +} + + +static PyObject * +structseq_item(PyObject *op, Py_ssize_t i) +{ + if (structseq_deprecation() < 0) { + return NULL; + } + + return PyTuple_Type.tp_as_sequence->sq_item(op, i); +} + + +static PyObject * +structseq_subscript(PyObject *op, PyObject *item) +{ + if (structseq_deprecation() < 0) { + return NULL; + } + + return PyTuple_Type.tp_as_mapping->mp_subscript(op, item); +} + + PyTypeObject * -_PyStructSequence_NewType(PyStructSequence_Desc *desc, unsigned long tp_flags) +_PyStructSequence_NewType(PyStructSequence_Desc *desc, unsigned long tp_flags, + int deprecate_tuple_api) { PyMemberDef *members; PyTypeObject *type; - PyType_Slot slots[8]; PyType_Spec spec; Py_ssize_t n_members, n_unnamed_members; @@ -758,6 +800,7 @@ _PyStructSequence_NewType(PyStructSequence_Desc *desc, unsigned long tp_flags) } /* Initialize Slots */ + PyType_Slot slots[11]; slots[0] = (PyType_Slot){Py_tp_dealloc, structseq_dealloc}; slots[1] = (PyType_Slot){Py_tp_repr, structseq_repr}; slots[2] = (PyType_Slot){Py_tp_doc, (void *)desc->doc}; @@ -765,7 +808,16 @@ _PyStructSequence_NewType(PyStructSequence_Desc *desc, unsigned long tp_flags) slots[4] = (PyType_Slot){Py_tp_new, structseq_new}; slots[5] = (PyType_Slot){Py_tp_members, members}; slots[6] = (PyType_Slot){Py_tp_traverse, structseq_traverse}; - slots[7] = (PyType_Slot){0, 0}; + if (deprecate_tuple_api) { + slots[7] = (PyType_Slot){Py_sq_item, structseq_item}; + slots[8] = (PyType_Slot){Py_mp_subscript, structseq_subscript}; + slots[9] = (PyType_Slot){Py_sq_length, structseq_length}; + slots[10] = (PyType_Slot){0, 0}; + } + else { + slots[7] = (PyType_Slot){0, 0}; + // following slots are ignored + } /* Initialize Spec */ /* The name in this PyType_Spec is statically allocated so it is */ @@ -796,5 +848,5 @@ _PyStructSequence_NewType(PyStructSequence_Desc *desc, unsigned long tp_flags) PyTypeObject * PyStructSequence_NewType(PyStructSequence_Desc *desc) { - return _PyStructSequence_NewType(desc, 0); + return _PyStructSequence_NewType(desc, 0, 0); } diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 500a1a1949a5a8..8283691b7e3d84 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2072,7 +2072,6 @@ finalize_interp_types(PyInterpreterState *interp) { _PyTypes_FiniExtTypes(interp); _PyUnicode_FiniTypes(interp); - _PySys_FiniTypes(interp); _PyXI_FiniTypes(interp); _PyExc_Fini(interp); _PyFloat_FiniType(interp); @@ -2112,12 +2111,13 @@ finalize_interp_types(PyInterpreterState *interp) static void finalize_interp_clear(PyThreadState *tstate) { - int is_main_interp = _Py_IsMainInterpreter(tstate->interp); + PyInterpreterState *interp = tstate->interp; + int is_main_interp = _Py_IsMainInterpreter(interp); - _PyXI_Fini(tstate->interp); - _PyExc_ClearExceptionGroupType(tstate->interp); - _Py_clear_generic_types(tstate->interp); - _PyTypes_FiniCachedDescriptors(tstate->interp); + _PyXI_Fini(interp); + _PyExc_ClearExceptionGroupType(interp); + _Py_clear_generic_types(interp); + _PyTypes_FiniCachedDescriptors(interp); /* Clear interpreter state and all thread states */ _PyInterpreterState_Clear(tstate); @@ -2136,13 +2136,14 @@ finalize_interp_clear(PyThreadState *tstate) _PyPerfTrampoline_Fini(); } - finalize_interp_types(tstate->interp); + finalize_interp_types(interp); + _PySys_Fini(interp); /* Finalize dtoa at last so that finalizers calling repr of float doesn't crash */ - _PyDtoa_Fini(tstate->interp); + _PyDtoa_Fini(interp); /* Free any delayed free requests immediately */ - _PyMem_FiniDelayed(tstate->interp); + _PyMem_FiniDelayed(interp); /* finalize_interp_types may allocate Python objects so we may need to abandon mimalloc segments again */ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 1e6e914b066bc5..8a82804ed4ba9e 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -35,7 +35,7 @@ Data members: #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_pystats.h" // _Py_PrintSpecializationStats() #include "pycore_runtime.h" // _PyRuntimeState_Get*() -#include "pycore_structseq.h" // _PyStructSequence_InitBuiltinWithFlags() +#include "pycore_structseq.h" // _PyStructSequence_NewType() #include "pycore_sysmodule.h" // export _PySys_GetSizeOf() #include "pycore_unicodeobject.h" // _PyUnicode_InternImmortal() @@ -76,6 +76,13 @@ module sys #include "clinic/sysmodule.c.h" +static _PySys_State* +sys_get_state(PyInterpreterState *interp) +{ + return &interp->sys_state; +} + + PyObject * PySys_GetAttr(PyObject *name) { @@ -1414,8 +1421,6 @@ sys_get_coroutine_origin_tracking_depth_impl(PyObject *module) return _PyEval_GetCoroutineOriginTrackingDepth(); } -static PyTypeObject AsyncGenHooksType; - PyDoc_STRVAR(asyncgen_hooks_doc, "asyncgen_hooks\n\ \n\ @@ -1429,7 +1434,7 @@ static PyStructSequence_Field asyncgen_hooks_fields[] = { }; static PyStructSequence_Desc asyncgen_hooks_desc = { - "asyncgen_hooks", /* name */ + "sys.asyncgen_hooks", /* name */ asyncgen_hooks_doc, /* doc */ asyncgen_hooks_fields , /* fields */ 2 @@ -1514,8 +1519,10 @@ sys_get_asyncgen_hooks_impl(PyObject *module) PyObject *res; PyObject *firstiter = _PyEval_GetAsyncGenFirstiter(); PyObject *finalizer = _PyEval_GetAsyncGenFinalizer(); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PySys_State *state = sys_get_state(interp); - res = PyStructSequence_New(&AsyncGenHooksType); + res = PyStructSequence_New(state->async_gen_hooks_type); if (res == NULL) { return NULL; } @@ -1535,8 +1542,6 @@ sys_get_asyncgen_hooks_impl(PyObject *module) } -static PyTypeObject Hash_InfoType; - PyDoc_STRVAR(hash_info_doc, "hash_info\n\ \n\ @@ -1566,12 +1571,12 @@ static PyStructSequence_Desc hash_info_desc = { }; static PyObject * -get_hash_info(PyThreadState *tstate) +get_hash_info(PyTypeObject *hash_info_type) { PyObject *hash_info; int field = 0; PyHash_FuncDef *hashfunc; - hash_info = PyStructSequence_New(&Hash_InfoType); + hash_info = PyStructSequence_New(hash_info_type); if (hash_info == NULL) { return NULL; } @@ -1620,8 +1625,6 @@ sys_getrecursionlimit_impl(PyObject *module) #ifdef MS_WINDOWS -static PyTypeObject WindowsVersionType = { 0 }; - static PyStructSequence_Field windows_version_fields[] = { {"major", "Major version number"}, {"minor", "Minor version number"}, @@ -1721,10 +1724,14 @@ sys_getwindowsversion_impl(PyObject *module) int pos = 0; OSVERSIONINFOEXW ver; + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PySys_State *state = sys_get_state(interp); + PyTypeObject *windows_version_type = state->windows_version_type; + if (PyObject_GetOptionalAttrString(module, "_cached_windows_version", &version) < 0) { return NULL; - }; - if (version && PyObject_TypeCheck(version, &WindowsVersionType)) { + } + if (version && PyObject_TypeCheck(version, windows_version_type)) { return version; } Py_XDECREF(version); @@ -1733,7 +1740,7 @@ sys_getwindowsversion_impl(PyObject *module) if (!GetVersionExW((OSVERSIONINFOW*) &ver)) return PyErr_SetFromWindowsErr(0); - version = PyStructSequence_New(&WindowsVersionType); + version = PyStructSequence_New(windows_version_type); if (version == NULL) return NULL; @@ -3437,8 +3444,6 @@ PyDoc_STRVAR(flags__doc__, \n\ Flags provided through command line arguments or environment vars."); -static PyTypeObject FlagsType; - static PyStructSequence_Field flags_fields[] = { {"debug", "-d"}, {"inspect", "-i"}, @@ -3499,7 +3504,9 @@ _PySys_SetFlagObj(Py_ssize_t pos, PyObject *value) goto error; } - new_flags = PyStructSequence_New(&FlagsType); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PySys_State *state = sys_get_state(interp); + new_flags = PyStructSequence_New(state->flags_type); if (new_flags == NULL) { goto error; } @@ -3598,9 +3605,9 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags) static PyObject* -make_flags(PyInterpreterState *interp) +make_flags(PyInterpreterState *interp, PyTypeObject *flags_type) { - PyObject *flags = PyStructSequence_New(&FlagsType); + PyObject *flags = PyStructSequence_New(flags_type); if (flags == NULL) { return NULL; } @@ -3618,8 +3625,6 @@ PyDoc_STRVAR(version_info__doc__, \n\ Version information as a named tuple."); -static PyTypeObject VersionInfoType; - static PyStructSequence_Field version_info_fields[] = { {"major", "Major release number"}, {"minor", "Minor release number"}, @@ -3637,13 +3642,13 @@ static PyStructSequence_Desc version_info_desc = { }; static PyObject * -make_version_info(PyThreadState *tstate) +make_version_info(PyThreadState *tstate, PyTypeObject *version_info_type) { PyObject *version_info; char *s; int pos = 0; - version_info = PyStructSequence_New(&VersionInfoType); + version_info = PyStructSequence_New(version_info_type); if (version_info == NULL) { return NULL; } @@ -3843,8 +3848,6 @@ PyDoc_STRVAR(emscripten_info__doc__, \n\ WebAssembly Emscripten platform information."); -static PyTypeObject *EmscriptenInfoType; - static PyStructSequence_Field emscripten_info_fields[] = { {"emscripten_version", "Emscripten version (major, minor, micro)"}, {"runtime", "Runtime (Node.JS version, browser user agent)"}, @@ -3898,14 +3901,14 @@ EM_JS(char *, _Py_emscripten_runtime, (void), { }); static PyObject * -make_emscripten_info(void) +make_emscripten_info(PyTypeObject *emscripten_info_type) { PyObject *emscripten_info = NULL; PyObject *version = NULL; char *ua; int pos = 0; - emscripten_info = PyStructSequence_New(EmscriptenInfoType); + emscripten_info = PyStructSequence_New(emscripten_info_type); if (emscripten_info == NULL) { return NULL; } @@ -3993,6 +3996,7 @@ _PySys_InitCore(PyThreadState *tstate, PyObject *sysdict) PyObject *version_info; int res; PyInterpreterState *interp = tstate->interp; + _PySys_State *state = sys_get_state(interp); /* stdin/stdout/stderr are set in pylifecycle.c */ @@ -4017,13 +4021,17 @@ _PySys_InitCore(PyThreadState *tstate, PyObject *sysdict) SET_SYS("maxsize", PyLong_FromSsize_t(PY_SSIZE_T_MAX)); SET_SYS("float_info", PyFloat_GetInfo()); SET_SYS("int_info", PyLong_GetInfo()); + /* initialize hash_info */ - if (_PyStructSequence_InitBuiltin(interp, &Hash_InfoType, - &hash_info_desc) < 0) - { + PyTypeObject *hash_info_type = _PyStructSequence_NewType( + &hash_info_desc, 0, 1); + if (hash_info_type == NULL) { goto type_init_failed; } - SET_SYS("hash_info", get_hash_info(tstate)); + PyObject *hash_info = get_hash_info(hash_info_type); + Py_DECREF(hash_info_type); + SET_SYS("hash_info", hash_info); + SET_SYS("maxunicode", PyLong_FromLong(0x10FFFF)); SET_SYS("builtin_module_names", list_builtin_module_names()); SET_SYS("stdlib_module_names", list_stdlib_module_names()); @@ -4041,35 +4049,38 @@ _PySys_InitCore(PyThreadState *tstate, PyObject *sysdict) SET_SYS_FROM_STRING("abiflags", ABIFLAGS); #endif -#define ENSURE_INFO_TYPE(TYPE, DESC) \ - do { \ - if (_PyStructSequence_InitBuiltinWithFlags( \ - interp, &TYPE, &DESC, Py_TPFLAGS_DISALLOW_INSTANTIATION) < 0) { \ - goto type_init_failed; \ - } \ - } while (0) - /* version_info */ - ENSURE_INFO_TYPE(VersionInfoType, version_info_desc); - version_info = make_version_info(tstate); + PyTypeObject *version_info_type = _PyStructSequence_NewType( + &version_info_desc, Py_TPFLAGS_DISALLOW_INSTANTIATION, 0); + if (version_info_type == NULL) { + goto type_init_failed; + } + version_info = make_version_info(tstate, version_info_type); + Py_DECREF(version_info_type); SET_SYS("version_info", version_info); /* implementation */ SET_SYS("implementation", make_impl_info(version_info)); // sys.flags: updated later by _PySys_UpdateConfig() - ENSURE_INFO_TYPE(FlagsType, flags_desc); - SET_SYS("flags", make_flags(tstate->interp)); + state->flags_type = _PyStructSequence_NewType( + &flags_desc, Py_TPFLAGS_DISALLOW_INSTANTIATION, 1); + if (state->flags_type == NULL) { + goto type_init_failed; + } + SET_SYS("flags", make_flags(tstate->interp, state->flags_type)); #if defined(MS_WINDOWS) /* getwindowsversion */ - ENSURE_INFO_TYPE(WindowsVersionType, windows_version_desc); + state->windows_version_type = _PyStructSequence_NewType( + &windows_version_desc, Py_TPFLAGS_DISALLOW_INSTANTIATION, 1); + if (state->windows_version_type == NULL) { + goto type_init_failed; + } SET_SYS_FROM_STRING("_vpath", VPATH); #endif -#undef ENSURE_INFO_TYPE - /* float repr style: 0.03 (short) vs 0.029999999999999999 (legacy) */ #if _PY_SHORT_FLOAT_REPR == 1 SET_SYS("float_repr_style", &_Py_ID(short)); @@ -4082,20 +4093,19 @@ _PySys_InitCore(PyThreadState *tstate, PyObject *sysdict) SET_SYS("abi_info", make_abi_info()); /* initialize asyncgen_hooks */ - if (_PyStructSequence_InitBuiltin(interp, &AsyncGenHooksType, - &asyncgen_hooks_desc) < 0) - { + state->async_gen_hooks_type = _PyStructSequence_NewType( + &asyncgen_hooks_desc, 0, 1); + if (state->async_gen_hooks_type == NULL) { goto type_init_failed; } #ifdef __EMSCRIPTEN__ - if (EmscriptenInfoType == NULL) { - EmscriptenInfoType = PyStructSequence_NewType(&emscripten_info_desc); - if (EmscriptenInfoType == NULL) { - goto type_init_failed; - } + state->emscripten_info_type = _PyStructSequence_NewType( + &emscripten_info_desc, 0, 1); + if (state->emscripten_info_type == NULL) { + goto type_init_failed; } - SET_SYS("_emscripten_info", make_emscripten_info()); + SET_SYS("_emscripten_info", make_emscripten_info(state->emscripten_info_type)); #endif /* adding sys.path_hooks and sys.path_importer_cache */ @@ -4178,7 +4188,8 @@ _PySys_UpdateConfig(PyThreadState *tstate) #undef COPY_WSTR // replace sys.flags - PyObject *new_flags = PyStructSequence_New(&FlagsType); + _PySys_State *state = sys_get_state(interp); + PyObject *new_flags = PyStructSequence_New(state->flags_type); if (new_flags == NULL) { return -1; } @@ -4395,19 +4406,16 @@ _PySys_Create(PyThreadState *tstate, PyObject **sysmod_p) void -_PySys_FiniTypes(PyInterpreterState *interp) +_PySys_Fini(PyInterpreterState *interp) { - _PyStructSequence_FiniBuiltin(interp, &VersionInfoType); - _PyStructSequence_FiniBuiltin(interp, &FlagsType); + _PySys_State *state = sys_get_state(interp); + Py_CLEAR(state->async_gen_hooks_type); + Py_CLEAR(state->flags_type); #if defined(MS_WINDOWS) - _PyStructSequence_FiniBuiltin(interp, &WindowsVersionType); + Py_CLEAR(state->windows_version_type); #endif - _PyStructSequence_FiniBuiltin(interp, &Hash_InfoType); - _PyStructSequence_FiniBuiltin(interp, &AsyncGenHooksType); #ifdef __EMSCRIPTEN__ - if (_Py_IsMainInterpreter(interp)) { - Py_CLEAR(EmscriptenInfoType); - } + Py_CLEAR(state->emscripten_info_type); #endif } From 98624e4298dc252d4e7adae37d1a84a7d88a310d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 7 Aug 2026 21:01:02 +0200 Subject: [PATCH 2/8] WIP: Deprecate tuple API in namedtuple --- Lib/collections/__init__.py | 11 ++++++++++- Lib/difflib.py | 2 +- Lib/getpass.py | 2 +- Lib/http/cookiejar.py | 2 +- Lib/http/server.py | 4 ++-- Lib/shutil.py | 4 ++-- Lib/tarfile.py | 4 ++-- Lib/test/ssl_servers.py | 2 +- Lib/test/support/__init__.py | 2 +- Lib/test/test_dataclasses/__init__.py | 5 ++++- Lib/test/test_getpass.py | 5 ++++- Lib/test/test_hash.py | 4 ++-- Lib/test/test_os/test_os.py | 24 +++++++++++++----------- Lib/test/test_pkgutil.py | 4 ++-- Lib/test/test_shutil.py | 12 ++++++------ Lib/test/test_statistics.py | 4 ++-- Lib/test/test_structseq.py | 8 +++++--- Lib/test/test_tokenize.py | 6 +++--- Lib/test/test_utf8_mode.py | 8 ++++---- Lib/urllib/request.py | 16 +++++++++------- Lib/urllib/robotparser.py | 4 +++- 21 files changed, 78 insertions(+), 55 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 5dbcac19e7a927..e1f08080e5feb2 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -358,7 +358,7 @@ def __ror__(self, other): except ImportError: _tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc) -def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None): +def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None, deprecate_tuple_api=True): """Returns a new subclass of tuple with named fields. >>> Point = namedtuple('Point', ['x', 'y']) @@ -512,6 +512,15 @@ def __getnewargs__(self): doc = _sys.intern(f'Alias for field number {index}') class_namespace[name] = _tuplegetter(index, doc) + if deprecate_tuple_api: + def __getitem__(self, key): + import warnings + warnings.warn('tuple API is deprecated, use named attributes', + DeprecationWarning, stacklevel=2) + return tuple.__getitem__(self, key) + + class_namespace['__getitem__'] = __getitem__ + result = type(typename, (tuple,), class_namespace) # For pickling to work, the __module__ variable needs to be set to the frame diff --git a/Lib/difflib.py b/Lib/difflib.py index 95ba8fd782c6c3..e5a19d815d5599 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -617,7 +617,7 @@ def ratio(self): 1.0 """ - matches = sum(triple[-1] for triple in self.get_matching_blocks()) + matches = sum(triple.size for triple in self.get_matching_blocks()) return _calculate_ratio(matches, len(self.a) + len(self.b)) def quick_ratio(self): diff --git a/Lib/getpass.py b/Lib/getpass.py index cfbd63dded6cc1..b9eec4c57abc97 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/cookiejar.py b/Lib/http/cookiejar.py index 13e5b104a81ea2..eea60c4285d362 100644 --- a/Lib/http/cookiejar.py +++ b/Lib/http/cookiejar.py @@ -626,7 +626,7 @@ def request_host(request): """ url = request.get_full_url() - host = urllib.parse.urlparse(url)[1] + host = urllib.parse.urlparse(url).netloc if host == "": host = request.get_header("Host", "") diff --git a/Lib/http/server.py b/Lib/http/server.py index 6af70ed75c13e6..d804e3210c8d59 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -796,8 +796,8 @@ def send_head(self): if not parts.path.endswith(('/', '%2f', '%2F')): # redirect browser - doing basically what apache does self.send_response(HTTPStatus.MOVED_PERMANENTLY) - new_parts = (parts[0], parts[1], parts[2] + '/', - parts[3], parts[4]) + new_parts = (parts.scheme, parts.netloc, parts.path + '/', + parts.query, parts.fragment) new_url = urllib.parse.urlunsplit(new_parts) self.send_header("Location", new_url) self.send_header("Content-Length", "0") diff --git a/Lib/shutil.py b/Lib/shutil.py index 94617ec296f508..ce6969d6a4bf5a 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 d12bd15aa2d231..a1352dcd9bcdd3 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] diff --git a/Lib/test/ssl_servers.py b/Lib/test/ssl_servers.py index 15b071e04dda1f..e3416a822f6525 100644 --- a/Lib/test/ssl_servers.py +++ b/Lib/test/ssl_servers.py @@ -61,7 +61,7 @@ def translate_path(self, path): """ # abandon query parameters - path = urllib.parse.urlparse(path)[2] + path = urllib.parse.urlparse(path).path path = os.path.normpath(urllib.parse.unquote(path)) words = path.split('/') words = filter(None, words) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 74d3794289bf69..a0b57779afbea7 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -868,7 +868,7 @@ def open_urlresource(url, *args, **kw): check = kw.pop('check', None) - filename = urllib.parse.urlparse(url)[2].split('/')[-1] # '/': it's URL! + filename = urllib.parse.urlparse(url).path.split('/')[-1] # '/': it's URL! fn = os.path.join(TEST_DATA_DIR, filename) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index a89999bb97938c..2fa2c1e7ee62f4 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -16,6 +16,7 @@ import sys import textwrap import unittest +import warnings from unittest.mock import Mock from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol, DefaultDict from typing import get_type_hints @@ -1779,7 +1780,9 @@ class C: # Make sure that the returned dicts are actually OrderedDicts. self.assertIs(type(d), OrderedDict) - self.assertIs(type(d['y'][1]), OrderedDict) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertIs(type(d['y'][1]), OrderedDict) def test_helper_asdict_namedtuple_key(self): # Ensure that a field that contains a dict which has a diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 272414a6204856..23f8a328506c6e 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_hash.py b/Lib/test/test_hash.py index cf9db66a29ae11..63b745f7a9f52f 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 c7efa12668be00..d6e500b3779938 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -915,18 +915,20 @@ def test_statvfs_attributes(self): result = os.statvfs(self.fname) # Make sure direct access works - self.assertEqual(result.f_bfree, result[3]) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(result.f_bfree, result[3]) - # Make sure all the attributes are there. - members = ('bsize', 'frsize', 'blocks', 'bfree', 'bavail', 'files', - 'ffree', 'favail', 'flag', 'namemax') - for value, member in enumerate(members): - self.assertEqual(getattr(result, 'f_' + member), result[value]) + # Make sure all the attributes are there. + members = ('bsize', 'frsize', 'blocks', 'bfree', 'bavail', 'files', + 'ffree', 'favail', 'flag', 'namemax') + for value, member in enumerate(members): + self.assertEqual(getattr(result, 'f_' + member), result[value]) - self.assertTrue(isinstance(result.f_fsid, int)) + # Test that the size of the tuple doesn't change + self.assertEqual(len(result), 10) - # Test that the size of the tuple doesn't change - self.assertEqual(len(result), 10) + self.assertTrue(isinstance(result.f_fsid, int)) # Make sure that assignment really fails try: @@ -2467,8 +2469,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_pkgutil.py b/Lib/test/test_pkgutil.py index 4623b7eb4434b0..eccdb9b53000a6 100644 --- a/Lib/test/test_pkgutil.py +++ b/Lib/test/test_pkgutil.py @@ -180,7 +180,7 @@ def test_walkpackages_filesys(self): 'test_walkpackages_filesys.sub', 'test_walkpackages_filesys.sub.mod', ] - actual= [e[1] for e in pkgutil.walk_packages([self.dirname])] + actual= [e.name for e in pkgutil.walk_packages([self.dirname])] self.assertEqual(actual, expected) for pkg in expected: @@ -214,7 +214,7 @@ def test_walkpackages_zipfile(self): 'test_walkpackages_zipfile.sub', 'test_walkpackages_zipfile.sub.mod', ] - actual= [e[1] for e in pkgutil.walk_packages([zip_file])] + actual= [e.name for e in pkgutil.walk_packages([zip_file])] self.assertEqual(actual, expected) del sys.path[0] diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index ed5d15ecc7ddad..d6b3b6a642bee1 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_statistics.py b/Lib/test/test_statistics.py index 700c5ac304f717..0ef7616e2e9e9a 100644 --- a/Lib/test/test_statistics.py +++ b/Lib/test/test_statistics.py @@ -59,8 +59,8 @@ def _nan_equal(a, b): return False if isinstance(a, float): return math.isnan(a) and math.isnan(b) - aexp = a.as_tuple()[2] - bexp = b.as_tuple()[2] + aexp = a.as_tuple().exponent + bexp = b.as_tuple().exponent return (aexp == bexp) and (aexp in ('n', 'N')) # Both NAN or both sNAN. diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py index ee90952745c81f..6c95e91b570e04 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -281,9 +281,11 @@ def test_copy_replace_all_fields_visible(self): # visible fields self.assertEqual(copy.replace(t), t) self.assertIsInstance(copy.replace(t), os.times_result) - self.assertEqual(copy.replace(t, user=1.5), (1.5, *t[1:])) - self.assertEqual(copy.replace(t, system=2.5), (t[0], 2.5, *t[2:])) - self.assertEqual(copy.replace(t, user=1.5, system=2.5), (1.5, 2.5, *t[2:])) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(copy.replace(t, user=1.5), (1.5, *t[1:])) + self.assertEqual(copy.replace(t, system=2.5), (t[0], 2.5, *t[2:])) + self.assertEqual(copy.replace(t, user=1.5, system=2.5), (1.5, 2.5, *t[2:])) # unknown fields with self.assertRaisesRegex(TypeError, 'unexpected field name'): diff --git a/Lib/test/test_tokenize.py b/Lib/test/test_tokenize.py index e2db09d61f409b..bf528ffe5976b5 100644 --- a/Lib/test/test_tokenize.py +++ b/Lib/test/test_tokenize.py @@ -2015,16 +2015,16 @@ def check_roundtrip(self, f): code = f.read() readline = iter(code.splitlines(keepends=True)).__next__ tokens5 = list(tokenize.tokenize(readline)) - tokens2 = [tok[:2] for tok in tokens5] + tokens2 = [(tok.type, tok.string) for tok in tokens5] # Reproduce tokens2 from pairs bytes_from2 = tokenize.untokenize(tokens2) readline2 = iter(bytes_from2.splitlines(keepends=True)).__next__ - tokens2_from2 = [tok[:2] for tok in tokenize.tokenize(readline2)] + tokens2_from2 = [(tok.type, tok.string) for tok in tokenize.tokenize(readline2)] self.assertEqual(tokens2_from2, tokens2) # Reproduce tokens2 from 5-tuples bytes_from5 = tokenize.untokenize(tokens5) readline5 = iter(bytes_from5.splitlines(keepends=True)).__next__ - tokens2_from5 = [tok[:2] for tok in tokenize.tokenize(readline5)] + tokens2_from5 = [(tok.type, tok.string) for tok in tokenize.tokenize(readline5)] self.assertEqual(tokens2_from5, tokens2) if not contains_ambiguous_backslash(code): diff --git a/Lib/test/test_utf8_mode.py b/Lib/test/test_utf8_mode.py index b8e49440c9f7da..6cd156b7e9293a 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') diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 660301fef61258..a58923c3087517 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -274,7 +274,7 @@ def request_host(request): """ url = request.full_url - host = urlparse(url)[1] + host = urlparse(url).netloc if host == "": host = request.get_header("Host", "") @@ -832,11 +832,11 @@ def reduce_uri(self, uri, default_port=True): """Accept authority or URI and extract only the authority and path.""" # note HTTP URLs do not have a userinfo component parts = urlsplit(uri) - if parts[1]: + if parts.netloc: # URI - scheme = parts[0] - authority = parts[1] - path = parts[2] or '/' + scheme = parts.scheme + authority = parts.netloc + path = parts.path or '/' else: # host or host:port scheme = None @@ -1209,7 +1209,7 @@ class HTTPDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler): handler_order = 490 # before Basic auth def http_error_401(self, req, fp, code, msg, headers): - host = urlparse(req.full_url)[1] + host = urlparse(req.full_url).netloc retry = self.http_error_auth_reqed('www-authenticate', host, req, headers) self.reset_retry_count() @@ -1668,7 +1668,9 @@ def url2pathname(url, *, require_scheme=False, resolve_host=False): """ if not require_scheme: url = 'file:' + url - scheme, authority, url = urlsplit(url)[:3] # Discard query and fragment. + parts = urlsplit(url) + # Discard query and fragment. + scheme, authority, url = parts.scheme, parts.netloc, parts.path if scheme != 'file': raise URLError("URL is missing a 'file:' scheme") if os.name == 'nt': diff --git a/Lib/urllib/robotparser.py b/Lib/urllib/robotparser.py index 8d0311d96f5e0b..985333c7100438 100644 --- a/Lib/urllib/robotparser.py +++ b/Lib/urllib/robotparser.py @@ -62,7 +62,9 @@ def set_url(self, url): if isinstance(url, urllib.request.Request): url = url.full_url - self.host, self.path = urllib.parse.urlsplit(url)[1:3] + parts = urllib.parse.urlsplit(url) + self.host = parts.netloc + self.path = parts.path def read(self): """Reads the robots.txt URL and feeds it to the parser.""" From 2bccd2c981fc589fe6938ee2e35bdb4a6b8eb774 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 7 Aug 2026 21:37:27 +0200 Subject: [PATCH 3/8] Keep tuple API for os.get_terminal_size() pip is not ready for this change: $ PYTHONWARNINGS=error env/bin/python -Werror -m pip install setuptools -v Traceback (most recent call last): File "/home/vstinner/python/main/Lib/runpy.py", line 201, in _run_module_as_main return _run_code(code, main_globals, None, "__main__", mod_spec) File "/home/vstinner/python/main/Lib/runpy.py", line 87, in _run_code exec(code, run_globals) ~~~~^^^^^^^^^^^^^^^^^^^ File "/home/vstinner/python/main/env/lib/python3.16t/site-packages/pip/__main__.py", line 24, in sys.exit(_main()) ~~~~~^^ File "/home/vstinner/python/main/env/lib/python3.16t/site-packages/pip/_internal/cli/main.py", line 70, in main cmd_name, cmd_args = parse_command(args) ~~~~~~~~~~~~~^^^^^^ File "/home/vstinner/python/main/env/lib/python3.16t/site-packages/pip/_internal/cli/main_parser.py", line 71, in parse_command parser = create_main_parser() File "/home/vstinner/python/main/env/lib/python3.16t/site-packages/pip/_internal/cli/main_parser.py", line 26, in create_main_parser formatter=UpdatingDefaultsHelpFormatter(), ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^ File "/home/vstinner/python/main/env/lib/python3.16t/site-packages/pip/_internal/cli/parser.py", line 45, in __init__ kwargs["width"] = shutil.get_terminal_size()[0] - 2 ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^ DeprecationWarning: tuple API is deprecated, use named attributes --- Modules/posixmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 2599aef0351f12..572effd14d3160 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -18973,7 +18973,7 @@ posixmodule_exec(PyObject *m) #endif /* initialize TerminalSize_info */ - state->TerminalSizeType = (PyObject *)_PyStructSequence_NewType(&TerminalSize_desc, 0, 1); + state->TerminalSizeType = (PyObject *)PyStructSequence_NewType(&TerminalSize_desc); if (PyModule_AddObjectRef(m, "terminal_size", state->TerminalSizeType) < 0) { return -1; } From 96cdc6c4c780e7a1bd98502d392f49950ebde7f2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 7 Aug 2026 21:49:28 +0200 Subject: [PATCH 4/8] WIP: use named attributes inspect: keep tuple API for 3 namedtuple --- Lib/collections/__init__.py | 2 +- Lib/inspect.py | 4 ++-- Lib/pydoc.py | 2 +- Lib/tarfile.py | 4 ++-- Lib/test/test_calendar.py | 3 ++- Lib/test/test_collections.py | 6 +++--- .../test_profiling/test_sampling_profiler/mocks.py | 4 +++- .../test_sampling_profiler/test_dump.py | 3 +++ Lib/test/test_script_helper.py | 2 +- Lib/test/test_sys.py | 12 +++++++++--- Lib/test/test_tarfile.py | 4 ++-- Lib/test/test_typing.py | 4 +++- Lib/test/test_yield_from.py | 2 +- Lib/xmlrpc/client.py | 2 +- 14 files changed, 34 insertions(+), 20 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index e1f08080e5feb2..ecea4c8935bbf8 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -361,7 +361,7 @@ def __ror__(self, other): def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None, deprecate_tuple_api=True): """Returns a new subclass of tuple with named fields. - >>> Point = namedtuple('Point', ['x', 'y']) + >>> Point = namedtuple('Point', ['x', 'y'], deprecate_tuple_api=False) >>> Point.__doc__ # docstring for the new class 'Point(x, y)' >>> p = Point(11, y=22) # instantiate with positional args or keywords diff --git a/Lib/inspect.py b/Lib/inspect.py index 2a14e43b66f2fa..1bc77bbe9ed0a8 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -540,7 +540,7 @@ def getmembers_static(object, predicate=None): """ return _getmembers(object, predicate, getattr_static) -Attribute = namedtuple('Attribute', 'name kind defining_class object') +Attribute = namedtuple('Attribute', 'name kind defining_class object', deprecate_tuple_api=False) def classify_class_attrs(cls): """Return list of attribute-descriptor tuples. @@ -1643,7 +1643,7 @@ def getlineno(frame): """Get the line number from a frame object, allowing for optimization.""" return frame.f_lineno -_FrameInfo = namedtuple('_FrameInfo', ('frame',) + Traceback._fields) +_FrameInfo = namedtuple('_FrameInfo', ('frame',) + Traceback._fields, deprecate_tuple_api=False) class FrameInfo(_FrameInfo): def __new__(cls, frame, filename, lineno, function, code_context, index, *, positions=None): instance = super().__new__(cls, frame, filename, lineno, function, code_context, index) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 72974af26bee64..1eb712aa483dc1 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -2022,7 +2022,7 @@ def output(self): return self._output or sys.stdout def __repr__(self): - if inspect.stack()[1][3] == '?': + if inspect.stack()[1].function == '?': self() return '' return '<%s.%s instance>' % (self.__class__.__module__, diff --git a/Lib/tarfile.py b/Lib/tarfile.py index a1352dcd9bcdd3..dc5c3a59744cbc 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -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/test_calendar.py b/Lib/test/test_calendar.py index 8646cfcad58cea..15cce2b30da576 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_collections.py b/Lib/test/test_collections.py index b1b2dd2ca5ca0d..a5f0a7424539cb 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -319,7 +319,7 @@ def __ror__(self, other): class TestNamedTuple(unittest.TestCase): def test_factory(self): - Point = namedtuple('Point', 'x y') + Point = namedtuple('Point', 'x y', deprecate_tuple_api=False) self.assertEqual(Point.__name__, 'Point') self.assertEqual(Point.__slots__, ()) self.assertEqual(Point.__module__, __name__) @@ -398,7 +398,7 @@ def test_defaults(self): self.assertEqual(Point(), (10, 20)) def test_readonly(self): - Point = namedtuple('Point', 'x y') + Point = namedtuple('Point', 'x y', deprecate_tuple_api=False) p = Point(11, 22) with self.assertRaises(AttributeError): p.x = 33 @@ -504,7 +504,7 @@ def test_instance(self): self.assertEqual(repr(p), 'Point(x=11, y=22)') def test_tupleness(self): - Point = namedtuple('Point', 'x y') + Point = namedtuple('Point', 'x y', deprecate_tuple_api=False) p = Point(11, 22) self.assertIsInstance(p, tuple) diff --git a/Lib/test/test_profiling/test_sampling_profiler/mocks.py b/Lib/test/test_profiling/test_sampling_profiler/mocks.py index 6ac2d08e898d81..f24e8865cdd5e1 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/mocks.py +++ b/Lib/test/test_profiling/test_sampling_profiler/mocks.py @@ -3,7 +3,9 @@ from collections import namedtuple # Matches the C structseq LocationInfo from _remote_debugging -LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset']) +LocationInfo = namedtuple('LocationInfo', + ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'], + deprecate_tuple_api=False) class MockFrameInfo: diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_dump.py b/Lib/test/test_profiling/test_sampling_profiler/test_dump.py index 7a0f8df38a8533..81a9d28dbf2b11 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_dump.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_dump.py @@ -37,14 +37,17 @@ StructseqInterpreterInfo = namedtuple( "StructseqInterpreterInfo", ["interpreter_id", "threads"], + deprecate_tuple_api=False, ) StructseqThreadInfo = namedtuple( "StructseqThreadInfo", ["thread_id", "status", "frame_info"], + deprecate_tuple_api=False, ) StructseqFrameInfo = namedtuple( "StructseqFrameInfo", ["filename", "location", "funcname", "opcode"], + deprecate_tuple_api=False, ) diff --git a/Lib/test/test_script_helper.py b/Lib/test/test_script_helper.py index eeea6c4842b488..e65b3efdcd0a70 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_sys.py b/Lib/test/test_sys.py index c0321e998b5537..fce1b833f70427 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -657,7 +657,9 @@ def test_attributes(self): self.assertIsInstance(sys.int_info.str_digits_check_threshold, int) self.assertIsInstance(sys.hexversion, int) - self.assertEqual(len(sys.hash_info), 9) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(len(sys.hash_info), 9) self.assertLess(sys.hash_info.modulus, 2**sys.hash_info.width) # sys.hash_info.modulus should be a prime; we do a quick # probable primality test (doesn't exclude the possibility of @@ -879,7 +881,9 @@ def test_sys_flags_indexable_attributes(self): self.assertEqual(sys.flags[attr_idx], attr_value, msg=f"sys.flags .{attr} vs [{attr_idx}]") self.assertTrue(repr(sys.flags)) - self.assertEqual(len(sys.flags), 18, msg="Do not increase, see GH-122575") + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(len(sys.flags), 18, msg="Do not increase, see GH-122575") self.assertIn(sys.flags.utf8_mode, {0, 1, 2}) @@ -1941,7 +1945,9 @@ def test_pythontypes(self): # per GH-122575 would be nice... # Q: What is the actual point of this sys.flags C size derived from PyStructSequence_Field array assertion? non_sequence_fields = 4 - check(sys.flags, vsize('') + self.P + self.P * (non_sequence_fields + len(sys.flags))) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + check(sys.flags, vsize('') + self.P + self.P * (non_sequence_fields + len(sys.flags))) def test_asyncgen_hooks(self): old = sys.get_asyncgen_hooks() diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index c86bcb79eb85d8..5fa97e2ac226c4 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/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2875303fb15619..fa957a552d4e68 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -8352,7 +8352,9 @@ class NonDefaultAfterDefault(NamedTuple): def test_annotation_usage_with_methods(self): self.assertEqual(XMeth(1).double(), 2) - self.assertEqual(XMeth(42).x, XMeth(42)[0]) + with warnings.catch_warnings(category=DeprecationWarning): + warnings.simplefilter("ignore", category=DeprecationWarning) + self.assertEqual(XMeth(42).x, XMeth(42)[0]) self.assertEqual(str(XRepr(42)), '42 -> 1') self.assertEqual(XRepr(1, 2) + XRepr(3), 0) diff --git a/Lib/test/test_yield_from.py b/Lib/test/test_yield_from.py index 74c9fa16987638..226bf34e723ea1 100644 --- a/Lib/test/test_yield_from.py +++ b/Lib/test/test_yield_from.py @@ -963,7 +963,7 @@ def one(): def test_delegator_is_visible_to_debugger(self): def call_stack(): - return [f[3] for f in inspect.stack()] + return [f.function for f in inspect.stack()] def gen(): yield call_stack() diff --git a/Lib/xmlrpc/client.py b/Lib/xmlrpc/client.py index 84e4e4d11a7319..418ffbfecb82f5 100644 --- a/Lib/xmlrpc/client.py +++ b/Lib/xmlrpc/client.py @@ -1402,7 +1402,7 @@ def __init__(self, uri, transport=None, encoding=None, verbose=False, if p.scheme not in ("http", "https"): raise OSError("unsupported XML-RPC protocol") self.__host = p.netloc - self.__handler = urllib.parse.urlunsplit(["", "", *p[2:]]) + self.__handler = urllib.parse.urlunsplit(["", "", p.path, p.query, p.fragment]) if not self.__handler: self.__handler = "/RPC2" From 79f7cd33887d611a360ce989283a018894bd6fff Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 7 Aug 2026 23:45:08 +0200 Subject: [PATCH 5/8] Fix test_embed.test_no_memleak() --- Python/pylifecycle.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 8283691b7e3d84..759971027af6d3 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2118,8 +2118,9 @@ finalize_interp_clear(PyThreadState *tstate) _PyExc_ClearExceptionGroupType(interp); _Py_clear_generic_types(interp); _PyTypes_FiniCachedDescriptors(interp); + _PySys_Fini(interp); - /* Clear interpreter state and all thread states */ + /* Clear interpreter state and all thread states: last GC collection! */ _PyInterpreterState_Clear(tstate); /* Clear all loghooks */ @@ -2137,7 +2138,6 @@ finalize_interp_clear(PyThreadState *tstate) } finalize_interp_types(interp); - _PySys_Fini(interp); /* Finalize dtoa at last so that finalizers calling repr of float doesn't crash */ _PyDtoa_Fini(interp); From 0a61d7f07c0f2ce19deea5ce99fcea016263e573 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 8 Aug 2026 00:37:30 +0200 Subject: [PATCH 6/8] Fix test_profiling --- Lib/profiling/sampling/collector.py | 5 ++++- .../test_sampling_profiler/_live_collector_helpers.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/profiling/sampling/collector.py b/Lib/profiling/sampling/collector.py index 1dc3656e0ebe97..3aec13477e6e2f 100644 --- a/Lib/profiling/sampling/collector.py +++ b/Lib/profiling/sampling/collector.py @@ -47,7 +47,10 @@ def extract_lineno(location): return 0 if isinstance(location, int): return location - return location[0] + try: + return location[0] + except DeprecationWarning as exc: + raise Exception(type(location)) from exc def _is_internal_frame(frame): if isinstance(frame, tuple): diff --git a/Lib/test/test_profiling/test_sampling_profiler/_live_collector_helpers.py b/Lib/test/test_profiling/test_sampling_profiler/_live_collector_helpers.py index 2c672895099140..52f0dcc5c165dd 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/_live_collector_helpers.py +++ b/Lib/test/test_profiling/test_sampling_profiler/_live_collector_helpers.py @@ -9,7 +9,9 @@ # Matches the C structseq LocationInfo from _remote_debugging -LocationInfo = namedtuple('LocationInfo', ['lineno', 'end_lineno', 'col_offset', 'end_col_offset']) +LocationInfo = namedtuple('LocationInfo', + ['lineno', 'end_lineno', 'col_offset', 'end_col_offset'], + deprecate_tuple_api=False) class MockFrameInfo: From a8a9cfae0a6e44e3ea1738803c99ea4aee2b2825 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 8 Aug 2026 00:39:25 +0200 Subject: [PATCH 7/8] Fix test_tkinter --- Lib/tkinter/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index dde4accecf61ba..bf8e755283dc8b 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -149,7 +149,8 @@ def _splitdict(tk, v, cut_minus=True, conv=None): return dict class _VersionInfoType(collections.namedtuple('_VersionInfoType', - ('major', 'minor', 'micro', 'releaselevel', 'serial'))): + ('major', 'minor', 'micro', 'releaselevel', 'serial'), + deprecate_tuple_api=False)): def __str__(self): if self.releaselevel == 'final': return f'{self.major}.{self.minor}.{self.micro}' From 88aefc2731d5dd7188d90c61eaefef23f393f749 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 8 Aug 2026 01:34:33 +0200 Subject: [PATCH 8/8] Don't deprecate tuple API of sys.getwindowsversion() --- Python/sysmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 8a82804ed4ba9e..40879485cc9c99 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -4073,7 +4073,7 @@ _PySys_InitCore(PyThreadState *tstate, PyObject *sysdict) #if defined(MS_WINDOWS) /* getwindowsversion */ state->windows_version_type = _PyStructSequence_NewType( - &windows_version_desc, Py_TPFLAGS_DISALLOW_INSTANTIATION, 1); + &windows_version_desc, Py_TPFLAGS_DISALLOW_INSTANTIATION, 0); if (state->windows_version_type == NULL) { goto type_init_failed; }