From 5f7d7096129e055dc34838dfc2aee2190c635006 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 4 Sep 2026 11:10:28 +0300 Subject: [PATCH 1/4] gh-156886: Fix spurious ZeroDivisionError for complex powers (GH-156887) cos() and sin() of an infinite phase set errno to EDOM, which complex_pow() reported as a zero base. Co-Authored-By: Claude Opus 5 (1M context) --- Lib/test/test_complex.py | 10 ++++++++++ .../2026-09-03-12-40-00.gh-issue-156886.Kx7mQt.rst | 3 +++ Objects/complexobject.c | 3 +++ 3 files changed, 16 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-03-12-40-00.gh-issue-156886.Kx7mQt.rst diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 4cc9eea1f27214d..3d02bb6ec2389ba 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -366,6 +366,16 @@ def test_pow(self): self.assertRaises(TypeError, pow, None, 1j) self.assertAlmostEqual(pow(1j, 0.5), 0.7071067811865476+0.7071067811865475j) + # gh-156886: an infinite phase is not a zero base. + for base, exp in [(complex(INF), 1j), + (complex(INF, 1), 1j), + (1e300, 1e308j), + (complex(2), complex(0, INF))]: + with self.subTest(base=base, exponent=exp): + r = base ** exp + self.assertTrue(isnan(r.real)) + self.assertTrue(isnan(r.imag)) + a = 3.33+4.43j self.assertEqual(a ** 0j, 1) self.assertEqual(a ** 0.+0.j, 1) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-03-12-40-00.gh-issue-156886.Kx7mQt.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-03-12-40-00.gh-issue-156886.Kx7mQt.rst new file mode 100644 index 000000000000000..69fbc33992100e3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-03-12-40-00.gh-issue-156886.Kx7mQt.rst @@ -0,0 +1,3 @@ +Fix :exc:`ZeroDivisionError` spuriously raised for a complex power +with a non-zero base when the phase of the result is infinite, +e.g. ``1e300**1e308j`` or ``complex('inf')**1j``. diff --git a/Objects/complexobject.c b/Objects/complexobject.c index 5f7acdeb7cfd8df..9328baf013c972a 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -331,6 +331,9 @@ _Py_c_pow(Py_complex a, Py_complex b) r.real = len*cos(phase); r.imag = len*sin(phase); + /* Don't rely on errno set by the math functions above, for + example cos() and sin() set EDOM for an infinite phase. */ + errno = 0; if (isfinite(a.real) && isfinite(a.imag) && isfinite(b.real) && isfinite(b.imag)) { From 112560ee34a70c02baebdc11519d1f77e90ecab8 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 4 Sep 2026 11:32:15 +0200 Subject: [PATCH 2/4] gh-155561: Use multi-phase init for Modules/_testlimitedcapi.c (GH-156052) Co-authored-by: Victor Stinner --- Modules/_testlimitedcapi.c | 109 +++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 59 deletions(-) diff --git a/Modules/_testlimitedcapi.c b/Modules/_testlimitedcapi.c index de8bed77d7d3275..1f7392dd430d4cb 100644 --- a/Modules/_testlimitedcapi.c +++ b/Modules/_testlimitedcapi.c @@ -2,122 +2,113 @@ * Test the limited C API. * * The 'test_*' functions exported by this module are run as part of the - * standard Python regression test, via Lib/test/test_capi.py. + * standard Python regression test, via Lib/test/test_capi/test_misc.py. */ -#include "pyconfig.h" // Py_GIL_DISABLED - -#ifdef Py_GIL_DISABLED - // Cannot test the limited C API -#else - // Use the oldest limited C API version -# define Py_LIMITED_API 0x03020000 -#endif - #include "_testlimitedcapi/parts.h" -static PyMethodDef TestMethods[] = { - {NULL, NULL} /* sentinel */ -}; - -static struct PyModuleDef _testlimitedcapimodule = { - PyModuleDef_HEAD_INIT, - .m_name = "_testlimitedcapi", - .m_size = 0, - .m_methods = TestMethods, -}; - -PyMODINIT_FUNC -PyInit__testlimitedcapi(void) +static int +module_exec(PyObject *mod) { - PyObject *mod = PyModule_Create(&_testlimitedcapimodule); - if (mod == NULL) { - return NULL; - } -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); -#endif - if (_PyTestLimitedCAPI_Init_Abstract(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_ByteArray(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Bytes(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Capsule(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Codec(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Complex(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Dict(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Eval(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Float(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_HeaptypeRelative(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Import(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_List(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Long(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Object(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_PyOS(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Set(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Slots(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Sys(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_ThreadState(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Tuple(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Unicode(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_VectorcallLimited(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Version(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_File(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Weakref(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Run(mod) < 0) { - return NULL; + return -1; } if (_PyTestLimitedCAPI_Init_Type(mod) < 0) { - return NULL; + return -1; } - return mod; + return 0; +} + +static struct PyModuleDef _testlimitedcapimodule_def = { + PyModuleDef_HEAD_INIT, + .m_name = "_testlimitedcapi", + .m_size = 0, + .m_slots = (PyModuleDef_Slot[]){ + {Py_mod_exec, module_exec}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0} + } +}; + +PyMODINIT_FUNC +PyInit__testlimitedcapi(void) +{ + return PyModuleDef_Init(&_testlimitedcapimodule_def); } From e50d23389be07bf702bde74c3cda794c6febb53e Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 4 Sep 2026 07:10:07 -0400 Subject: [PATCH 3/4] gh-156896: Stop scrubbing tkinker submodules upon idlelib.run import (#156915) Instead, invoke the scrubbing within run.main, which is called after the import when starting the IDLE user process. To manually test that scrub_tkinter_submodules is called when proper, start (or restart) Shell, enter `import tkinter; dir(tkinter), and check that that font, messagebox, ttk, and the dialog modules are missing. It is obvious from the code that the function is not otherwise called. To test anyway, continue with `import tkinter.ttk; import idlelib.run; tkinter.ttk` and check for proper output. --- Lib/idlelib/run.py | 17 +++++++++-------- ...26-09-03-17-16-26.gh-issue-156896.XTQJlq.rst | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-09-03-17-16-26.gh-issue-156896.XTQJlq.rst diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index 802f0248e4e4594..2725043b4ed9253 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -1,6 +1,6 @@ """ idlelib.run -Simplified, pyshell.ModifiedInterpreter spawns a subprocess with +Simplified: pyshell.ModifiedInterpreter spawns a subprocess with f'''{sys.executable} -c "__import__('idlelib.run').run.main()"''' '.run' is needed because __import__ returns idlelib, not idlelib.run. """ @@ -26,11 +26,12 @@ from idlelib import rpc # multiple objects from idlelib import stackviewer # StackTreeItem from idlelib import util # fix_scaling -import __main__ +import __main__ # self.locals in Executive.__init__. import tkinter # Use tcl and, if startup fails, messagebox. -if not hasattr(sys.modules['idlelib.run'], 'firstrun'): - # Undo modifications of tkinter by idlelib imports; see bpo-25507. + +def scrub_tkinter_submodules(): # Call in main when starting user process. + # Undo modifications of tkinter by idlelib imports; see gh-69693. # Which of these submodules got imported (and thus added as a tkinter # attribute) depends on what idlelib pulled in, so tolerate missing # ones rather than assuming a fixed set; see gh-59396. @@ -42,8 +43,6 @@ del sys.modules['tkinter.' + mod] except (AttributeError, KeyError): pass - # Avoid AttributeError if run again; see bpo-37038. - sys.modules['idlelib.run'].firstrun = False LOCALHOST = '127.0.0.1' @@ -139,6 +138,9 @@ def main(del_exitfunc=False): register and unregister themselves. """ + + scrub_tkinter_submodules() + global exit_now global quitting global no_exitfunc @@ -705,8 +707,7 @@ def stackviewer(self, flist_oid=None): item = stackviewer.StackTreeItem(exc, flist) return debugobj_r.remote_object_tree_item(item) - -if __name__ == '__main__': +if __name__ == '__main__': # __name__ is 'idlelib.run' in user subprocess. from unittest import main main('idlelib.idle_test.test_run', verbosity=2) diff --git a/Misc/NEWS.d/next/IDLE/2026-09-03-17-16-26.gh-issue-156896.XTQJlq.rst b/Misc/NEWS.d/next/IDLE/2026-09-03-17-16-26.gh-issue-156896.XTQJlq.rst new file mode 100644 index 000000000000000..fd759cdbe92c2ff --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-03-17-16-26.gh-issue-156896.XTQJlq.rst @@ -0,0 +1 @@ +Stop deleting tkinter submodules when idlelib.run is imported. From 59a691361cb7b1d4fde3b92f98a61490381c62c9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 4 Sep 2026 14:14:13 +0300 Subject: [PATCH 4/4] gh-156894: Fix the position of syntax errors which cover a range (GH-156901) The callers of _PyTokenizer_syntaxerror_known_range() pass columns in bytes, but SyntaxError.offset and end_offset are columns in characters. Co-Authored-By: Claude Opus 5 (1M context) --- Lib/test/test_exceptions.py | 17 ++++++++++++++++ Lib/test/test_tokenize.py | 2 +- ...-09-03-15-20-00.gh-issue-156894.Rt9Bx4.rst | 2 ++ Parser/tokenizer/helpers.c | 20 +++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-03-15-20-00.gh-issue-156894.Rt9Bx4.rst diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index c34cf44d722456c..0c02b38dd3c0a89 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -233,6 +233,23 @@ def test_error_offset_continuation_characters(self): check = self.check check('"\\\n"(1 for c in I,\\\n\\', 2, 2) + def testSyntaxErrorRange(self): + # gh-156894: the position was reported in bytes, not in characters, + # for the errors which cover a range + for source, offset, end_offset in [ + ('abcd = 00010', 8, 11), + ('\u03b1\u03b2\u03b3\u03b4 = 00010', 8, 11), + ('a\u0301b\u0308c\u20d7d\u1ab0 = 00010', 12, 15), + ("abcd = ub'a'", 8, 10), + ("\u03b1\u03b2\u03b3\u03b4 = ub'a'", 8, 10), + ("a\u0301b\u0308c\u20d7d\u1ab0 = ub'a'", 12, 14), + ]: + with self.subTest(source=source): + with self.assertRaises(SyntaxError) as cm: + compile(source, '', 'exec') + self.assertEqual(cm.exception.offset, offset) + self.assertEqual(cm.exception.end_offset, end_offset) + def testSyntaxErrorOffset(self): check = self.check check('def fact(x):\n\treturn x!\n', 2, 10) diff --git a/Lib/test/test_tokenize.py b/Lib/test/test_tokenize.py index 7e02191db86be5a..c471f857660ec90 100644 --- a/Lib/test/test_tokenize.py +++ b/Lib/test/test_tokenize.py @@ -2566,7 +2566,7 @@ def test_tolerant_incompatible_prefix_position_after_non_ascii(self): self._get_tokens('bé )tf"2 ', extra_tokens=True) self.assertEqual( caught.exception.args, - ("'f' and 't' prefixes are incompatible", (1, 6)), + ("'f' and 't' prefixes are incompatible", (1, 5)), ) def test_tolerant_fstring_closer_at_expression_entry_depth(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-03-15-20-00.gh-issue-156894.Rt9Bx4.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-03-15-20-00.gh-issue-156894.Rt9Bx4.rst new file mode 100644 index 000000000000000..9b279ab27947867 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-03-15-20-00.gh-issue-156894.Rt9Bx4.rst @@ -0,0 +1,2 @@ +Fix the position of syntax errors which cover a range if the line contains +non-ASCII characters before the error. diff --git a/Parser/tokenizer/helpers.c b/Parser/tokenizer/helpers.c index bbd64760a18a664..e99be70ea5e9801 100644 --- a/Parser/tokenizer/helpers.c +++ b/Parser/tokenizer/helpers.c @@ -9,6 +9,20 @@ /* ############## ERRORS ############## */ +/* Convert a 1-based column in bytes into a 1-based column in characters. + The line is UTF-8 encoded, so it is enough to skip continuation bytes. */ +static int +byte_col_to_char_col(const char *line, int byte_col) +{ + int char_col = 1; + for (int i = 0; i < byte_col - 1; i++) { + if ((line[i] & 0xC0) != 0x80) { + char_col++; + } + } + return char_col; +} + static int _syntaxerror_range(struct tok_state *tok, const char *format, int col_offset, int end_col_offset, @@ -35,9 +49,15 @@ _syntaxerror_range(struct tok_state *tok, const char *format, if (col_offset == -1) { col_offset = (int)PyUnicode_GET_LENGTH(errtext); } + else if (col_offset > 0) { + col_offset = byte_col_to_char_col(tok->line_start, col_offset); + } if (end_col_offset == -1) { end_col_offset = col_offset; } + else if (end_col_offset > 0) { + end_col_offset = byte_col_to_char_col(tok->line_start, end_col_offset); + } Py_ssize_t line_len = strcspn(tok->line_start, "\n"); if (line_len != tok->cur - tok->line_start) {