Skip to content

Commit e2816cd

Browse files
authored
Check for contextlib.suppress in node_ignores_exception (#7327)
1 parent 5746f0d commit e2816cd

File tree

4 files changed

+91
-5
lines changed

4 files changed

+91
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
``import-error`` now correctly checks for ``contextlib.suppress`` guards on import statements.
2+
3+
Closes #7270

pylint/checkers/utils.py

+58-5
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,7 @@ def _except_handlers_ignores_exceptions(
10451045

10461046

10471047
def get_exception_handlers(
1048-
node: nodes.NodeNG, exception: type[Exception] = Exception
1048+
node: nodes.NodeNG, exception: type[Exception] | str = Exception
10491049
) -> list[nodes.ExceptHandler] | None:
10501050
"""Return the collections of handlers handling the exception in arguments.
10511051
@@ -1064,6 +1064,59 @@ def get_exception_handlers(
10641064
return []
10651065

10661066

1067+
def get_contextlib_with_statements(node: nodes.NodeNG) -> Iterator[nodes.With]:
1068+
"""Get all contextlib.with statements in the ancestors of the given node."""
1069+
for with_node in node.node_ancestors():
1070+
if isinstance(with_node, nodes.With):
1071+
yield with_node
1072+
1073+
1074+
def _suppresses_exception(
1075+
call: nodes.Call, exception: type[Exception] | str = Exception
1076+
) -> bool:
1077+
"""Check if the given node suppresses the given exception."""
1078+
if not isinstance(exception, str):
1079+
exception = exception.__name__
1080+
for arg in call.args:
1081+
inferred = safe_infer(arg)
1082+
if isinstance(inferred, nodes.ClassDef):
1083+
if inferred.name == exception:
1084+
return True
1085+
elif isinstance(inferred, nodes.Tuple):
1086+
for elt in inferred.elts:
1087+
inferred_elt = safe_infer(elt)
1088+
if (
1089+
isinstance(inferred_elt, nodes.ClassDef)
1090+
and inferred_elt.name == exception
1091+
):
1092+
return True
1093+
return False
1094+
1095+
1096+
def get_contextlib_suppressors(
1097+
node: nodes.NodeNG, exception: type[Exception] | str = Exception
1098+
) -> Iterator[nodes.With]:
1099+
"""Return the contextlib suppressors handling the exception.
1100+
1101+
Args:
1102+
node (nodes.NodeNG): A node that is potentially wrapped in a contextlib.suppress.
1103+
exception (builtin.Exception): exception or name of the exception.
1104+
1105+
Yields:
1106+
nodes.With: A with node that is suppressing the exception.
1107+
"""
1108+
for with_node in get_contextlib_with_statements(node):
1109+
for item, _ in with_node.items:
1110+
if isinstance(item, nodes.Call):
1111+
inferred = safe_infer(item.func)
1112+
if (
1113+
isinstance(inferred, nodes.ClassDef)
1114+
and inferred.qname() == "contextlib.suppress"
1115+
):
1116+
if _suppresses_exception(item, exception):
1117+
yield with_node
1118+
1119+
10671120
def is_node_inside_try_except(node: nodes.Raise) -> bool:
10681121
"""Check if the node is directly under a Try/Except statement
10691122
(but not under an ExceptHandler!).
@@ -1079,17 +1132,17 @@ def is_node_inside_try_except(node: nodes.Raise) -> bool:
10791132

10801133

10811134
def node_ignores_exception(
1082-
node: nodes.NodeNG, exception: type[Exception] = Exception
1135+
node: nodes.NodeNG, exception: type[Exception] | str = Exception
10831136
) -> bool:
10841137
"""Check if the node is in a TryExcept which handles the given exception.
10851138
10861139
If the exception is not given, the function is going to look for bare
10871140
excepts.
10881141
"""
10891142
managing_handlers = get_exception_handlers(node, exception)
1090-
if not managing_handlers:
1091-
return False
1092-
return any(managing_handlers)
1143+
if managing_handlers:
1144+
return True
1145+
return any(get_contextlib_suppressors(node, exception))
10931146

10941147

10951148
def class_is_abstract(node: nodes.ClassDef) -> bool:

tests/functional/i/import_error.py

+29
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,32 @@
7979

8080
import foo
8181
import bar
82+
83+
# Issues with contextlib.suppress reported in
84+
# https://github.com/PyCQA/pylint/issues/7270
85+
import contextlib
86+
with contextlib.suppress(ImportError):
87+
import foo2
88+
89+
with contextlib.suppress(ValueError):
90+
import foo2 # [import-error]
91+
92+
with contextlib.suppress(ImportError, ValueError):
93+
import foo2
94+
95+
with contextlib.suppress((ImportError, ValueError)):
96+
import foo2
97+
98+
with contextlib.suppress((ImportError,), (ValueError,)):
99+
import foo2
100+
101+
x = True
102+
with contextlib.suppress(ImportError):
103+
if x:
104+
import foo2
105+
else:
106+
pass
107+
108+
with contextlib.suppress(ImportError):
109+
with contextlib.suppress(TypeError):
110+
import foo2

tests/functional/i/import_error.txt

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ import-error:21:4:21:26::Unable to import 'maybe_missing_2':UNDEFINED
33
no-name-in-module:33:0:33:49::No name 'syntax_error' in module 'functional.s.syntax':UNDEFINED
44
syntax-error:33:0:None:None::Cannot import 'functional.s.syntax.syntax_error' due to 'invalid syntax (<unknown>, line 1)':HIGH
55
multiple-imports:78:0:78:15::Multiple imports on one line (foo, bar):UNDEFINED
6+
import-error:90:4:90:15::Unable to import 'foo2':UNDEFINED

0 commit comments

Comments
 (0)