From 19e123c5cc7edaddcba53525d4defacd00b2fec6 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 21 Jul 2026 14:40:15 +0300 Subject: [PATCH 1/3] gh-145868: Suggest names on typos in `__future__` imports --- Include/cpython/compile.h | 1 + Lib/test/test_future_stmt/test_future.py | 44 +++++++++++++++++-- ...-07-21-14-38-49.gh-issue-145868.7iGRvU.rst | 2 + Python/future.c | 35 +++++++++++++-- 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-21-14-38-49.gh-issue-145868.7iGRvU.rst diff --git a/Include/cpython/compile.h b/Include/cpython/compile.h index cfdb7080d45f2b..91100e14bedbbc 100644 --- a/Include/cpython/compile.h +++ b/Include/cpython/compile.h @@ -44,6 +44,7 @@ typedef struct { #define FUTURE_BARRY_AS_BDFL "barry_as_FLUFL" #define FUTURE_GENERATOR_STOP "generator_stop" #define FUTURE_ANNOTATIONS "annotations" +#define FUTURE_BRACES "braces" #define PY_INVALID_STACK_EFFECT INT_MAX PyAPI_FUNC(int) PyCompile_OpcodeStackEffect(int opcode, int oparg); diff --git a/Lib/test/test_future_stmt/test_future.py b/Lib/test/test_future_stmt/test_future.py index acd8d76dc90a29..c81ea52f88aa95 100644 --- a/Lib/test/test_future_stmt/test_future.py +++ b/Lib/test/test_future_stmt/test_future.py @@ -87,8 +87,46 @@ def test_unknown_future_flag(self): from __future__ import rested_snopes # typo error here: nested => rested """ self.assertSyntaxError( - code, lineno=2, - message='future feature rested_snopes is not defined', offset=24, + code, + lineno=2, + message=( + "future feature 'rested_snopes' is not defined. " + "Did you mean: 'nested_scopes'?" + ), + offset=24, + ) + + def test_typos_in_future_imports(self): + for typo, origin in { + "nest_scopes": "nested_scopes", + "gneretors": "generators", + "divicion": "division", + "absolute_imports": "absolute_import", + "print_func": "print_function", + "unicode_literal": "unicode_literals", + "barry_as_bdfl": "barry_as_FLUFL", + "generatorstop": "generator_stop", + "anotations": "annotations", + "brces": "braces", + "brace": "braces", + }.items(): + with self.subTest(typo=typo, origin=origin): + self.assertSyntaxError( + f"from __future__ import {typo}", + lineno=1, + message=( + f"future feature '{typo}' is not defined. " + f"Did you mean: '{origin}'?" + ), + offset=24, + ) + + def test_no_suggestion_on_missing_name(self): + self.assertSyntaxError( + "from __future__ import missing_name", + lineno=1, + message="future feature 'missing_name' is not defined", + offset=24, ) def test_future_import_not_on_top(self): @@ -137,7 +175,7 @@ def test_future_import_star(self): code = """ from __future__ import * """ - self.assertSyntaxError(code, message='future feature * is not defined', offset=24) + self.assertSyntaxError(code, message="future feature '*' is not defined", offset=24) def test_future_import_braces(self): code = """ diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-21-14-38-49.gh-issue-145868.7iGRvU.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-21-14-38-49.gh-issue-145868.7iGRvU.rst new file mode 100644 index 00000000000000..50b68667faef58 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-21-14-38-49.gh-issue-145868.7iGRvU.rst @@ -0,0 +1,2 @@ +Calculate suggestions during :exc:`SyntaxError` on typos when importing +names from :mod:`__future__`. diff --git a/Python/future.c b/Python/future.c index 79b6c0c503bace..72746a136d37b9 100644 --- a/Python/future.c +++ b/Python/future.c @@ -2,8 +2,9 @@ #include "pycore_ast.h" // _PyAST_GetDocString() #include "pycore_symtable.h" // _PyFutureFeatures #include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString() +#include "pycore_pyerrors.h" // _Py_CalculateSuggestions() -#define UNDEFINED_FUTURE_FEATURE "future feature %.100s is not defined" +#define UNDEFINED_FUTURE_FEATURE "future feature '%.100s' is not defined" static int future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename) @@ -38,7 +39,7 @@ future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename) continue; } else if (strcmp(feature, FUTURE_ANNOTATIONS) == 0) { ff->ff_features |= CO_FUTURE_ANNOTATIONS; - } else if (strcmp(feature, "braces") == 0) { + } else if (strcmp(feature, FUTURE_BRACES) == 0) { PyErr_SetString(PyExc_SyntaxError, "not a chance"); PyErr_RangedSyntaxLocationObject(filename, @@ -48,8 +49,34 @@ future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename) name->end_col_offset + 1); return 0; } else { - PyErr_Format(PyExc_SyntaxError, - UNDEFINED_FUTURE_FEATURE, feature); + PyObject *suggestion; + PyObject *future_features = Py_BuildValue("[sssssssssss]", + FUTURE_NESTED_SCOPES, + FUTURE_GENERATORS, + FUTURE_DIVISION, + FUTURE_ABSOLUTE_IMPORT, + FUTURE_WITH_STATEMENT, + FUTURE_PRINT_FUNCTION, + FUTURE_UNICODE_LITERALS, + FUTURE_BARRY_AS_BDFL, + FUTURE_GENERATOR_STOP, + FUTURE_ANNOTATIONS, + FUTURE_BRACES); + if ( + future_features != NULL && + (suggestion = _Py_CalculateSuggestions(future_features, name->name)) != NULL + ) { + PyErr_Format(PyExc_SyntaxError, + UNDEFINED_FUTURE_FEATURE ". Did you mean: %R?", + feature, suggestion); + } + else { + // Do not fail on missing suggestion, + // just show the default message. + PyErr_Format(PyExc_SyntaxError, + UNDEFINED_FUTURE_FEATURE, + feature); + } PyErr_RangedSyntaxLocationObject(filename, name->lineno, name->col_offset + 1, From 03b06c947d807c36a39ceb4891dba5de848df5c2 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 22 Jul 2026 13:29:05 +0300 Subject: [PATCH 2/3] Address review --- Lib/test/test_future_stmt/test_future.py | 49 ++++++++++++------------ Python/future.c | 4 +- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_future_stmt/test_future.py b/Lib/test/test_future_stmt/test_future.py index c81ea52f88aa95..6d73cb9727c34e 100644 --- a/Lib/test/test_future_stmt/test_future.py +++ b/Lib/test/test_future_stmt/test_future.py @@ -3,7 +3,7 @@ import __future__ import ast import unittest -from test.support import force_not_colorized, import_helper +from test.support import force_not_colorized, import_helper, subTests from test.support.script_helper import spawn_python, kill_python from textwrap import dedent import os @@ -96,30 +96,29 @@ def test_unknown_future_flag(self): offset=24, ) - def test_typos_in_future_imports(self): - for typo, origin in { - "nest_scopes": "nested_scopes", - "gneretors": "generators", - "divicion": "division", - "absolute_imports": "absolute_import", - "print_func": "print_function", - "unicode_literal": "unicode_literals", - "barry_as_bdfl": "barry_as_FLUFL", - "generatorstop": "generator_stop", - "anotations": "annotations", - "brces": "braces", - "brace": "braces", - }.items(): - with self.subTest(typo=typo, origin=origin): - self.assertSyntaxError( - f"from __future__ import {typo}", - lineno=1, - message=( - f"future feature '{typo}' is not defined. " - f"Did you mean: '{origin}'?" - ), - offset=24, - ) + @subTests("typo, origin", [ + ("nest_scopes", "nested_scopes"), + ("gneretors", "generators"), + ("divicion", "division"), + ("absolute_imports", "absolute_import"), + ("print_func", "print_function"), + ("unicode_literal", "unicode_literals"), + ("barry_as_bdfl", "barry_as_FLUFL"), + ("generatorstop", "generator_stop"), + ("anotations", "annotations"), + ("brces", "braces"), + ("brace", "braces"), + ]) + def test_typos_in_future_imports(self, typo, origin): + self.assertSyntaxError( + f"from __future__ import {typo}", + lineno=1, + message=( + f"future feature '{typo}' is not defined. " + f"Did you mean: '{origin}'?" + ), + offset=24, + ) def test_no_suggestion_on_missing_name(self): self.assertSyntaxError( diff --git a/Python/future.c b/Python/future.c index 72746a136d37b9..5dff3c2379790f 100644 --- a/Python/future.c +++ b/Python/future.c @@ -1,8 +1,8 @@ #include "Python.h" #include "pycore_ast.h" // _PyAST_GetDocString() +#include "pycore_pyerrors.h" // _Py_CalculateSuggestions() #include "pycore_symtable.h" // _PyFutureFeatures #include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString() -#include "pycore_pyerrors.h" // _Py_CalculateSuggestions() #define UNDEFINED_FUTURE_FEATURE "future feature '%.100s' is not defined" @@ -69,6 +69,8 @@ future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename) PyErr_Format(PyExc_SyntaxError, UNDEFINED_FUTURE_FEATURE ". Did you mean: %R?", feature, suggestion); + Py_DECREF(future_features); + Py_DECREF(suggestion); } else { // Do not fail on missing suggestion, From 0bb63cff7079ef8f2678fcc62661bbf8031b86f8 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 22 Jul 2026 15:31:43 +0300 Subject: [PATCH 3/3] Address review --- Python/future.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/future.c b/Python/future.c index 5dff3c2379790f..0cac01f9a364c3 100644 --- a/Python/future.c +++ b/Python/future.c @@ -69,7 +69,6 @@ future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename) PyErr_Format(PyExc_SyntaxError, UNDEFINED_FUTURE_FEATURE ". Did you mean: %R?", feature, suggestion); - Py_DECREF(future_features); Py_DECREF(suggestion); } else { @@ -79,6 +78,7 @@ future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename) UNDEFINED_FUTURE_FEATURE, feature); } + Py_XDECREF(future_features); PyErr_RangedSyntaxLocationObject(filename, name->lineno, name->col_offset + 1,