https://github.com/python/cpython/commit/dbac44740fe5b6402464ca272414f9b32f5999e2 commit: dbac44740fe5b6402464ca272414f9b32f5999e2 branch: main author: sobolevn <[email protected]> committer: pablogsal <[email protected]> date: 2026-09-24T16:29:48Z summary:
gh-145868: Suggest names on typos in `__future__` imports (#154347) * gh-145868: Suggest names on typos in `__future__` imports * Address review * Address review * gh-145868: Suggest only valid future features --------- Co-authored-by: Pablo Galindo Salgado <[email protected]> files: A Misc/NEWS.d/next/Core_and_Builtins/2026-07-21-14-38-49.gh-issue-145868.7iGRvU.rst M Lib/test/test_future_stmt/test_future.py M Python/future.c diff --git a/Lib/test/test_future_stmt/test_future.py b/Lib/test/test_future_stmt/test_future.py index acd8d76dc90a29..5243d24a9ecf26 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,44 @@ 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"), + ]) + 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, + ) + + @subTests("name", ["missing_name", "brces", "brace"]) + def test_no_suggestion_on_missing_name(self, name): + self.assertSyntaxError( + f"from __future__ import {name}", + lineno=1, + message=f"future feature '{name}' is not defined", + offset=24, ) def test_future_import_not_on_top(self): @@ -137,7 +173,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..c3aa54979f3d16 --- /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 @@ +Suggest the closest valid feature name in the :exc:`SyntaxError` message +when an unknown name is imported from :mod:`__future__`. diff --git a/Python/future.c b/Python/future.c index 79b6c0c503bace..41c4fa07916189 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) @@ -48,8 +49,37 @@ 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); + // Keep this list in sync with the feature checks above. + PyObject *future_features = Py_BuildValue("[ssssssssss]", + 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); + PyObject *suggestion = NULL; + if (future_features != NULL) { + suggestion = _Py_CalculateSuggestions(future_features, + name->name); + } + if (suggestion != 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, _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
