Skip to content

Commit e07c443

Browse files
Filter out missing import problems for imports within a try-except clause with ImportError (#2332)
## Changes Filter out missing import problems for imports within a try-except clause with ImportError ### Linked issues Resolves #1705 ### Functionality None ### Tests - [x] added unit tests --------- Co-authored-by: Eric Vergnaud <[email protected]> Co-authored-by: Cor <[email protected]>
1 parent 08f90f2 commit e07c443

File tree

2 files changed

+58
-12
lines changed

2 files changed

+58
-12
lines changed

src/databricks/labs/ucx/source_code/notebooks/cells.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pathlib import Path
1111
from typing import TypeVar, cast
1212

13-
from astroid import Call, Const, ImportFrom, Name, NodeNG # type: ignore
13+
from astroid import Call, Const, ImportFrom, Name, NodeNG, Try # type: ignore
1414
from astroid.exceptions import AstroidSyntaxError # type: ignore
1515
from sqlglot import parse as parse_sql, ParseError as SQLParseError
1616

@@ -482,7 +482,7 @@ def _parse_and_extract_nodes(self) -> tuple[Tree, list[NodeBase], Iterable[Depen
482482
nodes = sorted(nodes, key=lambda node: (node.node.lineno, node.node.col_offset))
483483
return tree, nodes, problems
484484

485-
def _build_graph_from_node(self, base_node: NodeBase):
485+
def _build_graph_from_node(self, base_node: NodeBase) -> Iterable[DependencyProblem]:
486486
if isinstance(base_node, SysPathChange):
487487
yield from self._mutate_path_lookup(base_node)
488488
elif isinstance(base_node, NotebookRunCall):
@@ -509,14 +509,42 @@ def _build_inherited_context_from_node(self, base_node: NodeBase, child_path: Pa
509509
logger.warning(f"Can't build inherited context for node {NodeBase.__name__} of type {type(base_node).__name__}")
510510
return InheritedContext(None, False)
511511

512-
def _register_import(self, base_node: ImportSource):
512+
def _register_import(self, base_node: ImportSource) -> Iterable[DependencyProblem]:
513513
prefix = ""
514514
if isinstance(base_node.node, ImportFrom) and base_node.node.level is not None:
515515
prefix = "." * base_node.node.level
516516
name = base_node.name or ""
517-
yield from self._context.parent.register_import(prefix + name)
517+
problems = self._context.parent.register_import(prefix + name)
518+
for problem in problems:
519+
prob = self._filter_import_problem_in_try_except(problem, base_node)
520+
if prob is not None:
521+
yield prob
518522

519-
def _register_notebook(self, base_node: NotebookRunCall):
523+
@classmethod
524+
def _filter_import_problem_in_try_except(
525+
cls, problem: DependencyProblem, base_node: ImportSource
526+
) -> DependencyProblem | None:
527+
if problem.code != 'import-not-found':
528+
return problem
529+
# is base_node in a try-except clause ?
530+
node = base_node.node.parent
531+
while node and not isinstance(node, Try):
532+
node = node.parent
533+
if cls._is_try_except_import_error(node):
534+
return None
535+
return problem
536+
537+
@classmethod
538+
def _is_try_except_import_error(cls, node: Try | None) -> bool:
539+
if not isinstance(node, Try):
540+
return False
541+
for handler in node.handlers:
542+
if isinstance(handler.type, Name):
543+
if handler.type.name == "ImportError":
544+
return True
545+
return False
546+
547+
def _register_notebook(self, base_node: NotebookRunCall) -> Iterable[DependencyProblem]:
520548
has_unresolved, paths = base_node.get_notebook_paths(self._context.session_state)
521549
if has_unresolved:
522550
yield DependencyProblem(
@@ -527,7 +555,7 @@ def _register_notebook(self, base_node: NotebookRunCall):
527555
# notebooks ran via dbutils.notebook.run do not inherit or propagate context
528556
yield from self._context.parent.register_notebook(Path(path), False)
529557

530-
def _mutate_path_lookup(self, change: SysPathChange):
558+
def _mutate_path_lookup(self, change: SysPathChange) -> Iterable[DependencyProblem]:
531559
if isinstance(change, UnresolvedPath):
532560
yield DependencyProblem(
533561
'sys-path-cannot-compute-value',

tests/unit/source_code/linters/test_python_imports.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
4+
35
import pytest
46

57
from databricks.labs.ucx.source_code.base import CurrentSessionState
6-
from databricks.labs.ucx.source_code.graph import DependencyProblem
8+
from databricks.labs.ucx.source_code.graph import Dependency, DependencyGraph, DependencyProblem
9+
from databricks.labs.ucx.source_code.linters.files import FileLoader
710

811
from databricks.labs.ucx.source_code.linters.imports import DbutilsLinter, ImportSource, SysPathChange
912
from databricks.labs.ucx.source_code.linters.python_ast import Tree
13+
from databricks.labs.ucx.source_code.notebooks.cells import PythonCodeAnalyzer
1014

1115

1216
def test_linter_returns_empty_list_of_dbutils_notebook_run_calls():
@@ -27,27 +31,27 @@ def test_linter_returns_list_of_dbutils_notebook_run_calls():
2731

2832
def test_linter_returns_empty_list_of_imports():
2933
tree = Tree.parse('')
30-
assert not ImportSource.extract_from_tree(tree, DependencyProblem)[0]
34+
assert not ImportSource.extract_from_tree(tree, DependencyProblem.from_node)[0]
3135

3236

3337
def test_linter_returns_import():
3438
tree = Tree.parse('import x')
35-
assert ["x"] == [node.name for node in ImportSource.extract_from_tree(tree, DependencyProblem)[0]]
39+
assert ["x"] == [node.name for node in ImportSource.extract_from_tree(tree, DependencyProblem.from_node)[0]]
3640

3741

3842
def test_linter_returns_import_from():
3943
tree = Tree.parse('from x import z')
40-
assert ["x"] == [node.name for node in ImportSource.extract_from_tree(tree, DependencyProblem)[0]]
44+
assert ["x"] == [node.name for node in ImportSource.extract_from_tree(tree, DependencyProblem.from_node)[0]]
4145

4246

4347
def test_linter_returns_import_module():
4448
tree = Tree.parse('importlib.import_module("x")')
45-
assert ["x"] == [node.name for node in ImportSource.extract_from_tree(tree, DependencyProblem)[0]]
49+
assert ["x"] == [node.name for node in ImportSource.extract_from_tree(tree, DependencyProblem.from_node)[0]]
4650

4751

4852
def test_linter_returns__import__():
4953
tree = Tree.parse('importlib.__import__("x")')
50-
assert ["x"] == [node.name for node in ImportSource.extract_from_tree(tree, DependencyProblem)[0]]
54+
assert ["x"] == [node.name for node in ImportSource.extract_from_tree(tree, DependencyProblem.from_node)[0]]
5155

5256

5357
def test_linter_returns_appended_absolute_paths():
@@ -191,3 +195,17 @@ def test_infers_dbutils_notebook_run_dynamic_value(code, expected) -> None:
191195
_, paths = call.get_notebook_paths(CurrentSessionState())
192196
all_paths.extend(paths)
193197
assert all_paths == expected
198+
199+
200+
def test_failing_import_in_try_except_is_silent(simple_dependency_resolver, mock_path_lookup) -> None:
201+
code = """
202+
try:
203+
import missing_library
204+
except ImportError as e:
205+
pass
206+
"""
207+
dep = Dependency(FileLoader(), Path(""))
208+
graph = DependencyGraph(dep, None, simple_dependency_resolver, mock_path_lookup, CurrentSessionState())
209+
analyzer = PythonCodeAnalyzer(graph.new_dependency_graph_context(), code)
210+
problems = analyzer.build_graph()
211+
assert not problems

0 commit comments

Comments
 (0)