From 4507d2a6abbf168c2cbed7681fdf816ee8abc1d1 Mon Sep 17 00:00:00 2001 From: sandbubbles <160503471+sandbubbles@users.noreply.github.com> Date: Fri, 3 Jan 2025 16:44:14 +0100 Subject: [PATCH] fix[ux]: improve error message on failed imports (#4409) Previously, when an import failed, the error message only displayed the paths that were attempted, but did not point to the specific import statement which caused the exception. To address this, we wrap the main node-handling loop in `ImportAnalyzer` with `tag_exception`, which propagates information with the specific line and file to the error message. --- tests/functional/syntax/test_import.py | 9 ++++++--- tests/functional/syntax/test_interfaces.py | 15 ++++++++++----- vyper/semantics/analysis/imports.py | 10 ++++++---- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/tests/functional/syntax/test_import.py b/tests/functional/syntax/test_import.py index 07b1a336c3..acc556206e 100644 --- a/tests/functional/syntax/test_import.py +++ b/tests/functional/syntax/test_import.py @@ -28,8 +28,9 @@ def foo(): ) file_input = input_bundle.load_file("top.vy") - with pytest.raises(ModuleNotFound): + with pytest.raises(ModuleNotFound) as e: compiler.compile_from_file_input(file_input, input_bundle=input_bundle) + assert "lib0.vy:" in str(e.value) def test_implicitly_relative_import_crashes_2(make_input_bundle): @@ -44,8 +45,9 @@ def foo(): ) file_input = input_bundle.load_file("top.vy") - with pytest.raises(ModuleNotFound): + with pytest.raises(ModuleNotFound) as e: compiler.compile_from_file_input(file_input, input_bundle=input_bundle) + assert "lib0.vy:" in str(e.value) def test_relative_import_searches_only_current_path(make_input_bundle): @@ -70,8 +72,9 @@ def foo(): input_bundle = make_input_bundle({"top.vy": top, "a.vy": a, "subdir/b.vy": b}) file_input = input_bundle.load_file("top.vy") - with pytest.raises(ModuleNotFound): + with pytest.raises(ModuleNotFound) as e: compiler.compile_from_file_input(file_input, input_bundle=input_bundle) + assert "b.vy:" in str(e.value) def test_absolute_import_within_relative_import(make_input_bundle): diff --git a/tests/functional/syntax/test_interfaces.py b/tests/functional/syntax/test_interfaces.py index baf0c73c30..fcfa5ba0c9 100644 --- a/tests/functional/syntax/test_interfaces.py +++ b/tests/functional/syntax/test_interfaces.py @@ -411,26 +411,31 @@ def foobar(): assert compiler.compile_code(code, input_bundle=input_bundle) is not None -def test_builtins_not_found(): +def test_builtins_not_found(make_input_bundle): code = """ from vyper.interfaces import foobar """ + input_bundle = make_input_bundle({"code.vy": code}) + file_input = input_bundle.load_file("code.vy") with pytest.raises(ModuleNotFound) as e: - compiler.compile_code(code) - + compiler.compile_from_file_input(file_input, input_bundle=input_bundle) assert e.value._message == "vyper.interfaces.foobar" assert e.value._hint == "try renaming `vyper.interfaces` to `ethereum.ercs`" + assert "code.vy:" in str(e.value) @pytest.mark.parametrize("erc", ("ERC20", "ERC721", "ERC4626")) -def test_builtins_not_found2(erc): +def test_builtins_not_found2(erc, make_input_bundle): code = f""" from ethereum.ercs import {erc} """ + input_bundle = make_input_bundle({"code.vy": code}) + file_input = input_bundle.load_file("code.vy") with pytest.raises(ModuleNotFound) as e: - compiler.compile_code(code) + compiler.compile_from_file_input(file_input, input_bundle=input_bundle) assert e.value._message == f"ethereum.ercs.{erc}" assert e.value._hint == f"try renaming `{erc}` to `I{erc}`" + assert "code.vy:" in str(e.value) def test_interface_body_check(make_input_bundle): diff --git a/vyper/semantics/analysis/imports.py b/vyper/semantics/analysis/imports.py index bcd62feb07..148205f5f8 100644 --- a/vyper/semantics/analysis/imports.py +++ b/vyper/semantics/analysis/imports.py @@ -19,6 +19,7 @@ ImportCycle, ModuleNotFound, StructureException, + tag_exceptions, ) from vyper.semantics.analysis.base import ImportInfo from vyper.utils import safe_relpath, sha256sum @@ -106,10 +107,11 @@ def _resolve_imports_r(self, module_ast: vy_ast.Module): return with self.graph.enter_path(module_ast): for node in module_ast.body: - if isinstance(node, vy_ast.Import): - self._handle_Import(node) - elif isinstance(node, vy_ast.ImportFrom): - self._handle_ImportFrom(node) + with tag_exceptions(node): + if isinstance(node, vy_ast.Import): + self._handle_Import(node) + elif isinstance(node, vy_ast.ImportFrom): + self._handle_ImportFrom(node) self.seen.add(id(module_ast)) def _handle_Import(self, node: vy_ast.Import):