Skip to content

Commit dbac447

Browse files
sobolevnpablogsal
andauthored
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 <pablogsal@gmail.com>
1 parent b7f81d4 commit dbac447

3 files changed

Lines changed: 75 additions & 7 deletions

File tree

‎Lib/test/test_future_stmt/test_future.py‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import __future__
44
import ast
55
import unittest
6-
from test.support import force_not_colorized, import_helper
6+
from test.support import force_not_colorized, import_helper, subTests
77
from test.support.script_helper import spawn_python, kill_python
88
from textwrap import dedent
99
import os
@@ -87,8 +87,44 @@ def test_unknown_future_flag(self):
8787
from __future__ import rested_snopes # typo error here: nested => rested
8888
"""
8989
self.assertSyntaxError(
90-
code, lineno=2,
91-
message='future feature rested_snopes is not defined', offset=24,
90+
code,
91+
lineno=2,
92+
message=(
93+
"future feature 'rested_snopes' is not defined. "
94+
"Did you mean: 'nested_scopes'?"
95+
),
96+
offset=24,
97+
)
98+
99+
@subTests("typo, origin", [
100+
("nest_scopes", "nested_scopes"),
101+
("gneretors", "generators"),
102+
("divicion", "division"),
103+
("absolute_imports", "absolute_import"),
104+
("print_func", "print_function"),
105+
("unicode_literal", "unicode_literals"),
106+
("barry_as_bdfl", "barry_as_FLUFL"),
107+
("generatorstop", "generator_stop"),
108+
("anotations", "annotations"),
109+
])
110+
def test_typos_in_future_imports(self, typo, origin):
111+
self.assertSyntaxError(
112+
f"from __future__ import {typo}",
113+
lineno=1,
114+
message=(
115+
f"future feature '{typo}' is not defined. "
116+
f"Did you mean: '{origin}'?"
117+
),
118+
offset=24,
119+
)
120+
121+
@subTests("name", ["missing_name", "brces", "brace"])
122+
def test_no_suggestion_on_missing_name(self, name):
123+
self.assertSyntaxError(
124+
f"from __future__ import {name}",
125+
lineno=1,
126+
message=f"future feature '{name}' is not defined",
127+
offset=24,
92128
)
93129

94130
def test_future_import_not_on_top(self):
@@ -137,7 +173,7 @@ def test_future_import_star(self):
137173
code = """
138174
from __future__ import *
139175
"""
140-
self.assertSyntaxError(code, message='future feature * is not defined', offset=24)
176+
self.assertSyntaxError(code, message="future feature '*' is not defined", offset=24)
141177

142178
def test_future_import_braces(self):
143179
code = """
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Suggest the closest valid feature name in the :exc:`SyntaxError` message
2+
when an unknown name is imported from :mod:`__future__`.

‎Python/future.c‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
#include "Python.h"
22
#include "pycore_ast.h" // _PyAST_GetDocString()
3+
#include "pycore_pyerrors.h" // _Py_CalculateSuggestions()
34
#include "pycore_symtable.h" // _PyFutureFeatures
45
#include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString()
56

6-
#define UNDEFINED_FUTURE_FEATURE "future feature %.100s is not defined"
7+
#define UNDEFINED_FUTURE_FEATURE "future feature '%.100s' is not defined"
78

89
static int
910
future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename)
@@ -48,8 +49,37 @@ future_check_features(_PyFutureFeatures *ff, stmt_ty s, PyObject *filename)
4849
name->end_col_offset + 1);
4950
return 0;
5051
} else {
51-
PyErr_Format(PyExc_SyntaxError,
52-
UNDEFINED_FUTURE_FEATURE, feature);
52+
// Keep this list in sync with the feature checks above.
53+
PyObject *future_features = Py_BuildValue("[ssssssssss]",
54+
FUTURE_NESTED_SCOPES,
55+
FUTURE_GENERATORS,
56+
FUTURE_DIVISION,
57+
FUTURE_ABSOLUTE_IMPORT,
58+
FUTURE_WITH_STATEMENT,
59+
FUTURE_PRINT_FUNCTION,
60+
FUTURE_UNICODE_LITERALS,
61+
FUTURE_BARRY_AS_BDFL,
62+
FUTURE_GENERATOR_STOP,
63+
FUTURE_ANNOTATIONS);
64+
PyObject *suggestion = NULL;
65+
if (future_features != NULL) {
66+
suggestion = _Py_CalculateSuggestions(future_features,
67+
name->name);
68+
}
69+
if (suggestion != NULL) {
70+
PyErr_Format(PyExc_SyntaxError,
71+
UNDEFINED_FUTURE_FEATURE ". Did you mean: %R?",
72+
feature, suggestion);
73+
Py_DECREF(suggestion);
74+
}
75+
else {
76+
// Do not fail on missing suggestion,
77+
// just show the default message.
78+
PyErr_Format(PyExc_SyntaxError,
79+
UNDEFINED_FUTURE_FEATURE,
80+
feature);
81+
}
82+
Py_XDECREF(future_features);
5383
PyErr_RangedSyntaxLocationObject(filename,
5484
name->lineno,
5585
name->col_offset + 1,

0 commit comments

Comments
 (0)