| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- # 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
- """Check for imports on private external modules and names."""
- from __future__ import annotations
- from pathlib import Path
- from typing import TYPE_CHECKING
- from astroid import nodes
- from pylint.checkers import BaseChecker, utils
- from pylint.interfaces import HIGH
- if TYPE_CHECKING:
- from pylint.lint.pylinter import PyLinter
- class PrivateImportChecker(BaseChecker):
- name = "import-private-name"
- msgs = {
- "C2701": (
- "Imported private %s (%s)",
- "import-private-name",
- "Used when a private module or object prefixed with _ is imported. "
- "PEP8 guidance on Naming Conventions states that public attributes with "
- "leading underscores should be considered private.",
- ),
- }
- def __init__(self, linter: PyLinter) -> None:
- BaseChecker.__init__(self, linter)
- # A mapping of private names used as a type annotation to whether it is an acceptable import
- self.all_used_type_annotations: dict[str, bool] = {}
- self.populated_annotations = False
- @utils.only_required_for_messages("import-private-name")
- def visit_import(self, node: nodes.Import) -> None:
- if utils.in_type_checking_block(node):
- return
- names = [name[0] for name in node.names]
- private_names = self._get_private_imports(names)
- private_names = self._get_type_annotation_names(node, private_names)
- if private_names:
- imported_identifier = "modules" if len(private_names) > 1 else "module"
- private_name_string = ", ".join(private_names)
- self.add_message(
- "import-private-name",
- node=node,
- args=(imported_identifier, private_name_string),
- confidence=HIGH,
- )
- @utils.only_required_for_messages("import-private-name")
- def visit_importfrom(self, node: nodes.ImportFrom) -> None:
- if utils.in_type_checking_block(node):
- return
- # Only check imported names if the module is external
- if self.same_root_dir(node, node.modname):
- return
- names = [n[0] for n in node.names]
- # Check the imported objects first. If they are all valid type annotations,
- # the package can be private
- private_names = self._get_type_annotation_names(node, names)
- if not private_names:
- return
- # There are invalid imported objects, so check the name of the package
- private_module_imports = self._get_private_imports([node.modname])
- private_module_imports = self._get_type_annotation_names(
- node, private_module_imports
- )
- if private_module_imports:
- self.add_message(
- "import-private-name",
- node=node,
- args=("module", private_module_imports[0]),
- confidence=HIGH,
- )
- return # Do not emit messages on the objects if the package is private
- private_names = self._get_private_imports(private_names)
- if private_names:
- imported_identifier = "objects" if len(private_names) > 1 else "object"
- private_name_string = ", ".join(private_names)
- self.add_message(
- "import-private-name",
- node=node,
- args=(imported_identifier, private_name_string),
- confidence=HIGH,
- )
- def _get_private_imports(self, names: list[str]) -> list[str]:
- """Returns the private names from input names by a simple string check."""
- return [name for name in names if self._name_is_private(name)]
- @staticmethod
- def _name_is_private(name: str) -> bool:
- """Returns true if the name exists, starts with `_`, and if len(name) > 4
- it is not a dunder, i.e. it does not begin and end with two underscores.
- """
- return (
- bool(name)
- and name[0] == "_"
- and (len(name) <= 4 or name[1] != "_" or name[-2:] != "__")
- )
- def _get_type_annotation_names(
- self, node: nodes.Import | nodes.ImportFrom, names: list[str]
- ) -> list[str]:
- """Removes from names any names that are used as type annotations with no other
- illegal usages.
- """
- if names and not self.populated_annotations:
- self._populate_type_annotations(node.root(), self.all_used_type_annotations)
- self.populated_annotations = True
- return [
- n
- for n in names
- if n not in self.all_used_type_annotations
- or (
- n in self.all_used_type_annotations
- and not self.all_used_type_annotations[n]
- )
- ]
- def _populate_type_annotations(
- self, node: nodes.LocalsDictNodeNG, all_used_type_annotations: dict[str, bool]
- ) -> None:
- """Adds to `all_used_type_annotations` all names ever used as a type annotation
- in the node's (nested) scopes and whether they are only used as annotation.
- """
- for name in node.locals:
- # If we find a private type annotation, make sure we do not mask illegal usages
- private_name = None
- # All the assignments using this variable that we might have to check for
- # illegal usages later
- name_assignments = []
- for usage_node in node.locals[name]:
- if isinstance(usage_node, nodes.AssignName) and isinstance(
- usage_node.parent, (nodes.AnnAssign, nodes.Assign)
- ):
- assign_parent = usage_node.parent
- if isinstance(assign_parent, nodes.AnnAssign):
- name_assignments.append(assign_parent)
- private_name = self._populate_type_annotations_annotation(
- usage_node.parent.annotation, all_used_type_annotations
- )
- elif isinstance(assign_parent, nodes.Assign):
- name_assignments.append(assign_parent)
- if isinstance(usage_node, nodes.FunctionDef):
- self._populate_type_annotations_function(
- usage_node, all_used_type_annotations
- )
- if isinstance(usage_node, nodes.LocalsDictNodeNG):
- self._populate_type_annotations(
- usage_node, all_used_type_annotations
- )
- if private_name is not None:
- # Found a new private annotation, make sure we are not accessing it elsewhere
- all_used_type_annotations[
- private_name
- ] = self._assignments_call_private_name(name_assignments, private_name)
- def _populate_type_annotations_function(
- self, node: nodes.FunctionDef, all_used_type_annotations: dict[str, bool]
- ) -> None:
- """Adds all names used as type annotation in the arguments and return type of
- the function node into the dict `all_used_type_annotations`.
- """
- if node.args and node.args.annotations:
- for annotation in node.args.annotations:
- self._populate_type_annotations_annotation(
- annotation, all_used_type_annotations
- )
- if node.returns:
- self._populate_type_annotations_annotation(
- node.returns, all_used_type_annotations
- )
- def _populate_type_annotations_annotation(
- self,
- node: nodes.Attribute | nodes.Subscript | nodes.Name | None,
- all_used_type_annotations: dict[str, bool],
- ) -> str | None:
- """Handles the possibility of an annotation either being a Name, i.e. just type,
- or a Subscript e.g. `Optional[type]` or an Attribute, e.g. `pylint.lint.linter`.
- """
- if isinstance(node, nodes.Name) and node.name not in all_used_type_annotations:
- all_used_type_annotations[node.name] = True
- return node.name # type: ignore[no-any-return]
- if isinstance(node, nodes.Subscript): # e.g. Optional[List[str]]
- # slice is the next nested type
- self._populate_type_annotations_annotation(
- node.slice, all_used_type_annotations
- )
- # value is the current type name: could be a Name or Attribute
- return self._populate_type_annotations_annotation(
- node.value, all_used_type_annotations
- )
- if isinstance(node, nodes.Attribute):
- # An attribute is a type like `pylint.lint.pylinter`. node.expr is the next level
- # up, could be another attribute
- return self._populate_type_annotations_annotation(
- node.expr, all_used_type_annotations
- )
- return None
- @staticmethod
- def _assignments_call_private_name(
- assignments: list[nodes.AnnAssign | nodes.Assign], private_name: str
- ) -> bool:
- """Returns True if no assignments involve accessing `private_name`."""
- if all(not assignment.value for assignment in assignments):
- # Variable annotated but unassigned is not allowed because there may be
- # possible illegal access elsewhere
- return False
- for assignment in assignments:
- current_attribute = None
- if isinstance(assignment.value, nodes.Call):
- current_attribute = assignment.value.func
- elif isinstance(assignment.value, nodes.Attribute):
- current_attribute = assignment.value
- elif isinstance(assignment.value, nodes.Name):
- current_attribute = assignment.value.name
- if not current_attribute:
- continue
- while isinstance(current_attribute, (nodes.Attribute, nodes.Call)):
- if isinstance(current_attribute, nodes.Call):
- current_attribute = current_attribute.func
- if not isinstance(current_attribute, nodes.Name):
- current_attribute = current_attribute.expr
- if (
- isinstance(current_attribute, nodes.Name)
- and current_attribute.name == private_name
- ):
- return False
- return True
- @staticmethod
- def same_root_dir(
- node: nodes.Import | nodes.ImportFrom, import_mod_name: str
- ) -> bool:
- """Does the node's file's path contain the base name of `import_mod_name`?"""
- if not import_mod_name: # from . import ...
- return True
- if node.level: # from .foo import ..., from ..bar import ...
- return True
- base_import_package = import_mod_name.split(".")[0]
- return base_import_package in Path(node.root().file).parent.parts
- def register(linter: PyLinter) -> None:
- linter.register_checker(PrivateImportChecker(linter))
|