diff options
Diffstat (limited to 'venv/Lib/site-packages/pylint/checkers/imports.py')
-rw-r--r-- | venv/Lib/site-packages/pylint/checkers/imports.py | 981 |
1 files changed, 981 insertions, 0 deletions
diff --git a/venv/Lib/site-packages/pylint/checkers/imports.py b/venv/Lib/site-packages/pylint/checkers/imports.py new file mode 100644 index 0000000..42d4362 --- /dev/null +++ b/venv/Lib/site-packages/pylint/checkers/imports.py @@ -0,0 +1,981 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr> +# Copyright (c) 2012-2014 Google, Inc. +# Copyright (c) 2013 buck@yelp.com <buck@yelp.com> +# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com> +# Copyright (c) 2014 Brett Cannon <brett@python.org> +# Copyright (c) 2014 Arun Persaud <arun@nubati.net> +# Copyright (c) 2015-2016 Moises Lopez <moylop260@vauxoo.com> +# Copyright (c) 2015 Dmitry Pribysh <dmand@yandex.ru> +# Copyright (c) 2015 Cezar <celnazli@bitdefender.com> +# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org> +# Copyright (c) 2015 Noam Yorav-Raphael <noamraph@gmail.com> +# Copyright (c) 2015 James Morgensen <james.morgensen@gmail.com> +# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro> +# Copyright (c) 2016 Jared Garst <cultofjared@gmail.com> +# Copyright (c) 2016 Maik Röder <maikroeder@gmail.com> +# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net> +# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk> +# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com> +# Copyright (c) 2017 Michka Popoff <michkapopoff@gmail.com> +# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com> +# Copyright (c) 2017 Erik Wright <erik.wright@shopify.com> +# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com> +# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com> +# Copyright (c) 2018 Marianna Polatoglou <mpolatoglou@bloomberg.net> +# Copyright (c) 2019 Paul Renvoise <renvoisepaul@gmail.com> + +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/master/COPYING + +"""imports checkers for Python code""" + +import collections +import copy +import os +import sys +from distutils import sysconfig + +import astroid +import isort +from astroid import modutils +from astroid.decorators import cached + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import ( + check_messages, + is_from_fallback_block, + node_ignores_exception, +) +from pylint.exceptions import EmptyReportError +from pylint.graph import DotBackend, get_cycles +from pylint.interfaces import IAstroidChecker +from pylint.reporters.ureports.nodes import Paragraph, VerbatimText +from pylint.utils import get_global_option + + +def _qualified_names(modname): + """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(".") + return [".".join(names[0 : i + 1]) for i in range(len(names))] + + +def _get_import_name(importnode, modname): + """Get a prepared module name from the given import node + + In the case of relative imports, this will return the + absolute qualified module name, which might be useful + for debugging. Otherwise, the initial module name + is returned unchanged. + """ + if isinstance(importnode, astroid.ImportFrom): + if importnode.level: + root = importnode.root() + if isinstance(root, astroid.Module): + modname = root.relative_to_absolute_name( + modname, level=importnode.level + ) + return modname + + +def _get_first_import(node, context, name, base, level, alias): + """return the node where [base.]<name> is imported or None if not found + """ + fullname = "%s.%s" % (base, name) if base else name + + first = None + found = False + 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, astroid.Import): + if any(fullname == iname[0] for iname in first.names): + found = True + break + elif isinstance(first, astroid.ImportFrom): + if level == first.level: + for imported_name, imported_alias in first.names: + if fullname == "%s.%s" % (first.modname, imported_name): + found = True + break + if ( + name != "*" + and name == imported_name + and not (alias or imported_alias) + ): + found = True + break + if found: + break + if found and not astroid.are_exclusive(first, node): + return first + return None + + +def _ignore_import_failure(node, modname, ignored_modules): + for submodule in _qualified_names(modname): + if submodule in ignored_modules: + 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): + """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 = {} + for mod, files in mod_files_list: + node = (tree_defs, ()) + for prefix in mod.split("."): + node = node[0].setdefault(prefix, [{}, []]) + node[1] += files + return tree_defs + + +def _repr_tree_defs(data, indent_str=None): + """return a string which represents imports as a tree""" + lines = [] + nodes = data.items() + for i, (mod, (sub, files)) in enumerate(sorted(nodes, key=lambda x: x[0])): + if not files: + files = "" + else: + files = "(%s)" % ",".join(sorted(files)) + if indent_str is None: + lines.append("%s %s" % (mod, files)) + sub_indent_str = " " + else: + lines.append(r"%s\-%s %s" % (indent_str, mod, files)) + if i == len(nodes) - 1: + sub_indent_str = "%s " % indent_str + else: + sub_indent_str = "%s| " % indent_str + if sub: + lines.append(_repr_tree_defs(sub, sub_indent_str)) + return "\n".join(lines) + + +def _dependencies_graph(filename, dep_info): + """write dependencies as a dot (graphviz) file + """ + done = {} + printer = DotBackend(filename[:-4], rankdir="LR") + printer.emit('URL="." node[shape="box"]') + for modname, dependencies in sorted(dep_info.items()): + done[modname] = 1 + printer.emit_node(modname) + for depmodname in dependencies: + if depmodname not in done: + done[depmodname] = 1 + printer.emit_node(depmodname) + for depmodname, dependencies in sorted(dep_info.items()): + for modname in dependencies: + printer.emit_edge(modname, depmodname) + printer.generate(filename) + + +def _make_graph(filename, dep_info, sect, gtype): + """generate a dependencies graph and add some information about it in the + report's section + """ + _dependencies_graph(filename, dep_info) + sect.append(Paragraph("%simports graph has been written to %s" % (gtype, filename))) + + +# the import checker itself ################################################### + +MSGS = { + "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.", + ), + "W0401": ( + "Wildcard import %s", + "wildcard-import", + "Used when `from module import *` is detected.", + ), + "W0402": ( + "Uses of a deprecated module %r", + "deprecated-module", + "Used a module marked as deprecated is imported.", + ), + "W0404": ( + "Reimport %r (imported line %s)", + "reimported", + "Used when a module is reimported multiple times.", + ), + "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.", + ), +} + + +DEFAULT_STANDARD_LIBRARY = () +DEFAULT_KNOWN_THIRD_PARTY = ("enchant",) +DEFAULT_PREFERRED_MODULES = () + + +class ImportsChecker(BaseChecker): + """checks for + * external modules dependencies + * relative / wildcard imports + * cyclic imports + * uses of deprecated modules + * uses of modules instead of preferred modules + """ + + __implements__ = IAstroidChecker + + name = "imports" + msgs = MSGS + priority = -2 + deprecated_modules = ("optparse", "tkinter.tix") + + options = ( + ( + "deprecated-modules", + { + "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": "string", + "metavar": "<file.dot>", + "help": "Create a graph of every (i.e. internal and" + " external) dependencies in the given file" + " (report RP0402 must not be disabled).", + }, + ), + ( + "ext-import-graph", + { + "default": "", + "type": "string", + "metavar": "<file.dot>", + "help": "Create a graph of external dependencies in the" + " given file (report RP0402 must not be disabled).", + }, + ), + ( + "int-import-graph", + { + "default": "", + "type": "string", + "metavar": "<file.dot>", + "help": "Create a graph of internal dependencies in 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." + ), + }, + ), + ( + "analyse-fallback-blocks", + { + "default": False, + "type": "yn", + "metavar": "<y_or_n>", + "help": "Analyse import fallback blocks. This can be used to " + "support both Python 2 and 3 compatible code, which " + "means that the block might have code that exists " + "only in one or another interpreter, leading to false " + "positives when analysed.", + }, + ), + ( + "allow-wildcard-with-all", + { + "default": False, + "type": "yn", + "metavar": "<y_or_n>", + "help": "Allow wildcard imports from modules that define __all__.", + }, + ), + ) + + def __init__(self, linter=None): + BaseChecker.__init__(self, linter) + self.stats = None + self.import_graph = None + self._imports_stack = [] + self._first_non_import_node = None + self._module_pkg = {} # mapping of modules to the pkg they belong in + self._allow_any_import_level = set() + self.reports = ( + ("RP0401", "External dependencies", self._report_external_dependencies), + ("RP0402", "Modules dependencies graph", self._report_dependencies_graph), + ) + + self._site_packages = self._compute_site_packages() + + @staticmethod + def _compute_site_packages(): + def _normalized_path(path): + return os.path.normcase(os.path.abspath(path)) + + paths = set() + real_prefix = getattr(sys, "real_prefix", None) + for prefix in filter(None, (real_prefix, sys.prefix)): + path = sysconfig.get_python_lib(prefix=prefix) + path = _normalized_path(path) + paths.add(path) + + # Handle Debian's derivatives /usr/local. + if os.path.isfile("/etc/debian_version"): + for prefix in filter(None, (real_prefix, sys.prefix)): + libpython = os.path.join( + prefix, + "local", + "lib", + "python" + sysconfig.get_python_version(), + "dist-packages", + ) + paths.add(libpython) + return paths + + def open(self): + """called before visiting project (i.e set of modules)""" + self.linter.add_stats(dependencies={}) + self.linter.add_stats(cycles=[]) + self.stats = self.linter.stats + self.import_graph = collections.defaultdict(set) + self._module_pkg = {} # mapping of modules to the pkg they belong in + self._excluded_edges = collections.defaultdict(set) + self._ignored_modules = get_global_option(self, "ignored-modules", default=[]) + # Build a mapping {'module': 'preferred-module'} + self.preferred_modules = dict( + module.split(":") + for module in self.config.preferred_modules + if ":" in module + ) + self._allow_any_import_level = set(self.config.allow_any_import_level) + + def _import_graph_without_ignored_edges(self): + 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): + """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)) + + @check_messages(*MSGS) + def visit_import(self, node): + """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, astroid.Module): + # Allow imports nested + self._check_position(node) + if isinstance(node.scope(), astroid.Module): + self._record_import(node, imported_module) + + if imported_module is None: + continue + + self._add_imported_module(node, imported_module.name) + + @check_messages(*MSGS) + def visit_importfrom(self, node): + """triggered when a from statement is seen""" + basename = node.modname + imported_module = self._get_imported_module(node, basename) + + self._check_import_as_rename(node) + self._check_misplaced_future(node) + self._check_deprecated_module(node, basename) + 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, astroid.Module): + # Allow imports nested + self._check_position(node) + if isinstance(node.scope(), astroid.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, "%s.%s" % (imported_module.name, name)) + else: + self._add_imported_module(node, imported_module.name) + + @check_messages(*MSGS) + def leave_module(self, node): + # 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() # set for 'import x' style + met_from = set() # set for 'from x import y' style + current_package = None + for import_node, import_name in std_imports + ext_imports + loc_imports: + if not self.linter.is_message_enabled( + "ungrouped-imports", import_node.fromlineno + ): + continue + if isinstance(import_node, astroid.node_classes.ImportFrom): + met = met_from + else: + met = met_import + package, _, _ = import_name.partition(".") + if current_package and current_package != package and package in met: + self.add_message("ungrouped-imports", node=import_node, args=package) + current_package = package + met.add(package) + + self._imports_stack = [] + self._first_non_import_node = None + + def compute_first_non_import_node(self, node): + if not self.linter.is_message_enabled("wrong-import-position", node.fromlineno): + return + # 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, astroid.Module): + return + nested_allowed = [astroid.TryExcept, astroid.TryFinally] + is_nested_allowed = [ + allowed for allowed in nested_allowed if isinstance(node, allowed) + ] + if is_nested_allowed and any( + node.nodes_of_class((astroid.Import, astroid.ImportFrom)) + ): + return + if isinstance(node, astroid.Assign): + # Add compatibility for module level dunder names + # https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names + valid_targets = [ + isinstance(target, astroid.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): + if not self.linter.is_message_enabled("wrong-import-position", node.fromlineno): + return + # 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(), astroid.Module): + return + + root = node + while not isinstance(root.parent, astroid.Module): + root = root.parent + + if isinstance(root, (astroid.If, astroid.TryFinally, astroid.TryExcept)): + if any(root.nodes_of_class((astroid.Import, astroid.ImportFrom))): + return + + self._first_non_import_node = node + + visit_classdef = visit_for = visit_while = visit_functiondef + + def _check_misplaced_future(self, node): + 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, astroid.ImportFrom) + and prev.modname == "__future__" + ): + self.add_message("misplaced-future", node=node) + return + + def _check_same_line_imports(self, node): + # 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): + """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: + self.add_message("wrong-import-position", node=node, args=node.as_string()) + + def _record_import(self, node, importedmodnode): + """Record the package `node` imports from""" + if isinstance(node, astroid.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, astroid.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, imports): + imports = [import_node for (import_node, _) in imports] + return any(astroid.are_exclusive(import_node, node) for import_node in imports) + + def _check_imports_order(self, _module_node): + """Checks imports of module `node` are grouped by category + + Imports must follow this order: standard, 3rd party, local + """ + std_imports = [] + third_party_imports = [] + first_party_imports = [] + # need of a list that holds third or first party ordered import + external_imports = [] + local_imports = [] + third_party_not_ignored = [] + first_party_not_ignored = [] + local_not_ignored = [] + isort_obj = isort.SortImports( + file_contents="", + known_third_party=self.config.known_third_party, + known_standard_library=self.config.known_standard_library, + ) + for node, modname in self._imports_stack: + if modname.startswith("."): + package = "." + modname.split(".")[1] + else: + package = modname.split(".")[0] + nested = not isinstance(node.parent, astroid.Module) + ignore_for_import_order = not self.linter.is_message_enabled( + "wrong-import-order", node.fromlineno + ) + import_category = isort_obj.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=( + 'standard import "%s"' % node.as_string(), + '"%s"' % 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 and not ignore_for_import_order: + third_party_not_ignored.append(node_and_package_import) + 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=( + 'third party import "%s"' % node.as_string(), + '"%s"' % 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 and not ignore_for_import_order: + first_party_not_ignored.append(node_and_package_import) + wrong_import = local_not_ignored + if wrong_import and not nested: + self.add_message( + "wrong-import-order", + node=node, + args=( + 'first party import "%s"' % node.as_string(), + '"%s"' % wrong_import[0][0].as_string(), + ), + ) + elif import_category == "LOCALFOLDER": + local_imports.append((node, package)) + if not nested and not ignore_for_import_order: + local_not_ignored.append((node, package)) + return std_imports, external_imports, local_imports + + def _get_imported_module(self, importnode, modname): + 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 = "Cannot import {!r} due to syntax error {!r}".format( + modname, str(exc.error) # pylint: disable=no-member; false positive + ) + self.add_message("syntax-error", line=importnode.lineno, args=message) + + except astroid.AstroidBuildingException: + 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.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) + + def _add_imported_module(self, node, importedmodname): + """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 = modutils.get_module_part(importedmodname, module_file) + except ImportError: + pass + + if context_name == importedmodname: + self.add_message("import-self", node=node) + + elif not modutils.is_standard_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 + importedmodnames = self.stats["dependencies"].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): + self._excluded_edges[context_name].add(importedmodname) + + def _check_deprecated_module(self, node, mod_path): + """check if the module is deprecated""" + for mod_name in self.config.deprecated_modules: + if mod_path == mod_name or mod_path.startswith(mod_name + "."): + self.add_message("deprecated-module", node=node, args=mod_path) + + def _check_preferred_module(self, node, mod_path): + """check if the module has a preferred replacement""" + if mod_path in self.preferred_modules: + self.add_message( + "preferred-module", + node=node, + args=(self.preferred_modules[mod_path], mod_path), + ) + + def _check_import_as_rename(self, node): + names = node.names + for name in names: + if not all(name): + return + + real_name = name[0] + splitted_packages = real_name.rsplit(".") + real_name = splitted_packages[-1] + imported_name = name[1] + # consider only following cases + # import x as x + # and ignore following + # import x.y.z as z + if real_name == imported_name and len(splitted_packages) == 1: + self.add_message("useless-import-alias", node=node) + + def _check_reimport(self, node, basename=None, level=None): + """check if the import is necessary (i.e. not already done)""" + if not self.linter.is_message_enabled("reimported"): + return + + frame = node.frame() + 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 = _get_first_import( + node, known_context, name, basename, known_level, alias + ) + if first is not None: + self.add_message( + "reimported", node=node, args=(name, first.fromlineno) + ) + + def _report_external_dependencies(self, sect, _, _dummy): + """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, _, _dummy): + """write dependencies as a dot (graphviz) file""" + dep_info = self.stats["dependencies"] + if not dep_info or not ( + self.config.import_graph + or self.config.ext_import_graph + or self.config.int_import_graph + ): + raise EmptyReportError() + filename = self.config.import_graph + if filename: + _make_graph(filename, dep_info, sect, "") + filename = self.config.ext_import_graph + if filename: + _make_graph(filename, self._external_dependencies_info(), sect, "external ") + filename = self.config.int_import_graph + if filename: + _make_graph(filename, self._internal_dependencies_info(), sect, "internal ") + + def _filter_dependencies_graph(self, internal): + """build the internal or the external dependency graph""" + graph = collections.defaultdict(set) + for importee, importers in self.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 + def _external_dependencies_info(self): + """return cached external dependencies information or build and + cache them + """ + return self._filter_dependencies_graph(internal=False) + + @cached + def _internal_dependencies_info(self): + """return cached internal dependencies information or build and + cache them + """ + return self._filter_dependencies_graph(internal=True) + + def _check_wildcard_imports(self, node, imported_module): + 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): + return ( + self.config.allow_wildcard_with_all + and imported_module is not None + and "__all__" in imported_module.locals + ) + + def _check_toplevel(self, node): + """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(), astroid.Module): + return + + if isinstance(node, astroid.ImportFrom): + module_names = [node.modname] + else: + module_names = [name[0] for name in node.names] + + # Get the full names of all the imports that are not whitelisted. + 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): + """required method to auto register this checker """ + linter.register_checker(ImportsChecker(linter)) |