From 8cace41877f9d52ef4d4ae3c8904591de484fe2b Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Thu, 25 Sep 2025 22:19:22 +0300 Subject: [PATCH 1/6] fix: scope wrong-import-position pragma to specific line only Signed-off-by: Emmanuel Ferdman --- pylint/checkers/imports.py | 2 +- .../d/disable_wrong_import_position.py | 9 +++++--- .../d/disable_wrong_import_position.txt | 1 + .../w/wrong_import_position_pragma_scope.py | 23 +++++++++++++++++++ .../w/wrong_import_position_pragma_scope.txt | 5 ++++ 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 tests/functional/d/disable_wrong_import_position.txt create mode 100644 tests/functional/w/wrong_import_position_pragma_scope.py create mode 100644 tests/functional/w/wrong_import_position_pragma_scope.txt diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index cdda5d9665..44e528044e 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -703,7 +703,7 @@ def _check_position(self, node: ImportNode) -> None: # 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 + "wrong-import-position", node.fromlineno ): self.add_message( "wrong-import-position", node=node, args=node.as_string() diff --git a/tests/functional/d/disable_wrong_import_position.py b/tests/functional/d/disable_wrong_import_position.py index 0703325a9e..07b81c0e91 100644 --- a/tests/functional/d/disable_wrong_import_position.py +++ b/tests/functional/d/disable_wrong_import_position.py @@ -1,7 +1,10 @@ -"""Checks that disabling 'wrong-import-position' on a statement prevents it from -invalidating subsequent imports.""" +"""Checks that disabling 'wrong-import-position' only affects the specific line. + +A pragma on a non-import statement should not affect subsequent import statements. +This demonstrates the correct behavior after fixing the bug. +""" # pylint: disable=unused-import CONSTANT = True # pylint: disable=wrong-import-position -import sys +import sys # [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..46209232b0 --- /dev/null +++ b/tests/functional/d/disable_wrong_import_position.txt @@ -0,0 +1 @@ +wrong-import-position:10:0:10:10::"Import ""import sys"" should be placed at the top of the module":UNDEFINED \ No newline at end of file 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..e7c7301e5d --- /dev/null +++ b/tests/functional/w/wrong_import_position_pragma_scope.py @@ -0,0 +1,23 @@ +"""Test that wrong-import-position pragma suppression is correctly scoped.""" +# pylint: disable=unused-import,invalid-name + +import logging +import sys + +# This pragma should not affect subsequent import statements +logger = logging.getLogger() # pylint: disable=wrong-import-position +logging.basicConfig(level='DEBUG') + +logger.debug('importing modules...') +import os # [wrong-import-position] +import pathlib # [wrong-import-position] +import random # [wrong-import-position] +logger.debug('done importing') + +# Test that pragma on import line works correctly (this import should not be flagged) +constant_var = "test" +import json # pylint: disable=wrong-import-position + +# Test that subsequent imports are not affected by the pragma above +import csv # [wrong-import-position] +import re # [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..7ab696e778 --- /dev/null +++ b/tests/functional/w/wrong_import_position_pragma_scope.txt @@ -0,0 +1,5 @@ +wrong-import-position:19:0:19:9::"Import ""import os"" should be placed at the top of the module":UNDEFINED +wrong-import-position:20:0:20:14::"Import ""import pathlib"" should be placed at the top of the module":UNDEFINED +wrong-import-position:21:0:21:13::"Import ""import random"" should be placed at the top of the module":UNDEFINED +wrong-import-position:29:0:29:10::"Import ""import csv"" should be placed at the top of the module":UNDEFINED +wrong-import-position:30:0:30:9::"Import ""import re"" should be placed at the top of the module":UNDEFINED \ No newline at end of file From a7e937a3631321f86b742ece740b6cd1479d80aa Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Thu, 25 Sep 2025 22:28:21 +0300 Subject: [PATCH 2/6] fix: scope wrong-import-position pragma to specific line only Signed-off-by: Emmanuel Ferdman --- doc/whatsnew/fragments/10589.bugfix | 3 +++ .../w/wrong_import_position_pragma_scope.txt | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 doc/whatsnew/fragments/10589.bugfix diff --git a/doc/whatsnew/fragments/10589.bugfix b/doc/whatsnew/fragments/10589.bugfix new file mode 100644 index 0000000000..65bb985d90 --- /dev/null +++ b/doc/whatsnew/fragments/10589.bugfix @@ -0,0 +1,3 @@ +Scope ``wrong-import-position`` pragma to specific line only. + +Closes #10589 diff --git a/tests/functional/w/wrong_import_position_pragma_scope.txt b/tests/functional/w/wrong_import_position_pragma_scope.txt index 7ab696e778..83393dd4d6 100644 --- a/tests/functional/w/wrong_import_position_pragma_scope.txt +++ b/tests/functional/w/wrong_import_position_pragma_scope.txt @@ -1,5 +1,5 @@ -wrong-import-position:19:0:19:9::"Import ""import os"" should be placed at the top of the module":UNDEFINED -wrong-import-position:20:0:20:14::"Import ""import pathlib"" should be placed at the top of the module":UNDEFINED -wrong-import-position:21:0:21:13::"Import ""import random"" should be placed at the top of the module":UNDEFINED -wrong-import-position:29:0:29:10::"Import ""import csv"" should be placed at the top of the module":UNDEFINED -wrong-import-position:30:0:30:9::"Import ""import re"" should be placed at the top of the module":UNDEFINED \ No newline at end of file +wrong-import-position:12:0:12:9::"Import ""import os"" should be placed at the top of the module":UNDEFINED +wrong-import-position:13:0:13:14::"Import ""import pathlib"" should be placed at the top of the module":UNDEFINED +wrong-import-position:14:0:14:13::"Import ""import random"" should be placed at the top of the module":UNDEFINED +wrong-import-position:22:0:22:10::"Import ""import csv"" should be placed at the top of the module":UNDEFINED +wrong-import-position:23:0:23:9::"Import ""import re"" should be placed at the top of the module":UNDEFINED From f957d35920cbe4cd4aa369247675703bb0dca5af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:30:41 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/checkers/imports.py | 4 +--- tests/functional/d/disable_wrong_import_position.txt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 44e528044e..c1c8f5ce5d 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -702,9 +702,7 @@ def _check_position(self, node: ImportNode) -> None: # 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", node.fromlineno - ): + if self.linter.is_message_enabled("wrong-import-position", node.fromlineno): self.add_message( "wrong-import-position", node=node, args=node.as_string() ) diff --git a/tests/functional/d/disable_wrong_import_position.txt b/tests/functional/d/disable_wrong_import_position.txt index 46209232b0..c56c0a200e 100644 --- a/tests/functional/d/disable_wrong_import_position.txt +++ b/tests/functional/d/disable_wrong_import_position.txt @@ -1 +1 @@ -wrong-import-position:10:0:10:10::"Import ""import sys"" should be placed at the top of the module":UNDEFINED \ No newline at end of file +wrong-import-position:10:0:10:10::"Import ""import sys"" should be placed at the top of the module":UNDEFINED From 1015d063ca4f03b4036b771483752237336bf78b Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Mon, 20 Oct 2025 01:54:15 +0300 Subject: [PATCH 4/6] Support block-level wrong-import-position pragma suppression Signed-off-by: Emmanuel Ferdman --- doc/whatsnew/fragments/10589.bugfix | 2 +- pylint/checkers/imports.py | 61 ++++++++++++------- .../d/disable_wrong_import_position.py | 15 ++--- .../d/disable_wrong_import_position.txt | 2 +- .../w/wrong_import_position_pragma_scope.py | 27 +++----- .../w/wrong_import_position_pragma_scope.txt | 6 +- 6 files changed, 61 insertions(+), 52 deletions(-) diff --git a/doc/whatsnew/fragments/10589.bugfix b/doc/whatsnew/fragments/10589.bugfix index 65bb985d90..2dd9339fe3 100644 --- a/doc/whatsnew/fragments/10589.bugfix +++ b/doc/whatsnew/fragments/10589.bugfix @@ -1,3 +1,3 @@ -Scope ``wrong-import-position`` pragma to specific line only. +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 c1c8f5ce5d..e28434c824 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -447,6 +447,7 @@ def __init__(self, linter: PyLinter) -> None: 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 = [] self._module_pkg: dict[Any, Any] = ( {} ) # mapping of modules to the pkg they belong in @@ -608,6 +609,7 @@ def leave_module(self, node: nodes.Module) -> None: self._imports_stack = [] self._first_non_import_node = None + self._non_import_nodes = [] def compute_first_non_import_node( self, @@ -621,12 +623,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 +641,11 @@ def compute_first_non_import_node( ] if all(valid_targets): return - self._first_non_import_node = node + + if not self._first_non_import_node: + 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 +654,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 +666,10 @@ def visit_functiondef( if any(root.nodes_of_class((nodes.Import, nodes.ImportFrom))): return - self._first_non_import_node = node + if not self._first_non_import_node: + self._first_non_import_node = node + + self._non_import_nodes.append(node) visit_classdef = visit_for = visit_while = visit_functiondef @@ -699,17 +698,37 @@ 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 + # Check if import comes after a non-import statement if self._first_non_import_node: - if self.linter.is_message_enabled("wrong-import-position", node.fromlineno): - self.add_message( - "wrong-import-position", node=node, args=node.as_string() - ) - else: + # Check for inline pragma on the import line + if not self.linter.is_message_enabled("wrong-import-position", node.fromlineno): 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 07b81c0e91..69de82aeaa 100644 --- a/tests/functional/d/disable_wrong_import_position.py +++ b/tests/functional/d/disable_wrong_import_position.py @@ -1,10 +1,11 @@ -"""Checks that disabling 'wrong-import-position' only affects the specific line. - -A pragma on a non-import statement should not affect subsequent import statements. -This demonstrates the correct behavior after fixing the bug. -""" +"""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 -import sys # [wrong-import-position] +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 index c56c0a200e..13f49c5b2c 100644 --- a/tests/functional/d/disable_wrong_import_position.txt +++ b/tests/functional/d/disable_wrong_import_position.txt @@ -1 +1 @@ -wrong-import-position:10:0:10:10::"Import ""import sys"" should be placed at the top of the module":UNDEFINED +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 index e7c7301e5d..111727bef4 100644 --- a/tests/functional/w/wrong_import_position_pragma_scope.py +++ b/tests/functional/w/wrong_import_position_pragma_scope.py @@ -1,23 +1,16 @@ -"""Test that wrong-import-position pragma suppression is correctly scoped.""" -# pylint: disable=unused-import,invalid-name +"""Test wrong-import-position pragma scoping.""" +# pylint: disable=unused-import -import logging +import os import sys -# This pragma should not affect subsequent import statements -logger = logging.getLogger() # pylint: disable=wrong-import-position -logging.basicConfig(level='DEBUG') +# Pragma on non-import suppresses following imports until next non-import +CONSTANT_A = False # pylint: disable=wrong-import-position +import time -logger.debug('importing modules...') -import os # [wrong-import-position] -import pathlib # [wrong-import-position] -import random # [wrong-import-position] -logger.debug('done importing') +CONSTANT_B = True +import logging # [wrong-import-position] -# Test that pragma on import line works correctly (this import should not be flagged) -constant_var = "test" +# Inline pragma on import line +CONSTANT_C = 42 import json # pylint: disable=wrong-import-position - -# Test that subsequent imports are not affected by the pragma above -import csv # [wrong-import-position] -import re # [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 index 83393dd4d6..70c1c60219 100644 --- a/tests/functional/w/wrong_import_position_pragma_scope.txt +++ b/tests/functional/w/wrong_import_position_pragma_scope.txt @@ -1,5 +1 @@ -wrong-import-position:12:0:12:9::"Import ""import os"" should be placed at the top of the module":UNDEFINED -wrong-import-position:13:0:13:14::"Import ""import pathlib"" should be placed at the top of the module":UNDEFINED -wrong-import-position:14:0:14:13::"Import ""import random"" should be placed at the top of the module":UNDEFINED -wrong-import-position:22:0:22:10::"Import ""import csv"" should be placed at the top of the module":UNDEFINED -wrong-import-position:23:0:23:9::"Import ""import re"" should be placed at the top of the module":UNDEFINED +wrong-import-position:12:0:12:14::"Import ""import logging"" should be placed at the top of the module":UNDEFINED From 4c56438d39719a95bb99de39f31fa933addbee8f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 19 Oct 2025 22:55:17 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/checkers/imports.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index e28434c824..bdd5487503 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -701,7 +701,9 @@ def _check_position(self, node: ImportNode) -> None: # Check if import comes after a non-import statement if self._first_non_import_node: # Check for inline pragma on the import line - if not self.linter.is_message_enabled("wrong-import-position", node.fromlineno): + if not self.linter.is_message_enabled( + "wrong-import-position", node.fromlineno + ): self.linter.add_ignored_message( "wrong-import-position", node.fromlineno, node ) @@ -717,7 +719,9 @@ def _check_position(self, node: ImportNode) -> None: if most_recent_non_import: check_line = most_recent_non_import.fromlineno - if not self.linter.is_message_enabled("wrong-import-position", check_line): + 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 ) @@ -726,9 +730,7 @@ def _check_position(self, node: ImportNode) -> None: ) return - self.add_message( - "wrong-import-position", node=node, args=node.as_string() - ) + self.add_message("wrong-import-position", node=node, args=node.as_string()) def _record_import( self, From 7fb7b1804d304035a21e5735ed625805b560e546 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Mon, 20 Oct 2025 02:16:02 +0300 Subject: [PATCH 6/6] fix: support block-level wrong-import-position pragma suppression Signed-off-by: Emmanuel Ferdman --- pylint/checkers/imports.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index bdd5487503..578a7a4640 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -446,8 +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 = [] + self._non_import_nodes: list[nodes.NodeNG] = [] self._module_pkg: dict[Any, Any] = ( {} ) # mapping of modules to the pkg they belong in @@ -608,7 +607,6 @@ 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( @@ -642,9 +640,6 @@ def compute_first_non_import_node( if all(valid_targets): return - if not self._first_non_import_node: - self._first_non_import_node = node - self._non_import_nodes.append(node) visit_try = visit_assignattr = visit_assign = visit_ifexp = visit_comprehension = ( @@ -666,9 +661,6 @@ def visit_functiondef( if any(root.nodes_of_class((nodes.Import, nodes.ImportFrom))): return - if not self._first_non_import_node: - self._first_non_import_node = node - self._non_import_nodes.append(node) visit_classdef = visit_for = visit_while = visit_functiondef @@ -699,7 +691,7 @@ def _check_position(self, node: ImportNode) -> None: Send a message if `node` comes before another instruction """ # Check if import comes after a non-import statement - if self._first_non_import_node: + 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