GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-91058: Add error suggestions to 'import from' import errors by pablogsal · Pull Request #98305 · python/cpython

gh-91058: Add error suggestions to 'import from' import errors #98305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Sign up for GitHub

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
pablogsal merged 8 commits into python:main from pablogsal:gh-91058
Oct 25, 2022

Uh oh!

There was an error while loading. Please reload this page.

Merged

gh-91058: Add error suggestions to 'import from' import errors #98305

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
8 commits
Select commit Hold shift + click to select a range
a35c5ac
gh-91058: Add error suggestions to 'import from' import errors
pablogsal Oct 15, 2022
bdbb37d
Add NEWS entry
pablogsal Oct 15, 2022
04e1e03
Update Lib/test/test_traceback.py
pablogsal Oct 16, 2022
77f9de1
Update Lib/test/test_traceback.py
pablogsal Oct 16, 2022
dee788b
Update with latest code
pablogsal Oct 17, 2022
c07068f
fixup! Update with latest code
pablogsal Oct 20, 2022
d6908e5
Apply suggestions from code review
pablogsal Oct 20, 2022
de553f6
Update Lib/test/test_traceback.py
pablogsal Oct 20, 2022
File filter
Filter by extension

Clear filters
Conversations
Failed to load comments.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Jump to
Jump to file
Failed to load files.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Diff view
Diff view

There are no files selected for viewing

8 changes: 8 additions & 0 deletions 8 Include/cpython/pyerrors.h
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ typedef struct {
PyObject *msg;
PyObject *name;
PyObject *path;
PyObject *name_from;
} PyImportErrorObject;

typedef struct {
Expand Down Expand Up @@ -176,4 +177,11 @@ PyAPI_FUNC(void) _Py_NO_RETURN _Py_FatalErrorFormat(
const char *format,
...);

extern PyObject *_PyErr_SetImportErrorWithNameFrom(
PyObject *,
PyObject *,
PyObject *,
PyObject *);


#define Py_FatalError(message) _Py_FatalErrorFunc(__func__, (message))
1 change: 1 addition & 0 deletions 1 Include/internal/pycore_global_strings.h
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(n_sequence_fields)
STRUCT_FOR_ID(n_unnamed_fields)
STRUCT_FOR_ID(name)
STRUCT_FOR_ID(name_from)
STRUCT_FOR_ID(namespace_separator)
STRUCT_FOR_ID(namespaces)
STRUCT_FOR_ID(narg)
Expand Down
7 changes: 7 additions & 0 deletions 7 Include/internal/pycore_runtime_init_generated.h
Show comments
View file
Open in desktop

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Oops, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

4 changes: 2 additions & 2 deletions 4 Lib/test/test_call.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ def test_varargs14_kw(self):
itertools.product, 0, repeat=1, foo=2)

def test_varargs15_kw(self):
msg = r"^ImportError\(\) takes at most 2 keyword arguments \(3 given\)$"
msg = r"^ImportError\(\) takes at most 3 keyword arguments \(4 given\)$"
self.assertRaisesRegex(TypeError, msg,
ImportError, 0, name=1, path=2, foo=3)
ImportError, 0, name=1, path=2, name_from=3, foo=3)

def test_varargs16_kw(self):
msg = r"^min\(\) takes at most 2 keyword arguments \(3 given\)$"
Expand Down
122 changes: 122 additions & 0 deletions 122 Lib/test/test_traceback.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
import sys
import types
import inspect
import importlib
import unittest
import re
import tempfile
import random
import string
from test import support
import shutil
from test.support import (Error, captured_output, cpython_only, ALWAYS_EQ,
requires_debug_ranges, has_no_debug_ranges,
requires_subprocess)
from test.support.os_helper import TESTFN, unlink
from test.support.script_helper import assert_python_ok, assert_python_failure
from test.support.import_helper import forget

import json
import textwrap
Expand Down Expand Up @@ -2985,6 +2991,122 @@ def __getattribute__(self, attr):
self.assertIn("Did you mean", actual)
self.assertIn("bluch", actual)

def make_module(self, code):
tmpdir = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmpdir)

sys.path.append(str(tmpdir))
self.addCleanup(sys.path.pop)

mod_name = ''.join(random.choices(string.ascii_letters, k=16))
module = tmpdir / (mod_name + ".py")
module.write_text(code)

return mod_name

def get_import_from_suggestion(self, mod_dict, name):
modname = self.make_module(mod_dict)

def callable():
try:
exec(f"from {modname} import {name}")
except ImportError as e:
raise e from None
except Exception as e:
self.fail(f"Expected ImportError but got {type(e)}")
self.addCleanup(forget, modname)

result_lines = self.get_exception(
callable, slice_start=-1, slice_end=None
)
return result_lines[0]

def test_import_from_suggestions(self):
substitution = textwrap.dedent("""\
noise = more_noise = a = bc = None
blech = None
""")

elimination = textwrap.dedent("""
noise = more_noise = a = bc = None
blch = None
""")

addition = textwrap.dedent("""
noise = more_noise = a = bc = None
bluchin = None
""")

substitutionOverElimination = textwrap.dedent("""
blach = None
bluc = None
""")

substitutionOverAddition = textwrap.dedent("""
blach = None
bluchi = None
""")

eliminationOverAddition = textwrap.dedent("""
blucha = None
bluc = None
""")

caseChangeOverSubstitution = textwrap.dedent("""
Luch = None
fluch = None
BLuch = None
""")

for code, suggestion in [
(addition, "'bluchin'?"),
(substitution, "'blech'?"),
(elimination, "'blch'?"),
(addition, "'bluchin'?"),
(substitutionOverElimination, "'blach'?"),
(substitutionOverAddition, "'blach'?"),
(eliminationOverAddition, "'bluc'?"),
(caseChangeOverSubstitution, "'BLuch'?"),
]:
actual = self.get_import_from_suggestion(code, 'bluch')
self.assertIn(suggestion, actual)

def test_import_from_suggestions_do_not_trigger_for_long_attributes(self):
code = "blech = None"

actual = self.get_suggestion(code, 'somethingverywrong')
self.assertNotIn("blech", actual)

def test_import_from_error_bad_suggestions_do_not_trigger_for_small_names(self):
code = "vvv = mom = w = id = pytho = None"

for name in ("b", "v", "m", "py"):
with self.subTest(name=name):
actual = self.get_import_from_suggestion(code, name)
self.assertNotIn("you mean", actual)
self.assertNotIn("vvv", actual)
self.assertNotIn("mom", actual)
self.assertNotIn("'id'", actual)
self.assertNotIn("'w'", actual)
self.assertNotIn("'pytho'", actual)

def test_import_from_suggestions_do_not_trigger_for_big_namespaces(self):
# A module with lots of names will not be considered for suggestions.
chunks = [f"index_{index} = " for index in range(200)]
chunks.append(" None")
code = " ".join(chunks)
actual = self.get_import_from_suggestion(code, 'bluch')
self.assertNotIn("blech", actual)

def test_import_from_error_with_bad_name(self):
def raise_attribute_error_with_bad_name():
raise ImportError(name=12, obj=23, name_from=11)

result_lines = self.get_exception(
raise_attribute_error_with_bad_name, slice_start=-1, slice_end=None
)
self.assertNotIn("?", result_lines[-1])

def test_name_error_suggestions(self):
def Substitution():
noise = more_noise = a = bc = None
Expand Down
18 changes: 15 additions & 3 deletions 18 Lib/traceback.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -707,9 +707,16 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
self.offset = exc_value.offset
self.end_offset = exc_value.end_offset
self.msg = exc_value.msg
elif exc_type and issubclass(exc_type, ImportError) and \
getattr(exc_value, "name_from", None) is not None:
wrong_name = getattr(exc_value, "name_from", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
self._str += f". Did you mean: '{suggestion}'?"
elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \
getattr(exc_value, "name", None) is not None:
suggestion = _compute_suggestion_error(exc_value, exc_traceback)
wrong_name = getattr(exc_value, "name", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
self._str += f". Did you mean: '{suggestion}'?"
if issubclass(exc_type, NameError):
Expand Down Expand Up @@ -1005,8 +1012,7 @@ def _substitution_cost(ch_a, ch_b):
return _MOVE_COST


def _compute_suggestion_error(exc_value, tb):
wrong_name = getattr(exc_value, "name", None)
def _compute_suggestion_error(exc_value, tb, wrong_name):
if wrong_name is None or not isinstance(wrong_name, str):
return None
if isinstance(exc_value, AttributeError):
Expand All @@ -1015,6 +1021,12 @@ def _compute_suggestion_error(exc_value, tb):
d = dir(obj)
except Exception:
return None
elif isinstance(exc_value, ImportError):
try:
mod = __import__(exc_value.name)
d = dir(mod)
except Exception:
return None
else:
assert isinstance(exc_value, NameError)
# find most recent frame
Expand Down
3 changes: 3 additions & 0 deletions 3 Misc/NEWS.d/next/Core and Builtins/2022-10-15-22-25-20.gh-issue-91058.Uo2kW-.rst
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:exc:`ImportError` raised from failed ``from <module> import <name>`` now
include suggestions for the value of ``<name>`` based on the available names
in ``<module>``. Patch by Pablo Galindo
20 changes: 16 additions & 4 deletions 20 Objects/exceptions.c
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1464,20 +1464,21 @@ SimpleExtendsException(PyExc_BaseException, KeyboardInterrupt,
static int
ImportError_init(PyImportErrorObject *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"name", "path", 0};
static char *kwlist[] = {"name", "path", "name_from", 0};
PyObject *empty_tuple;
PyObject *msg = NULL;
PyObject *name = NULL;
PyObject *path = NULL;
PyObject *name_from = NULL;

if (BaseException_init((PyBaseExceptionObject *)self, args, NULL) == -1)
return -1;

empty_tuple = PyTuple_New(0);
if (!empty_tuple)
return -1;
if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$OO:ImportError", kwlist,
&name, &path)) {
if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$OOO:ImportError", kwlist,
&name, &path, &name_from)) {
Py_DECREF(empty_tuple);
return -1;
}
Expand All @@ -1489,6 +1490,9 @@ ImportError_init(PyImportErrorObject *self, PyObject *args, PyObject *kwds)
Py_XINCREF(path);
Py_XSETREF(self->path, path);

Py_XINCREF(name_from);
Py_XSETREF(self->name_from, name_from);

if (PyTuple_GET_SIZE(args) == 1) {
msg = PyTuple_GET_ITEM(args, 0);
Py_INCREF(msg);
Expand All @@ -1504,6 +1508,7 @@ ImportError_clear(PyImportErrorObject *self)
Py_CLEAR(self->msg);
Py_CLEAR(self->name);
Py_CLEAR(self->path);
Py_CLEAR(self->name_from);
return BaseException_clear((PyBaseExceptionObject *)self);
}

Expand All @@ -1521,6 +1526,7 @@ ImportError_traverse(PyImportErrorObject *self, visitproc visit, void *arg)
Py_VISIT(self->msg);
Py_VISIT(self->name);
Py_VISIT(self->path);
Py_VISIT(self->name_from);
return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg);
}

Expand All @@ -1540,7 +1546,7 @@ static PyObject *
ImportError_getstate(PyImportErrorObject *self)
{
PyObject *dict = ((PyBaseExceptionObject *)self)->dict;
if (self->name || self->path) {
if (self->name || self->path || self->name_from) {
dict = dict ? PyDict_Copy(dict) : PyDict_New();
if (dict == NULL)
return NULL;
Expand All @@ -1552,6 +1558,10 @@ ImportError_getstate(PyImportErrorObject *self)
Py_DECREF(dict);
return NULL;
}
if (self->name_from && PyDict_SetItem(dict, &_Py_ID(name_from), self->name_from) < 0) {
Py_DECREF(dict);
return NULL;
}
return dict;
}
else if (dict) {
Expand Down Expand Up @@ -1588,6 +1598,8 @@ static PyMemberDef ImportError_members[] = {
PyDoc_STR("module name")},
{"path", T_OBJECT, offsetof(PyImportErrorObject, path), 0,
PyDoc_STR("module path")},
{"name_from", T_OBJECT, offsetof(PyImportErrorObject, name_from), 0,
PyDoc_STR("name imported from module")},
{NULL} /* Sentinel */
};

Expand Down
4 changes: 2 additions & 2 deletions 4 Python/ceval.c
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -6931,7 +6931,7 @@ import_from(PyThreadState *tstate, PyObject *v, PyObject *name)
name, pkgname_or_unknown
);
/* NULL checks for errmsg and pkgname done by PyErr_SetImportError. */
PyErr_SetImportError(errmsg, pkgname, NULL);
_PyErr_SetImportErrorWithNameFrom(errmsg, pkgname, NULL, name);
}
else {
PyObject *spec = PyObject_GetAttr(v, &_Py_ID(__spec__));
Expand All @@ -6944,7 +6944,7 @@ import_from(PyThreadState *tstate, PyObject *v, PyObject *name)

errmsg = PyUnicode_FromFormat(fmt, name, pkgname_or_unknown, pkgpath);
/* NULL checks for errmsg and pkgname done by PyErr_SetImportError. */
PyErr_SetImportError(errmsg, pkgname, pkgpath);
_PyErr_SetImportErrorWithNameFrom(errmsg, pkgname, pkgpath, name);
}

Py_XDECREF(errmsg);
Expand Down
Loading
Oops, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.