Skip to content

Commit bf493bb

Browse files
authored
Ignore import errors if in guarded import block (#4702)
* Ignore import errors if in guarded import block * Use new astroid helper methods
1 parent 6a16625 commit bf493bb

File tree

7 files changed

+77
-25
lines changed

7 files changed

+77
-25
lines changed

ChangeLog

+6
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ Release date: TBA
6262

6363
Closes #4698
6464

65+
* Don't emit ``import-error``, ``no-name-in-module``, and ``ungrouped-imports``
66+
for imports guarded by ``sys.version_info`` or ``typing.TYPE_CHECKING``.
67+
68+
Closes #3285
69+
Closes #3382
70+
6571
* Fix ``invalid-overridden-method`` with nested property
6672

6773
Closes #4368

pylint/checkers/imports.py

+16-18
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
import os
5050
import sys
5151
from distutils import sysconfig
52-
from typing import Dict, List, Union
52+
from typing import Dict, List, Set, Union
5353

5454
import astroid
5555

@@ -58,6 +58,7 @@
5858
check_messages,
5959
get_import_name,
6060
is_from_fallback_block,
61+
is_node_in_guarded_import_block,
6162
node_ignores_exception,
6263
)
6364
from pylint.exceptions import EmptyReportError
@@ -119,18 +120,10 @@ def _ignore_import_failure(node, modname, ignored_modules):
119120
if submodule in ignored_modules:
120121
return True
121122

122-
# ignore import failure if guarded by `sys.version_info` test
123-
if isinstance(node.parent, astroid.If) and isinstance(
124-
node.parent.test, astroid.Compare
125-
):
126-
value = node.parent.test.left
127-
if isinstance(value, astroid.Subscript):
128-
value = value.value
129-
if (
130-
isinstance(value, astroid.Attribute)
131-
and value.as_string() == "sys.version_info"
132-
):
133-
return True
123+
if is_node_in_guarded_import_block(node):
124+
# Ignore import failure if part of guarded import block
125+
# I.e. `sys.version_info` or `typing.TYPE_CHECKING`
126+
return True
134127

135128
return node_ignores_exception(node, ImportError)
136129

@@ -556,25 +549,30 @@ def visit_importfrom(self, node):
556549
self._add_imported_module(node, imported_module.name)
557550

558551
@check_messages(*MSGS)
559-
def leave_module(self, node):
552+
def leave_module(self, node: astroid.Module) -> None:
560553
# Check imports are grouped by category (standard, 3rd party, local)
561554
std_imports, ext_imports, loc_imports = self._check_imports_order(node)
562555

563556
# Check that imports are grouped by package within a given category
564-
met_import = set() # set for 'import x' style
565-
met_from = set() # set for 'from x import y' style
557+
met_import: Set[str] = set() # set for 'import x' style
558+
met_from: Set[str] = set() # set for 'from x import y' style
566559
current_package = None
567560
for import_node, import_name in std_imports + ext_imports + loc_imports:
568561
if not self.linter.is_message_enabled(
569562
"ungrouped-imports", import_node.fromlineno
570563
):
571564
continue
572-
if isinstance(import_node, astroid.node_classes.ImportFrom):
565+
if isinstance(import_node, astroid.ImportFrom):
573566
met = met_from
574567
else:
575568
met = met_import
576569
package, _, _ = import_name.partition(".")
577-
if current_package and current_package != package and package in met:
570+
if (
571+
current_package
572+
and current_package != package
573+
and package in met
574+
and is_node_in_guarded_import_block(import_node) is False
575+
):
578576
self.add_message("ungrouped-imports", node=import_node, args=package)
579577
current_package = package
580578
met.add(package)

pylint/checkers/utils.py

+9
Original file line numberDiff line numberDiff line change
@@ -1552,3 +1552,12 @@ def get_import_name(
15521552
modname, level=importnode.level
15531553
)
15541554
return modname
1555+
1556+
1557+
def is_node_in_guarded_import_block(node: astroid.NodeNG) -> bool:
1558+
"""Return True if node is part for guarded if block.
1559+
I.e. `sys.version_info` or `typing.TYPE_CHECKING`
1560+
"""
1561+
return isinstance(node.parent, astroid.If) and (
1562+
node.parent.is_sys_guard() or node.parent.is_typing_guard()
1563+
)

pylint/checkers/variables.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -1173,12 +1173,16 @@ def visit_name(self, node):
11731173
self.add_message("undefined-variable", args=name, node=node)
11741174

11751175
@utils.check_messages("no-name-in-module")
1176-
def visit_import(self, node):
1176+
def visit_import(self, node: astroid.Import) -> None:
11771177
"""check modules attribute accesses"""
11781178
if not self._analyse_fallback_blocks and utils.is_from_fallback_block(node):
11791179
# No need to verify this, since ImportError is already
11801180
# handled by the client code.
11811181
return
1182+
if utils.is_node_in_guarded_import_block(node) is True:
1183+
# Don't verify import if part of guarded import block
1184+
# I.e. `sys.version_info` or `typing.TYPE_CHECKING`
1185+
return
11821186

11831187
for name, _ in node.names:
11841188
parts = name.split(".")
@@ -1191,12 +1195,16 @@ def visit_import(self, node):
11911195
self._check_module_attrs(node, module, parts[1:])
11921196

11931197
@utils.check_messages("no-name-in-module")
1194-
def visit_importfrom(self, node):
1198+
def visit_importfrom(self, node: astroid.ImportFrom) -> None:
11951199
"""check modules attribute accesses"""
11961200
if not self._analyse_fallback_blocks and utils.is_from_fallback_block(node):
11971201
# No need to verify this, since ImportError is already
11981202
# handled by the client code.
11991203
return
1204+
if utils.is_node_in_guarded_import_block(node) is True:
1205+
# Don't verify import if part of guarded import block
1206+
# I.e. `sys.version_info` or `typing.TYPE_CHECKING`
1207+
return
12001208

12011209
name_parts = node.modname.split(".")
12021210
try:

tests/functional/i/import_error.py

+27-5
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,36 @@
2424
except ImportError:
2525
pass
2626

27-
# pylint: disable=no-name-in-module
28-
from functional.s.syntax_error import toto # [syntax-error]
2927

30-
# Don't emit import-error if guarded behind `sys.version_info`
28+
from functional.s.syntax_error import toto # [no-name-in-module,syntax-error]
29+
30+
31+
# Don't emit `import-error` or `no-name-in-module`
32+
# if guarded behind `sys.version_info` or `typing.TYPE_CHECKING`
3133
import sys
34+
import typing
35+
import typing as tp # pylint: disable=reimported
36+
from typing import TYPE_CHECKING
37+
3238

3339
if sys.version_info >= (3, 9):
34-
import zoneinfo
40+
import some_module
41+
from some_module import some_class
42+
else:
43+
import some_module_alt
3544

3645
if sys.version_info[:2] >= (3, 9):
37-
import zoneinfo
46+
import some_module
47+
else:
48+
import some_module_alt
49+
50+
51+
if typing.TYPE_CHECKING:
52+
import stub_import
53+
54+
if tp.TYPE_CHECKING:
55+
import stub_import
56+
57+
if TYPE_CHECKING:
58+
import stub_import
59+
from stub_import import stub_class

tests/functional/i/import_error.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import-error:3:0::Unable to import 'totally_missing'
22
import-error:16:4::Unable to import 'maybe_missing_2'
3+
no-name-in-module:28:0::No name 'syntax_error' in module 'functional.s'
34
syntax-error:28:0::Cannot import 'functional.s.syntax_error' due to syntax error 'invalid syntax (<unknown>, line 1)'

tests/functional/u/ungrouped_imports.py

+8
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@
2424
import unittest
2525
from unittest import TestCase
2626
from unittest.mock import MagicMock
27+
28+
29+
# https://github.com/PyCQA/pylint/issues/3382
30+
# Imports in a `if TYPE_CHECKING` block should not trigger `ungrouped-imports`
31+
from typing import TYPE_CHECKING
32+
if TYPE_CHECKING:
33+
import re
34+
from typing import List

0 commit comments

Comments
 (0)