Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/10589.bugfix
Original file line number Diff line number Diff line change
@@ -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
61 changes: 36 additions & 25 deletions pylint/checkers/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down
12 changes: 8 additions & 4 deletions tests/functional/d/disable_wrong_import_position.py
Original file line number Diff line number Diff line change
@@ -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]
1 change: 1 addition & 0 deletions tests/functional/d/disable_wrong_import_position.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wrong-import-position:11:0:11:14::"Import ""import logging"" should be placed at the top of the module":UNDEFINED
16 changes: 16 additions & 0 deletions tests/functional/w/wrong_import_position_pragma_scope.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions tests/functional/w/wrong_import_position_pragma_scope.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wrong-import-position:12:0:12:14::"Import ""import logging"" should be placed at the top of the module":UNDEFINED