diff --git a/Lib/_ast_unparse.py b/Lib/_ast_unparse.py index 916bb25d74dee9..58bc06397f5134 100644 --- a/Lib/_ast_unparse.py +++ b/Lib/_ast_unparse.py @@ -925,7 +925,11 @@ def visit_Attribute(self, node): # Special case: 3.__abs__() is a syntax error, so if node.value # is an integer literal then we need to either parenthesize # it or add an extra space to get 3 .__abs__(). - if isinstance(node.value, Constant) and isinstance(node.value.value, int): + # bool is a subclass of int, but True.real and False.real are + # valid without a space. + if (isinstance(node.value, Constant) + and isinstance(node.value.value, int) + and not isinstance(node.value.value, bool)): self.write(" ") self.write(".") self.write(node.attr) diff --git a/Lib/test/test_cext/__init__.py b/Lib/test/test_cext/__init__.py index 457925413c94ec..c4fd2a1e044d89 100644 --- a/Lib/test/test_cext/__init__.py +++ b/Lib/test/test_cext/__init__.py @@ -26,6 +26,7 @@ os.path.join(SOURCE_DIR, 'extension.cpp'), os.path.join(SOURCE_DIR, 'setup.py'), ] +RUNTESTS_PY = os.path.join(SOURCE_DIR, 'runtests.py') MSVC = support.MS_WINDOWS @@ -99,21 +100,10 @@ def run_cmd(operation, cmd): cmd.append('-v') run_cmd('Install', cmd) - # Do a reference run. Until we test that running python - # doesn't leak references (gh-94755), run it so one can manually check - # -X showrefcount results against this baseline. - cmd = [python_exe, - '-X', 'dev', - '-X', 'showrefcount', - '-c', 'pass'] - run_cmd('Reference run', cmd) - - # Import the C/C++ extension - cmd = [python_exe, - '-X', 'dev', - '-X', 'showrefcount', - '-c', f"import {extension_name}"] - run_cmd('Import', cmd) + # Import the extension module and run tests. + # On a debug build, check also for reference leaks. + cmd = [python_exe, '-X', 'dev', RUNTESTS_PY, extension_name] + run_cmd('Tests', cmd) class TestPublicC(BaseTests, unittest.TestCase): diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index 038f1a2af46c67..b56c4dbe78a3d8 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -264,8 +264,11 @@ class VirtualPyObject : public PyObject { public: VirtualPyObject(); virtual ~VirtualPyObject() { + PyTypeObject *type = Py_TYPE(this); delete [] internal_data; --instance_count; + // Do not call type->tp_free(this), C++ manages the memory + Py_DECREF(type); } virtual void set_internal_data() { internal_data[0] = 1; @@ -295,7 +298,7 @@ _Py_COMP_DIAG_PUSH #endif PyType_Slot VirtualPyObject_Slots[] = { - {Py_tp_free, (void*)VirtualPyObject::dealloc}, + {Py_tp_dealloc, (void*)VirtualPyObject::dealloc}, {0, _Py_NULL}, }; @@ -333,6 +336,10 @@ test_virtual_object(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) "instance_count should be 0, got %d", VirtualPyObject::instance_count); } + + // Force a garbage collection to delete the temporary heap type + // used by this test + PyGC_Collect(); Py_RETURN_NONE; } #endif // __cplusplus && !Py_TARGET_ABI3T @@ -356,8 +363,6 @@ static PyMethodDef module_methods[] = { static int module_exec(PyObject *module) { - PyObject *result; - #ifdef __STDC_VERSION__ if (PyModule_AddIntMacro(module, __STDC_VERSION__) < 0) { return -1; @@ -368,31 +373,13 @@ module_exec(PyObject *module) return -1; } #endif - - result = PyObject_CallMethod(module, "test_macros", ""); - if (!result) return -1; - Py_DECREF(result); - - result = PyObject_CallMethod(module, "test_datetime", ""); - if (!result) return -1; - Py_DECREF(result); - - result = PyObject_CallMethod(module, "test_unicode", ""); - if (!result) return -1; - Py_DECREF(result); - -#ifdef __cplusplus - result = PyObject_CallMethod(module, "test_api_casts", ""); - if (!result) return -1; - Py_DECREF(result); -#endif - -#if defined(__cplusplus) && !defined(Py_TARGET_ABI3T) - result = PyObject_CallMethod(module, "test_virtual_object", ""); - if (!result) return -1; - Py_DECREF(result); +#ifdef _MSVC_LANG + if (PyModule_AddIntMacro(module, _MSVC_LANG) < 0) { + return -1; + } #endif - + // Ignore "unused argument" warning when none of these macros is defined + (void)module; return 0; } diff --git a/Lib/test/test_cext/runtests.py b/Lib/test/test_cext/runtests.py new file mode 100644 index 00000000000000..86aab671200493 --- /dev/null +++ b/Lib/test/test_cext/runtests.py @@ -0,0 +1,63 @@ +import array +import gc +import importlib +import sys + + +def run_tests(testmod, verbose=True): + for name in dir(testmod): + if not name.startswith('test'): + continue + func = getattr(testmod, name) + print(f"{name}()") + func() + + print("add()") + if testmod.add(11, 23) != 34: + raise AssertionError("add() failed badly") + + print(flush=True) + + +def main(): + if len(sys.argv) < 2: + print("usage: python runtests.py TEST_MODULE_NAME") + sys.exit(1) + module_name = sys.argv[1] + + testmod = importlib.import_module(module_name) + + newline = False + for name in ('__STDC_VERSION__', '__cplusplus', '_MSVC_LANG'): + try: + value = getattr(testmod, name) + except AttributeError: + pass + else: + print(f'{name}: {value}') + newline = True + if newline: + print() + + if hasattr(sys, 'gettotalrefcount'): + # First run to warm up Python. For example, test_datetime() imports + # the datetime module. + run_tests(testmod, verbose=False) + + refcount = array.array('q', [0, 0]) + gc.collect() + + # Check for reference leak + refcount[0] = sys.gettotalrefcount() + run_tests(testmod) + refcount[1] = sys.gettotalrefcount() + + diff = refcount[1] - refcount[0] + if diff >= 1: + raise AssertionError(f'Tests leaked {diff} references') + else: + run_tests(testmod) + + +if __name__ == "__main__": + main() diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index 9bc04fc8e0affb..b1f359764fa4fe 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -335,6 +335,14 @@ def test_unary_parens(self): def test_integer_parens(self): self.check_ast_roundtrip("3 .__abs__()") + def test_attribute_on_bool(self): + # gh-158237: True.real should not gain a space + self.check_src_roundtrip("x = True.real") + self.check_src_roundtrip("x = False.__class__") + + def test_attribute_on_int_still_spaced(self): + self.check_src_roundtrip("x = 3 .__abs__()") + def test_huge_float(self): self.check_ast_roundtrip("1e1000") self.check_ast_roundtrip("-1e1000") diff --git a/Misc/NEWS.d/next/Library/2026-09-26-22-07-09.gh-issue-158237.AbCdEf.rst b/Misc/NEWS.d/next/Library/2026-09-26-22-07-09.gh-issue-158237.AbCdEf.rst new file mode 100644 index 00000000000000..1c28e527dea042 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-26-22-07-09.gh-issue-158237.AbCdEf.rst @@ -0,0 +1,3 @@ +Fix :func:`ast.unparse` to not insert a space before attribute +access on bool constants (e.g. ``True.real`` instead of +``True .real``). diff --git a/Python/codegen.c b/Python/codegen.c index 0ae13e40d4a1ee..71ec33724684dc 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -4625,6 +4625,78 @@ codegen_unpack_starred(compiler *c, location loc, expr_ty value, bool yield) return SUCCESS; } +static int +codegen_comprehension_generator_helper(compiler *c, location elt_loc, int depth, + expr_ty elt, expr_ty val, int type, + bool avoid_creation) +{ + switch (type) { + case COMP_GENEXP: + assert(!avoid_creation); + if (elt->kind == Starred_kind) { + RETURN_IF_ERROR(codegen_unpack_starred(c, elt_loc, elt->v.Starred.value, /*yield=*/true)); + } + else { + VISIT(c, expr, elt); + ADDOP_YIELD(c, elt_loc); + ADDOP(c, elt_loc, POP_TOP); + } + break; + case COMP_LISTCOMP: + if (avoid_creation) { + if (elt->kind == Starred_kind) { + RETURN_IF_ERROR(codegen_unpack_starred(c, elt_loc, elt->v.Starred.value, /*yield=*/false)); + } else { + VISIT(c, expr, elt); + ADDOP(c, elt_loc, POP_TOP); + } + break; + } + if (elt->kind == Starred_kind) { + VISIT(c, expr, elt->v.Starred.value); + ADDOP_I(c, elt_loc, LIST_EXTEND, depth + 1); + } + else { + VISIT(c, expr, elt); + ADDOP_I(c, elt_loc, LIST_APPEND, depth + 1); + } + break; + case COMP_SETCOMP: + assert(!avoid_creation); + if (elt->kind == Starred_kind) { + VISIT(c, expr, elt->v.Starred.value); + ADDOP_I(c, elt_loc, SET_UPDATE, depth + 1); + } + else { + VISIT(c, expr, elt); + ADDOP_I(c, elt_loc, SET_ADD, depth + 1); + } + break; + case COMP_DICTCOMP: + assert(!avoid_creation); + if (val == NULL) { + /* unpacking (**) case */ + VISIT(c, expr, elt); + ADDOP_I(c, elt_loc, DICT_UPDATE, depth + 1); + } + else { + /* With '{k: v}', k is evaluated before v, so we do + the same. */ + VISIT(c, expr, elt); + VISIT(c, expr, val); + elt_loc = LOCATION(elt->lineno, + val->end_lineno, + elt->col_offset, + val->end_col_offset); + ADDOP_I(c, elt_loc, MAP_ADD, depth + 1); + } + break; + default: + return ERROR; + } + return SUCCESS; +} + static int codegen_sync_comprehension_generator(compiler *c, location loc, asdl_comprehension_seq *generators, @@ -4706,67 +4778,14 @@ codegen_sync_comprehension_generator(compiler *c, location loc, /* only append after the last for generator */ if (gen_index >= asdl_seq_LEN(generators)) { /* comprehension specific code */ - switch (type) { - case COMP_GENEXP: - assert(!avoid_creation); - if (elt->kind == Starred_kind) { - RETURN_IF_ERROR(codegen_unpack_starred(c, elt_loc, elt->v.Starred.value, /*yield=*/true)); - } - else { - VISIT(c, expr, elt); - ADDOP_YIELD(c, elt_loc); - ADDOP(c, elt_loc, POP_TOP); - } - break; - case COMP_LISTCOMP: - if (avoid_creation) { - if (elt->kind == Starred_kind) { - RETURN_IF_ERROR(codegen_unpack_starred(c, elt_loc, elt->v.Starred.value, /*yield=*/false)); - } else { - VISIT(c, expr, elt); - ADDOP(c, elt_loc, POP_TOP); - } - break; - } - if (elt->kind == Starred_kind) { - VISIT(c, expr, elt->v.Starred.value); - ADDOP_I(c, elt_loc, LIST_EXTEND, depth + 1); - } - else { - VISIT(c, expr, elt); - ADDOP_I(c, elt_loc, LIST_APPEND, depth + 1); - } - break; - case COMP_SETCOMP: - if (elt->kind == Starred_kind) { - VISIT(c, expr, elt->v.Starred.value); - ADDOP_I(c, elt_loc, SET_UPDATE, depth + 1); - } - else { - VISIT(c, expr, elt); - ADDOP_I(c, elt_loc, SET_ADD, depth + 1); - } - break; - case COMP_DICTCOMP: - if (val == NULL) { - /* unpacking (**) case */ - VISIT(c, expr, elt); - ADDOP_I(c, elt_loc, DICT_UPDATE, depth+1); - } - else { - /* With '{k: v}', k is evaluated before v, so we do - the same. */ - VISIT(c, expr, elt); - VISIT(c, expr, val); - elt_loc = LOCATION(elt->lineno, - val->end_lineno, - elt->col_offset, - val->end_col_offset); - ADDOP_I(c, elt_loc, MAP_ADD, depth + 1); - } - break; - default: - return ERROR; + RETURN_IF_ERROR(codegen_comprehension_generator_helper(c, elt_loc, depth, + elt, val, type, + avoid_creation)); + if (type == COMP_DICTCOMP && val != NULL) { + elt_loc = LOCATION(elt->lineno, + val->end_lineno, + elt->col_offset, + val->end_col_offset); } } @@ -4848,81 +4867,14 @@ codegen_async_comprehension_generator(compiler *c, location loc, /* only append after the last for generator */ if (gen_index >= asdl_seq_LEN(generators)) { /* comprehension specific code */ - switch (type) { - case COMP_GENEXP: - assert(!avoid_creation); - if (elt->kind == Starred_kind) { - NEW_JUMP_TARGET_LABEL(c, unpack_start); - NEW_JUMP_TARGET_LABEL(c, unpack_end); - VISIT(c, expr, elt->v.Starred.value); - ADDOP_I(c, elt_loc, GET_ITER, 0); - USE_LABEL(c, unpack_start); - ADDOP_JUMP(c, elt_loc, FOR_ITER, unpack_end); - ADDOP_YIELD(c, elt_loc); - ADDOP(c, elt_loc, POP_TOP); - ADDOP_JUMP(c, NO_LOCATION, JUMP, unpack_start); - USE_LABEL(c, unpack_end); - ADDOP(c, NO_LOCATION, END_FOR); - ADDOP(c, NO_LOCATION, POP_ITER); - } - else { - VISIT(c, expr, elt); - ADDOP_YIELD(c, elt_loc); - ADDOP(c, elt_loc, POP_TOP); - } - break; - case COMP_LISTCOMP: - if (avoid_creation) { - if (elt->kind == Starred_kind) { - RETURN_IF_ERROR(codegen_unpack_starred(c, elt_loc, elt->v.Starred.value, /*yield=*/false)); - } else { - VISIT(c, expr, elt); - ADDOP(c, elt_loc, POP_TOP); - } - break; - } - - if (elt->kind == Starred_kind) { - VISIT(c, expr, elt->v.Starred.value); - ADDOP_I(c, elt_loc, LIST_EXTEND, depth + 1); - } - else { - VISIT(c, expr, elt); - ADDOP_I(c, elt_loc, LIST_APPEND, depth + 1); - } - break; - case COMP_SETCOMP: - assert(!avoid_creation); - if (elt->kind == Starred_kind) { - VISIT(c, expr, elt->v.Starred.value); - ADDOP_I(c, elt_loc, SET_UPDATE, depth + 1); - } - else { - VISIT(c, expr, elt); - ADDOP_I(c, elt_loc, SET_ADD, depth + 1); - } - break; - case COMP_DICTCOMP: - assert(!avoid_creation); - if (val == NULL) { - /* unpacking (**) case */ - VISIT(c, expr, elt); - ADDOP_I(c, elt_loc, DICT_UPDATE, depth+1); - } - else { - /* With '{k: v}', k is evaluated before v, so we do - the same. */ - VISIT(c, expr, elt); - VISIT(c, expr, val); - elt_loc = LOCATION(elt->lineno, - val->end_lineno, - elt->col_offset, - val->end_col_offset); - ADDOP_I(c, elt_loc, MAP_ADD, depth + 1); - } - break; - default: - return ERROR; + RETURN_IF_ERROR(codegen_comprehension_generator_helper(c, elt_loc, depth, + elt, val, type, + avoid_creation)); + if (type == COMP_DICTCOMP && val != NULL) { + elt_loc = LOCATION(elt->lineno, + val->end_lineno, + elt->col_offset, + val->end_col_offset); } }