diff --git a/doc/whatsnew/fragments/10589.bugfix b/doc/whatsnew/fragments/10589.bugfix new file mode 100644 index 0000000000..2dd9339fe3 --- /dev/null +++ b/doc/whatsnew/fragments/10589.bugfix @@ -0,0 +1,3 @@ +Allow ``wrong-import-position`` pragma on non-import lines to suppress following imports until the next non-import statement. + +Closes #10589 diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index cdda5d9665..578a7a4640 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -446,7 +446,7 @@ def __init__(self, linter: PyLinter) -> None: BaseChecker.__init__(self, linter) self.import_graph: defaultdict[str, set[str]] = defaultdict(set) self._imports_stack: list[tuple[ImportNode, str]] = [] - self._first_non_import_node = None + self._non_import_nodes: list[nodes.NodeNG] = [] self._module_pkg: dict[Any, Any] = ( {} ) # mapping of modules to the pkg they belong in @@ -607,7 +607,7 @@ def leave_module(self, node: nodes.Module) -> None: met.add(package) self._imports_stack = [] - self._first_non_import_node = None + self._non_import_nodes = [] def compute_first_non_import_node( self, @@ -621,12 +621,7 @@ def compute_first_non_import_node( | nodes.Try ), ) -> None: - # if the node does not contain an import instruction, and if it is the - # first node of the module, keep a track of it (all the import positions - # of the module will be compared to the position of this first - # instruction) - if self._first_non_import_node: - return + # Track non-import nodes at module level to check import positions if not isinstance(node.parent, nodes.Module): return if isinstance(node, nodes.Try) and any( @@ -644,7 +639,8 @@ def compute_first_non_import_node( ] if all(valid_targets): return - self._first_non_import_node = node + + self._non_import_nodes.append(node) visit_try = visit_assignattr = visit_assign = visit_ifexp = visit_comprehension = ( visit_expr @@ -653,12 +649,7 @@ def compute_first_non_import_node( def visit_functiondef( self, node: nodes.FunctionDef | nodes.While | nodes.For | nodes.ClassDef ) -> None: - # If it is the first non import instruction of the module, record it. - if self._first_non_import_node: - return - - # Check if the node belongs to an `If` or a `Try` block. If they - # contain imports, skip recording this node. + # Record non-import instruction unless inside an If/Try block that contains imports if not isinstance(node.parent.scope(), nodes.Module): return @@ -670,7 +661,7 @@ def visit_functiondef( if any(root.nodes_of_class((nodes.Import, nodes.ImportFrom))): return - self._first_non_import_node = node + self._non_import_nodes.append(node) visit_classdef = visit_for = visit_while = visit_functiondef @@ -699,19 +690,39 @@ def _check_position(self, node: ImportNode) -> None: Send a message if `node` comes before another instruction """ - # if a first non-import instruction has already been encountered, - # it means the import comes after it and therefore is not well placed - if self._first_non_import_node: - if self.linter.is_message_enabled( - "wrong-import-position", self._first_non_import_node.fromlineno + # Check if import comes after a non-import statement + if self._non_import_nodes: + # Check for inline pragma on the import line + if not self.linter.is_message_enabled( + "wrong-import-position", node.fromlineno ): - self.add_message( - "wrong-import-position", node=node, args=node.as_string() - ) - else: self.linter.add_ignored_message( "wrong-import-position", node.fromlineno, node ) + return + + # Check for pragma on the preceding non-import statement + most_recent_non_import = None + for non_import_node in self._non_import_nodes: + if non_import_node.fromlineno < node.fromlineno: + most_recent_non_import = non_import_node + else: + break + + if most_recent_non_import: + check_line = most_recent_non_import.fromlineno + if not self.linter.is_message_enabled( + "wrong-import-position", check_line + ): + self.linter.add_ignored_message( + "wrong-import-position", check_line, most_recent_non_import + ) + self.linter.add_ignored_message( + "wrong-import-position", node.fromlineno, node + ) + return + + self.add_message("wrong-import-position", node=node, args=node.as_string()) def _record_import( self, diff --git a/tests/functional/d/disable_wrong_import_position.py b/tests/functional/d/disable_wrong_import_position.py index 0703325a9e..69de82aeaa 100644 --- a/tests/functional/d/disable_wrong_import_position.py +++ b/tests/functional/d/disable_wrong_import_position.py @@ -1,7 +1,11 @@ -"""Checks that disabling 'wrong-import-position' on a statement prevents it from -invalidating subsequent imports.""" +"""Test wrong-import-position pragma on non-import statement.""" # pylint: disable=unused-import -CONSTANT = True # pylint: disable=wrong-import-position - +import os import sys + +CONSTANT_A = False # pylint: disable=wrong-import-position +import time + +CONSTANT_B = True +import logging # [wrong-import-position] diff --git a/tests/functional/d/disable_wrong_import_position.txt b/tests/functional/d/disable_wrong_import_position.txt new file mode 100644 index 0000000000..13f49c5b2c --- /dev/null +++ b/tests/functional/d/disable_wrong_import_position.txt @@ -0,0 +1 @@ +wrong-import-position:11:0:11:14::"Import ""import logging"" should be placed at the top of the module":UNDEFINED diff --git a/tests/functional/w/wrong_import_position_pragma_scope.py b/tests/functional/w/wrong_import_position_pragma_scope.py new file mode 100644 index 0000000000..111727bef4 --- /dev/null +++ b/tests/functional/w/wrong_import_position_pragma_scope.py @@ -0,0 +1,16 @@ +"""Test wrong-import-position pragma scoping.""" +# pylint: disable=unused-import + +import os +import sys + +# Pragma on non-import suppresses following imports until next non-import +CONSTANT_A = False # pylint: disable=wrong-import-position +import time + +CONSTANT_B = True +import logging # [wrong-import-position] + +# Inline pragma on import line +CONSTANT_C = 42 +import json # pylint: disable=wrong-import-position diff --git a/tests/functional/w/wrong_import_position_pragma_scope.txt b/tests/functional/w/wrong_import_position_pragma_scope.txt new file mode 100644 index 0000000000..70c1c60219 --- /dev/null +++ b/tests/functional/w/wrong_import_position_pragma_scope.txt @@ -0,0 +1 @@ +wrong-import-position:12:0:12:14::"Import ""import logging"" should be placed at the top of the module":UNDEFINED