diff --git a/Include/cpython/compile.h b/Include/cpython/compile.h index cfdb7080d45f2b1..91100e14bedbbc0 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 acd8d76dc90a293..6d73cb9727c34e7 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 @@ -87,8 +87,45 @@ 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, + ) + + @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( + "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 +174,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 000000000000000..50b68667faef586 --- /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 79b6c0c503bace1..0cac01f9a364c33 100644 --- a/Python/future.c +++ b/Python/future.c @@ -1,9 +1,10 @@ #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() -#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,36 @@ 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); + Py_DECREF(suggestion); + } + else { + // Do not fail on missing suggestion, + // just show the default message. + PyErr_Format(PyExc_SyntaxError, + UNDEFINED_FUTURE_FEATURE, + feature); + } + Py_XDECREF(future_features); PyErr_RangedSyntaxLocationObject(filename, name->lineno, name->col_offset + 1,