| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103 |
- # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
- # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
- # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
- """Imports checkers for Python code."""
- from __future__ import annotations
- import collections
- import copy
- import os
- import sys
- from collections import defaultdict
- from collections.abc import ItemsView, Sequence
- from typing import TYPE_CHECKING, Any, Dict, List, Union
- import astroid
- from astroid import nodes
- from astroid.nodes._base_nodes import ImportNode
- from pylint.checkers import BaseChecker, DeprecatedMixin
- from pylint.checkers.utils import (
- get_import_name,
- in_type_checking_block,
- is_from_fallback_block,
- is_sys_guard,
- node_ignores_exception,
- )
- from pylint.exceptions import EmptyReportError
- from pylint.graph import DotBackend, get_cycles
- from pylint.interfaces import HIGH
- from pylint.reporters.ureports.nodes import Paragraph, Section, VerbatimText
- from pylint.typing import MessageDefinitionTuple
- from pylint.utils import IsortDriver
- from pylint.utils.linterstats import LinterStats
- if TYPE_CHECKING:
- from pylint.lint import PyLinter
- if sys.version_info >= (3, 8):
- from functools import cached_property
- else:
- from astroid.decorators import cachedproperty as cached_property
- # The dictionary with Any should actually be a _ImportTree again
- # but mypy doesn't support recursive types yet
- _ImportTree = Dict[str, Union[List[Dict[str, Any]], List[str]]]
- DEPRECATED_MODULES = {
- (0, 0, 0): {"tkinter.tix", "fpectl"},
- (3, 2, 0): {"optparse"},
- (3, 3, 0): {"xml.etree.cElementTree"},
- (3, 4, 0): {"imp"},
- (3, 5, 0): {"formatter"},
- (3, 6, 0): {"asynchat", "asyncore", "smtpd"},
- (3, 7, 0): {"macpath"},
- (3, 9, 0): {"lib2to3", "parser", "symbol", "binhex"},
- (3, 10, 0): {"distutils", "typing.io", "typing.re"},
- (3, 11, 0): {
- "aifc",
- "audioop",
- "cgi",
- "cgitb",
- "chunk",
- "crypt",
- "imghdr",
- "msilib",
- "mailcap",
- "nis",
- "nntplib",
- "ossaudiodev",
- "pipes",
- "sndhdr",
- "spwd",
- "sunau",
- "sre_compile",
- "sre_constants",
- "sre_parse",
- "telnetlib",
- "uu",
- "xdrlib",
- },
- }
- def _qualified_names(modname: str | None) -> list[str]:
- """Split the names of the given module into subparts.
- For example,
- _qualified_names('pylint.checkers.ImportsChecker')
- returns
- ['pylint', 'pylint.checkers', 'pylint.checkers.ImportsChecker']
- """
- names = modname.split(".") if modname is not None else ""
- return [".".join(names[0 : i + 1]) for i in range(len(names))]
- def _get_first_import(
- node: ImportNode,
- context: nodes.LocalsDictNodeNG,
- name: str,
- base: str | None,
- level: int | None,
- alias: str | None,
- ) -> tuple[nodes.Import | nodes.ImportFrom | None, str | None]:
- """Return the node where [base.]<name> is imported or None if not found."""
- fullname = f"{base}.{name}" if base else name
- first = None
- found = False
- msg = "reimported"
- for first in context.body:
- if first is node:
- continue
- if first.scope() is node.scope() and first.fromlineno > node.fromlineno:
- continue
- if isinstance(first, nodes.Import):
- if any(fullname == iname[0] for iname in first.names):
- found = True
- break
- for imported_name, imported_alias in first.names:
- if not imported_alias and imported_name == alias:
- found = True
- msg = "shadowed-import"
- break
- if found:
- break
- elif isinstance(first, nodes.ImportFrom):
- if level == first.level:
- for imported_name, imported_alias in first.names:
- if fullname == f"{first.modname}.{imported_name}":
- found = True
- break
- if (
- name != "*"
- and name == imported_name
- and not (alias or imported_alias)
- ):
- found = True
- break
- if not imported_alias and imported_name == alias:
- found = True
- msg = "shadowed-import"
- break
- if found:
- break
- if found and not astroid.are_exclusive(first, node):
- return first, msg
- return None, None
- def _ignore_import_failure(
- node: ImportNode,
- modname: str | None,
- ignored_modules: Sequence[str],
- ) -> bool:
- for submodule in _qualified_names(modname):
- if submodule in ignored_modules:
- return True
- # Ignore import failure if part of guarded import block
- # I.e. `sys.version_info` or `typing.TYPE_CHECKING`
- if in_type_checking_block(node):
- return True
- if isinstance(node.parent, nodes.If) and is_sys_guard(node.parent):
- return True
- return node_ignores_exception(node, ImportError)
- # utilities to represents import dependencies as tree and dot graph ###########
- def _make_tree_defs(mod_files_list: ItemsView[str, set[str]]) -> _ImportTree:
- """Get a list of 2-uple (module, list_of_files_which_import_this_module),
- it will return a dictionary to represent this as a tree.
- """
- tree_defs: _ImportTree = {}
- for mod, files in mod_files_list:
- node: list[_ImportTree | list[str]] = [tree_defs, []]
- for prefix in mod.split("."):
- assert isinstance(node[0], dict)
- node = node[0].setdefault(prefix, ({}, [])) # type: ignore[arg-type,assignment]
- assert isinstance(node[1], list)
- node[1].extend(files)
- return tree_defs
- def _repr_tree_defs(data: _ImportTree, indent_str: str | None = None) -> str:
- """Return a string which represents imports as a tree."""
- lines = []
- nodes_items = data.items()
- for i, (mod, (sub, files)) in enumerate(sorted(nodes_items, key=lambda x: x[0])):
- files_list = "" if not files else f"({','.join(sorted(files))})"
- if indent_str is None:
- lines.append(f"{mod} {files_list}")
- sub_indent_str = " "
- else:
- lines.append(rf"{indent_str}\-{mod} {files_list}")
- if i == len(nodes_items) - 1:
- sub_indent_str = f"{indent_str} "
- else:
- sub_indent_str = f"{indent_str}| "
- if sub and isinstance(sub, dict):
- lines.append(_repr_tree_defs(sub, sub_indent_str))
- return "\n".join(lines)
- def _dependencies_graph(filename: str, dep_info: dict[str, set[str]]) -> str:
- """Write dependencies as a dot (graphviz) file."""
- done = {}
- printer = DotBackend(os.path.splitext(os.path.basename(filename))[0], rankdir="LR")
- printer.emit('URL="." node[shape="box"]')
- for modname, dependencies in sorted(dep_info.items()):
- sorted_dependencies = sorted(dependencies)
- done[modname] = 1
- printer.emit_node(modname)
- for depmodname in sorted_dependencies:
- if depmodname not in done:
- done[depmodname] = 1
- printer.emit_node(depmodname)
- for depmodname, dependencies in sorted(dep_info.items()):
- for modname in sorted(dependencies):
- printer.emit_edge(modname, depmodname)
- return printer.generate(filename)
- def _make_graph(
- filename: str, dep_info: dict[str, set[str]], sect: Section, gtype: str
- ) -> None:
- """Generate a dependencies graph and add some information about it in the
- report's section.
- """
- outputfile = _dependencies_graph(filename, dep_info)
- sect.append(Paragraph((f"{gtype}imports graph has been written to {outputfile}",)))
- # the import checker itself ###################################################
- MSGS: dict[str, MessageDefinitionTuple] = {
- "E0401": (
- "Unable to import %s",
- "import-error",
- "Used when pylint has been unable to import a module.",
- {"old_names": [("F0401", "old-import-error")]},
- ),
- "E0402": (
- "Attempted relative import beyond top-level package",
- "relative-beyond-top-level",
- "Used when a relative import tries to access too many levels "
- "in the current package.",
- ),
- "R0401": (
- "Cyclic import (%s)",
- "cyclic-import",
- "Used when a cyclic import between two or more modules is detected.",
- ),
- "R0402": (
- "Use 'from %s import %s' instead",
- "consider-using-from-import",
- "Emitted when a submodule of a package is imported and "
- "aliased with the same name, "
- "e.g., instead of ``import concurrent.futures as futures`` use "
- "``from concurrent import futures``.",
- ),
- "W0401": (
- "Wildcard import %s",
- "wildcard-import",
- "Used when `from module import *` is detected.",
- ),
- "W0404": (
- "Reimport %r (imported line %s)",
- "reimported",
- "Used when a module is imported more than once.",
- ),
- "W0406": (
- "Module import itself",
- "import-self",
- "Used when a module is importing itself.",
- ),
- "W0407": (
- "Prefer importing %r instead of %r",
- "preferred-module",
- "Used when a module imported has a preferred replacement module.",
- ),
- "W0410": (
- "__future__ import is not the first non docstring statement",
- "misplaced-future",
- "Python 2.5 and greater require __future__ import to be the "
- "first non docstring statement in the module.",
- ),
- "C0410": (
- "Multiple imports on one line (%s)",
- "multiple-imports",
- "Used when import statement importing multiple modules is detected.",
- ),
- "C0411": (
- "%s should be placed before %s",
- "wrong-import-order",
- "Used when PEP8 import order is not respected (standard imports "
- "first, then third-party libraries, then local imports).",
- ),
- "C0412": (
- "Imports from package %s are not grouped",
- "ungrouped-imports",
- "Used when imports are not grouped by packages.",
- ),
- "C0413": (
- 'Import "%s" should be placed at the top of the module',
- "wrong-import-position",
- "Used when code and imports are mixed.",
- ),
- "C0414": (
- "Import alias does not rename original package",
- "useless-import-alias",
- "Used when an import alias is same as original package, "
- "e.g., using import numpy as numpy instead of import numpy as np.",
- ),
- "C0415": (
- "Import outside toplevel (%s)",
- "import-outside-toplevel",
- "Used when an import statement is used anywhere other than the module "
- "toplevel. Move this import to the top of the file.",
- ),
- "W0416": (
- "Shadowed %r (imported line %s)",
- "shadowed-import",
- "Used when a module is aliased with a name that shadows another import.",
- ),
- }
- DEFAULT_STANDARD_LIBRARY = ()
- DEFAULT_KNOWN_THIRD_PARTY = ("enchant",)
- DEFAULT_PREFERRED_MODULES = ()
- class ImportsChecker(DeprecatedMixin, BaseChecker):
- """BaseChecker for import statements.
- Checks for
- * external modules dependencies
- * relative / wildcard imports
- * cyclic imports
- * uses of deprecated modules
- * uses of modules instead of preferred modules
- """
- name = "imports"
- msgs = {**DeprecatedMixin.DEPRECATED_MODULE_MESSAGE, **MSGS}
- default_deprecated_modules = ()
- options = (
- (
- "deprecated-modules",
- {
- "default": default_deprecated_modules,
- "type": "csv",
- "metavar": "<modules>",
- "help": "Deprecated modules which should not be used,"
- " separated by a comma.",
- },
- ),
- (
- "preferred-modules",
- {
- "default": DEFAULT_PREFERRED_MODULES,
- "type": "csv",
- "metavar": "<module:preferred-module>",
- "help": "Couples of modules and preferred modules,"
- " separated by a comma.",
- },
- ),
- (
- "import-graph",
- {
- "default": "",
- "type": "path",
- "metavar": "<file.gv>",
- "help": "Output a graph (.gv or any supported image format) of"
- " all (i.e. internal and external) dependencies to the given file"
- " (report RP0402 must not be disabled).",
- },
- ),
- (
- "ext-import-graph",
- {
- "default": "",
- "type": "path",
- "metavar": "<file.gv>",
- "help": "Output a graph (.gv or any supported image format)"
- " of external dependencies to the given file"
- " (report RP0402 must not be disabled).",
- },
- ),
- (
- "int-import-graph",
- {
- "default": "",
- "type": "path",
- "metavar": "<file.gv>",
- "help": "Output a graph (.gv or any supported image format)"
- " of internal dependencies to the given file"
- " (report RP0402 must not be disabled).",
- },
- ),
- (
- "known-standard-library",
- {
- "default": DEFAULT_STANDARD_LIBRARY,
- "type": "csv",
- "metavar": "<modules>",
- "help": "Force import order to recognize a module as part of "
- "the standard compatibility libraries.",
- },
- ),
- (
- "known-third-party",
- {
- "default": DEFAULT_KNOWN_THIRD_PARTY,
- "type": "csv",
- "metavar": "<modules>",
- "help": "Force import order to recognize a module as part of "
- "a third party library.",
- },
- ),
- (
- "allow-any-import-level",
- {
- "default": (),
- "type": "csv",
- "metavar": "<modules>",
- "help": (
- "List of modules that can be imported at any level, not just "
- "the top level one."
- ),
- },
- ),
- (
- "allow-wildcard-with-all",
- {
- "default": False,
- "type": "yn",
- "metavar": "<y or n>",
- "help": "Allow wildcard imports from modules that define __all__.",
- },
- ),
- (
- "allow-reexport-from-package",
- {
- "default": False,
- "type": "yn",
- "metavar": "<y or n>",
- "help": "Allow explicit reexports by alias from a package __init__.",
- },
- ),
- )
- 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._module_pkg: dict[
- Any, Any
- ] = {} # mapping of modules to the pkg they belong in
- self._allow_any_import_level: set[Any] = set()
- self.reports = (
- ("RP0401", "External dependencies", self._report_external_dependencies),
- ("RP0402", "Modules dependencies graph", self._report_dependencies_graph),
- )
- def open(self) -> None:
- """Called before visiting project (i.e set of modules)."""
- self.linter.stats.dependencies = {}
- self.linter.stats = self.linter.stats
- self.import_graph = defaultdict(set)
- self._module_pkg = {} # mapping of modules to the pkg they belong in
- self._current_module_package = False
- self._excluded_edges: defaultdict[str, set[str]] = defaultdict(set)
- self._ignored_modules: Sequence[str] = self.linter.config.ignored_modules
- # Build a mapping {'module': 'preferred-module'}
- self.preferred_modules = dict(
- module.split(":")
- for module in self.linter.config.preferred_modules
- if ":" in module
- )
- self._allow_any_import_level = set(self.linter.config.allow_any_import_level)
- self._allow_reexport_package = self.linter.config.allow_reexport_from_package
- def _import_graph_without_ignored_edges(self) -> defaultdict[str, set[str]]:
- filtered_graph = copy.deepcopy(self.import_graph)
- for node in filtered_graph:
- filtered_graph[node].difference_update(self._excluded_edges[node])
- return filtered_graph
- def close(self) -> None:
- """Called before visiting project (i.e set of modules)."""
- if self.linter.is_message_enabled("cyclic-import"):
- graph = self._import_graph_without_ignored_edges()
- vertices = list(graph)
- for cycle in get_cycles(graph, vertices=vertices):
- self.add_message("cyclic-import", args=" -> ".join(cycle))
- def deprecated_modules(self) -> set[str]:
- """Callback returning the deprecated modules."""
- # First get the modules the user indicated
- all_deprecated_modules = set(self.linter.config.deprecated_modules)
- # Now get the hard-coded ones from the stdlib
- for since_vers, mod_set in DEPRECATED_MODULES.items():
- if since_vers <= sys.version_info:
- all_deprecated_modules = all_deprecated_modules.union(mod_set)
- return all_deprecated_modules
- def visit_module(self, node: nodes.Module) -> None:
- """Store if current module is a package, i.e. an __init__ file."""
- self._current_module_package = node.package
- def visit_import(self, node: nodes.Import) -> None:
- """Triggered when an import statement is seen."""
- self._check_reimport(node)
- self._check_import_as_rename(node)
- self._check_toplevel(node)
- names = [name for name, _ in node.names]
- if len(names) >= 2:
- self.add_message("multiple-imports", args=", ".join(names), node=node)
- for name in names:
- self.check_deprecated_module(node, name)
- self._check_preferred_module(node, name)
- imported_module = self._get_imported_module(node, name)
- if isinstance(node.parent, nodes.Module):
- # Allow imports nested
- self._check_position(node)
- if isinstance(node.scope(), nodes.Module):
- self._record_import(node, imported_module)
- if imported_module is None:
- continue
- self._add_imported_module(node, imported_module.name)
- def visit_importfrom(self, node: nodes.ImportFrom) -> None:
- """Triggered when a from statement is seen."""
- basename = node.modname
- imported_module = self._get_imported_module(node, basename)
- absolute_name = get_import_name(node, basename)
- self._check_import_as_rename(node)
- self._check_misplaced_future(node)
- self.check_deprecated_module(node, absolute_name)
- self._check_preferred_module(node, basename)
- self._check_wildcard_imports(node, imported_module)
- self._check_same_line_imports(node)
- self._check_reimport(node, basename=basename, level=node.level)
- self._check_toplevel(node)
- if isinstance(node.parent, nodes.Module):
- # Allow imports nested
- self._check_position(node)
- if isinstance(node.scope(), nodes.Module):
- self._record_import(node, imported_module)
- if imported_module is None:
- return
- for name, _ in node.names:
- if name != "*":
- self._add_imported_module(node, f"{imported_module.name}.{name}")
- else:
- self._add_imported_module(node, imported_module.name)
- def leave_module(self, node: nodes.Module) -> None:
- # Check imports are grouped by category (standard, 3rd party, local)
- std_imports, ext_imports, loc_imports = self._check_imports_order(node)
- # Check that imports are grouped by package within a given category
- met_import: set[str] = set() # set for 'import x' style
- met_from: set[str] = set() # set for 'from x import y' style
- current_package = None
- for import_node, import_name in std_imports + ext_imports + loc_imports:
- met = met_from if isinstance(import_node, nodes.ImportFrom) else met_import
- package, _, _ = import_name.partition(".")
- if (
- current_package
- and current_package != package
- and package in met
- and not in_type_checking_block(import_node)
- and not (
- isinstance(import_node.parent, nodes.If)
- and is_sys_guard(import_node.parent)
- )
- ):
- self.add_message("ungrouped-imports", node=import_node, args=package)
- current_package = package
- if not self.linter.is_message_enabled(
- "ungrouped-imports", import_node.fromlineno
- ):
- continue
- met.add(package)
- self._imports_stack = []
- self._first_non_import_node = None
- def compute_first_non_import_node(
- self,
- node: nodes.If
- | nodes.Expr
- | nodes.Comprehension
- | nodes.IfExp
- | nodes.Assign
- | nodes.AssignAttr
- | nodes.TryExcept
- | nodes.TryFinally,
- ) -> 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
- if not isinstance(node.parent, nodes.Module):
- return
- nested_allowed = [nodes.TryExcept, nodes.TryFinally]
- is_nested_allowed = [
- allowed for allowed in nested_allowed if isinstance(node, allowed)
- ]
- if is_nested_allowed and any(
- node.nodes_of_class((nodes.Import, nodes.ImportFrom))
- ):
- return
- if isinstance(node, nodes.Assign):
- # Add compatibility for module level dunder names
- # https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names
- valid_targets = [
- isinstance(target, nodes.AssignName)
- and target.name.startswith("__")
- and target.name.endswith("__")
- for target in node.targets
- ]
- if all(valid_targets):
- return
- self._first_non_import_node = node
- visit_tryfinally = (
- visit_tryexcept
- ) = (
- visit_assignattr
- ) = (
- visit_assign
- ) = (
- visit_ifexp
- ) = visit_comprehension = visit_expr = visit_if = 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.
- if not isinstance(node.parent.scope(), nodes.Module):
- return
- root = node
- while not isinstance(root.parent, nodes.Module):
- root = root.parent
- if isinstance(root, (nodes.If, nodes.TryFinally, nodes.TryExcept)):
- if any(root.nodes_of_class((nodes.Import, nodes.ImportFrom))):
- return
- self._first_non_import_node = node
- visit_classdef = visit_for = visit_while = visit_functiondef
- def _check_misplaced_future(self, node: nodes.ImportFrom) -> None:
- basename = node.modname
- if basename == "__future__":
- # check if this is the first non-docstring statement in the module
- prev = node.previous_sibling()
- if prev:
- # consecutive future statements are possible
- if not (
- isinstance(prev, nodes.ImportFrom) and prev.modname == "__future__"
- ):
- self.add_message("misplaced-future", node=node)
- return
- def _check_same_line_imports(self, node: nodes.ImportFrom) -> None:
- # Detect duplicate imports on the same line.
- names = (name for name, _ in node.names)
- counter = collections.Counter(names)
- for name, count in counter.items():
- if count > 1:
- self.add_message("reimported", node=node, args=(name, node.fromlineno))
- def _check_position(self, node: ImportNode) -> None:
- """Check `node` import or importfrom node position is correct.
- 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
- ):
- self.add_message(
- "wrong-import-position", node=node, args=node.as_string()
- )
- else:
- self.linter.add_ignored_message(
- "wrong-import-position", node.fromlineno, node
- )
- def _record_import(
- self,
- node: ImportNode,
- importedmodnode: nodes.Module | None,
- ) -> None:
- """Record the package `node` imports from."""
- if isinstance(node, nodes.ImportFrom):
- importedname = node.modname
- else:
- importedname = importedmodnode.name if importedmodnode else None
- if not importedname:
- importedname = node.names[0][0].split(".")[0]
- if isinstance(node, nodes.ImportFrom) and (node.level or 0) >= 1:
- # We need the importedname with first point to detect local package
- # Example of node:
- # 'from .my_package1 import MyClass1'
- # the output should be '.my_package1' instead of 'my_package1'
- # Example of node:
- # 'from . import my_package2'
- # the output should be '.my_package2' instead of '{pyfile}'
- importedname = "." + importedname
- self._imports_stack.append((node, importedname))
- @staticmethod
- def _is_fallback_import(
- node: ImportNode, imports: list[tuple[ImportNode, str]]
- ) -> bool:
- imports = [import_node for (import_node, _) in imports]
- return any(astroid.are_exclusive(import_node, node) for import_node in imports)
- # pylint: disable = too-many-statements
- def _check_imports_order(
- self, _module_node: nodes.Module
- ) -> tuple[
- list[tuple[ImportNode, str]],
- list[tuple[ImportNode, str]],
- list[tuple[ImportNode, str]],
- ]:
- """Checks imports of module `node` are grouped by category.
- Imports must follow this order: standard, 3rd party, local
- """
- std_imports: list[tuple[ImportNode, str]] = []
- third_party_imports: list[tuple[ImportNode, str]] = []
- first_party_imports: list[tuple[ImportNode, str]] = []
- # need of a list that holds third or first party ordered import
- external_imports: list[tuple[ImportNode, str]] = []
- local_imports: list[tuple[ImportNode, str]] = []
- third_party_not_ignored: list[tuple[ImportNode, str]] = []
- first_party_not_ignored: list[tuple[ImportNode, str]] = []
- local_not_ignored: list[tuple[ImportNode, str]] = []
- isort_driver = IsortDriver(self.linter.config)
- for node, modname in self._imports_stack:
- if modname.startswith("."):
- package = "." + modname.split(".")[1]
- else:
- package = modname.split(".")[0]
- nested = not isinstance(node.parent, nodes.Module)
- ignore_for_import_order = not self.linter.is_message_enabled(
- "wrong-import-order", node.fromlineno
- )
- import_category = isort_driver.place_module(package)
- node_and_package_import = (node, package)
- if import_category in {"FUTURE", "STDLIB"}:
- std_imports.append(node_and_package_import)
- wrong_import = (
- third_party_not_ignored
- or first_party_not_ignored
- or local_not_ignored
- )
- if self._is_fallback_import(node, wrong_import):
- continue
- if wrong_import and not nested:
- self.add_message(
- "wrong-import-order",
- node=node,
- args=(
- f'standard import "{node.as_string()}"',
- f'"{wrong_import[0][0].as_string()}"',
- ),
- )
- elif import_category == "THIRDPARTY":
- third_party_imports.append(node_and_package_import)
- external_imports.append(node_and_package_import)
- if not nested:
- if not ignore_for_import_order:
- third_party_not_ignored.append(node_and_package_import)
- else:
- self.linter.add_ignored_message(
- "wrong-import-order", node.fromlineno, node
- )
- wrong_import = first_party_not_ignored or local_not_ignored
- if wrong_import and not nested:
- self.add_message(
- "wrong-import-order",
- node=node,
- args=(
- f'third party import "{node.as_string()}"',
- f'"{wrong_import[0][0].as_string()}"',
- ),
- )
- elif import_category == "FIRSTPARTY":
- first_party_imports.append(node_and_package_import)
- external_imports.append(node_and_package_import)
- if not nested:
- if not ignore_for_import_order:
- first_party_not_ignored.append(node_and_package_import)
- else:
- self.linter.add_ignored_message(
- "wrong-import-order", node.fromlineno, node
- )
- wrong_import = local_not_ignored
- if wrong_import and not nested:
- self.add_message(
- "wrong-import-order",
- node=node,
- args=(
- f'first party import "{node.as_string()}"',
- f'"{wrong_import[0][0].as_string()}"',
- ),
- )
- elif import_category == "LOCALFOLDER":
- local_imports.append((node, package))
- if not nested:
- if not ignore_for_import_order:
- local_not_ignored.append((node, package))
- else:
- self.linter.add_ignored_message(
- "wrong-import-order", node.fromlineno, node
- )
- return std_imports, external_imports, local_imports
- def _get_imported_module(
- self, importnode: ImportNode, modname: str | None
- ) -> nodes.Module | None:
- try:
- return importnode.do_import_module(modname)
- except astroid.TooManyLevelsError:
- if _ignore_import_failure(importnode, modname, self._ignored_modules):
- return None
- self.add_message("relative-beyond-top-level", node=importnode)
- except astroid.AstroidSyntaxError as exc:
- message = f"Cannot import {modname!r} due to '{exc.error}'"
- self.add_message(
- "syntax-error", line=importnode.lineno, args=message, confidence=HIGH
- )
- except astroid.AstroidBuildingError:
- if not self.linter.is_message_enabled("import-error"):
- return None
- if _ignore_import_failure(importnode, modname, self._ignored_modules):
- return None
- if (
- not self.linter.config.analyse_fallback_blocks
- and is_from_fallback_block(importnode)
- ):
- return None
- dotted_modname = get_import_name(importnode, modname)
- self.add_message("import-error", args=repr(dotted_modname), node=importnode)
- except Exception as e: # pragma: no cover
- raise astroid.AstroidError from e
- return None
- def _add_imported_module(self, node: ImportNode, importedmodname: str) -> None:
- """Notify an imported module, used to analyze dependencies."""
- module_file = node.root().file
- context_name = node.root().name
- base = os.path.splitext(os.path.basename(module_file))[0]
- try:
- importedmodname = astroid.modutils.get_module_part(
- importedmodname, module_file
- )
- except ImportError:
- pass
- if context_name == importedmodname:
- self.add_message("import-self", node=node)
- elif not astroid.modutils.is_stdlib_module(importedmodname):
- # if this is not a package __init__ module
- if base != "__init__" and context_name not in self._module_pkg:
- # record the module's parent, or the module itself if this is
- # a top level module, as the package it belongs to
- self._module_pkg[context_name] = context_name.rsplit(".", 1)[0]
- # handle dependencies
- dependencies_stat: dict[str, set[str]] = self.linter.stats.dependencies
- importedmodnames = dependencies_stat.setdefault(importedmodname, set())
- if context_name not in importedmodnames:
- importedmodnames.add(context_name)
- # update import graph
- self.import_graph[context_name].add(importedmodname)
- if not self.linter.is_message_enabled(
- "cyclic-import", line=node.lineno
- ) or in_type_checking_block(node):
- self._excluded_edges[context_name].add(importedmodname)
- def _check_preferred_module(self, node: ImportNode, mod_path: str) -> None:
- """Check if the module has a preferred replacement."""
- mod_compare = [mod_path]
- # build a comparison list of possible names using importfrom
- if isinstance(node, astroid.nodes.node_classes.ImportFrom):
- mod_compare = [f"{node.modname}.{name[0]}" for name in node.names]
- # find whether there are matches with the import vs preferred_modules keys
- matches = [
- k
- for k in self.preferred_modules
- for mod in mod_compare
- # exact match
- if k == mod
- # checks for base module matches
- or k in mod.split(".")[0]
- ]
- # if we have matches, add message
- if matches:
- self.add_message(
- "preferred-module",
- node=node,
- args=(self.preferred_modules[matches[0]], matches[0]),
- )
- def _check_import_as_rename(self, node: ImportNode) -> None:
- names = node.names
- for name in names:
- if not all(name):
- return
- splitted_packages = name[0].rsplit(".", maxsplit=1)
- import_name = splitted_packages[-1]
- aliased_name = name[1]
- if import_name != aliased_name:
- continue
- if len(splitted_packages) == 1 and (
- self._allow_reexport_package is False
- or self._current_module_package is False
- ):
- self.add_message("useless-import-alias", node=node, confidence=HIGH)
- elif len(splitted_packages) == 2:
- self.add_message(
- "consider-using-from-import",
- node=node,
- args=(splitted_packages[0], import_name),
- )
- def _check_reimport(
- self,
- node: ImportNode,
- basename: str | None = None,
- level: int | None = None,
- ) -> None:
- """Check if a module with the same name is already imported or aliased."""
- if not self.linter.is_message_enabled(
- "reimported"
- ) and not self.linter.is_message_enabled("shadowed-import"):
- return
- frame = node.frame(future=True)
- root = node.root()
- contexts = [(frame, level)]
- if root is not frame:
- contexts.append((root, None))
- for known_context, known_level in contexts:
- for name, alias in node.names:
- first, msg = _get_first_import(
- node, known_context, name, basename, known_level, alias
- )
- if first is not None and msg is not None:
- name = name if msg == "reimported" else alias
- self.add_message(
- msg, node=node, args=(name, first.fromlineno), confidence=HIGH
- )
- def _report_external_dependencies(
- self, sect: Section, _: LinterStats, _dummy: LinterStats | None
- ) -> None:
- """Return a verbatim layout for displaying dependencies."""
- dep_info = _make_tree_defs(self._external_dependencies_info.items())
- if not dep_info:
- raise EmptyReportError()
- tree_str = _repr_tree_defs(dep_info)
- sect.append(VerbatimText(tree_str))
- def _report_dependencies_graph(
- self, sect: Section, _: LinterStats, _dummy: LinterStats | None
- ) -> None:
- """Write dependencies as a dot (graphviz) file."""
- dep_info = self.linter.stats.dependencies
- if not dep_info or not (
- self.linter.config.import_graph
- or self.linter.config.ext_import_graph
- or self.linter.config.int_import_graph
- ):
- raise EmptyReportError()
- filename = self.linter.config.import_graph
- if filename:
- _make_graph(filename, dep_info, sect, "")
- filename = self.linter.config.ext_import_graph
- if filename:
- _make_graph(filename, self._external_dependencies_info, sect, "external ")
- filename = self.linter.config.int_import_graph
- if filename:
- _make_graph(filename, self._internal_dependencies_info, sect, "internal ")
- def _filter_dependencies_graph(self, internal: bool) -> defaultdict[str, set[str]]:
- """Build the internal or the external dependency graph."""
- graph: defaultdict[str, set[str]] = defaultdict(set)
- for importee, importers in self.linter.stats.dependencies.items():
- for importer in importers:
- package = self._module_pkg.get(importer, importer)
- is_inside = importee.startswith(package)
- if is_inside and internal or not is_inside and not internal:
- graph[importee].add(importer)
- return graph
- @cached_property
- def _external_dependencies_info(self) -> defaultdict[str, set[str]]:
- """Return cached external dependencies information or build and
- cache them.
- """
- return self._filter_dependencies_graph(internal=False)
- @cached_property
- def _internal_dependencies_info(self) -> defaultdict[str, set[str]]:
- """Return cached internal dependencies information or build and
- cache them.
- """
- return self._filter_dependencies_graph(internal=True)
- def _check_wildcard_imports(
- self, node: nodes.ImportFrom, imported_module: nodes.Module | None
- ) -> None:
- if node.root().package:
- # Skip the check if in __init__.py issue #2026
- return
- wildcard_import_is_allowed = self._wildcard_import_is_allowed(imported_module)
- for name, _ in node.names:
- if name == "*" and not wildcard_import_is_allowed:
- self.add_message("wildcard-import", args=node.modname, node=node)
- def _wildcard_import_is_allowed(self, imported_module: nodes.Module | None) -> bool:
- return (
- self.linter.config.allow_wildcard_with_all
- and imported_module is not None
- and "__all__" in imported_module.locals
- )
- def _check_toplevel(self, node: ImportNode) -> None:
- """Check whether the import is made outside the module toplevel."""
- # If the scope of the import is a module, then obviously it is
- # not outside the module toplevel.
- if isinstance(node.scope(), nodes.Module):
- return
- module_names = [
- f"{node.modname}.{name[0]}"
- if isinstance(node, nodes.ImportFrom)
- else name[0]
- for name in node.names
- ]
- # Get the full names of all the imports that are only allowed at the module level
- scoped_imports = [
- name for name in module_names if name not in self._allow_any_import_level
- ]
- if scoped_imports:
- self.add_message(
- "import-outside-toplevel", args=", ".join(scoped_imports), node=node
- )
- def register(linter: PyLinter) -> None:
- linter.register_checker(ImportsChecker(linter))
|