| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504 |
- # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
- # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
- # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
- from __future__ import annotations
- from typing import TYPE_CHECKING, NamedTuple
- import astroid.bases
- from astroid import nodes
- from pylint.checkers import BaseChecker
- from pylint.checkers.utils import (
- in_type_checking_block,
- is_node_in_type_annotation_context,
- is_postponed_evaluation_enabled,
- only_required_for_messages,
- safe_infer,
- )
- from pylint.constants import TYPING_NORETURN
- from pylint.interfaces import HIGH, INFERENCE
- if TYPE_CHECKING:
- from pylint.lint import PyLinter
- class TypingAlias(NamedTuple):
- name: str
- name_collision: bool
- DEPRECATED_TYPING_ALIASES: dict[str, TypingAlias] = {
- "typing.Tuple": TypingAlias("tuple", False),
- "typing.List": TypingAlias("list", False),
- "typing.Dict": TypingAlias("dict", False),
- "typing.Set": TypingAlias("set", False),
- "typing.FrozenSet": TypingAlias("frozenset", False),
- "typing.Type": TypingAlias("type", False),
- "typing.Deque": TypingAlias("collections.deque", True),
- "typing.DefaultDict": TypingAlias("collections.defaultdict", True),
- "typing.OrderedDict": TypingAlias("collections.OrderedDict", True),
- "typing.Counter": TypingAlias("collections.Counter", True),
- "typing.ChainMap": TypingAlias("collections.ChainMap", True),
- "typing.Awaitable": TypingAlias("collections.abc.Awaitable", True),
- "typing.Coroutine": TypingAlias("collections.abc.Coroutine", True),
- "typing.AsyncIterable": TypingAlias("collections.abc.AsyncIterable", True),
- "typing.AsyncIterator": TypingAlias("collections.abc.AsyncIterator", True),
- "typing.AsyncGenerator": TypingAlias("collections.abc.AsyncGenerator", True),
- "typing.Iterable": TypingAlias("collections.abc.Iterable", True),
- "typing.Iterator": TypingAlias("collections.abc.Iterator", True),
- "typing.Generator": TypingAlias("collections.abc.Generator", True),
- "typing.Reversible": TypingAlias("collections.abc.Reversible", True),
- "typing.Container": TypingAlias("collections.abc.Container", True),
- "typing.Collection": TypingAlias("collections.abc.Collection", True),
- "typing.Callable": TypingAlias("collections.abc.Callable", True),
- "typing.AbstractSet": TypingAlias("collections.abc.Set", False),
- "typing.MutableSet": TypingAlias("collections.abc.MutableSet", True),
- "typing.Mapping": TypingAlias("collections.abc.Mapping", True),
- "typing.MutableMapping": TypingAlias("collections.abc.MutableMapping", True),
- "typing.Sequence": TypingAlias("collections.abc.Sequence", True),
- "typing.MutableSequence": TypingAlias("collections.abc.MutableSequence", True),
- "typing.ByteString": TypingAlias("collections.abc.ByteString", True),
- "typing.MappingView": TypingAlias("collections.abc.MappingView", True),
- "typing.KeysView": TypingAlias("collections.abc.KeysView", True),
- "typing.ItemsView": TypingAlias("collections.abc.ItemsView", True),
- "typing.ValuesView": TypingAlias("collections.abc.ValuesView", True),
- "typing.ContextManager": TypingAlias("contextlib.AbstractContextManager", False),
- "typing.AsyncContextManager": TypingAlias(
- "contextlib.AbstractAsyncContextManager", False
- ),
- "typing.Pattern": TypingAlias("re.Pattern", True),
- "typing.Match": TypingAlias("re.Match", True),
- "typing.Hashable": TypingAlias("collections.abc.Hashable", True),
- "typing.Sized": TypingAlias("collections.abc.Sized", True),
- }
- ALIAS_NAMES = frozenset(key.split(".")[1] for key in DEPRECATED_TYPING_ALIASES)
- UNION_NAMES = ("Optional", "Union")
- class DeprecatedTypingAliasMsg(NamedTuple):
- node: nodes.Name | nodes.Attribute
- qname: str
- alias: str
- parent_subscript: bool = False
- class TypingChecker(BaseChecker):
- """Find issue specifically related to type annotations."""
- name = "typing"
- msgs = {
- "W6001": (
- "'%s' is deprecated, use '%s' instead",
- "deprecated-typing-alias",
- "Emitted when a deprecated typing alias is used.",
- ),
- "R6002": (
- "'%s' will be deprecated with PY39, consider using '%s' instead%s",
- "consider-using-alias",
- "Only emitted if 'runtime-typing=no' and a deprecated "
- "typing alias is used in a type annotation context in "
- "Python 3.7 or 3.8.",
- ),
- "R6003": (
- "Consider using alternative Union syntax instead of '%s'%s",
- "consider-alternative-union-syntax",
- "Emitted when 'typing.Union' or 'typing.Optional' is used "
- "instead of the alternative Union syntax 'int | None'.",
- ),
- "E6004": (
- "'NoReturn' inside compound types is broken in 3.7.0 / 3.7.1",
- "broken-noreturn",
- "``typing.NoReturn`` inside compound types is broken in "
- "Python 3.7.0 and 3.7.1. If not dependent on runtime introspection, "
- "use string annotation instead. E.g. "
- "``Callable[..., 'NoReturn']``. https://bugs.python.org/issue34921",
- ),
- "E6005": (
- "'collections.abc.Callable' inside Optional and Union is broken in "
- "3.9.0 / 3.9.1 (use 'typing.Callable' instead)",
- "broken-collections-callable",
- "``collections.abc.Callable`` inside Optional and Union is broken in "
- "Python 3.9.0 and 3.9.1. Use ``typing.Callable`` for these cases instead. "
- "https://bugs.python.org/issue42965",
- ),
- "R6006": (
- "Type `%s` is used more than once in union type annotation. Remove redundant typehints.",
- "redundant-typehint-argument",
- "Duplicated type arguments will be skipped by `mypy` tool, therefore should be "
- "removed to avoid confusion.",
- ),
- }
- options = (
- (
- "runtime-typing",
- {
- "default": True,
- "type": "yn",
- "metavar": "<y or n>",
- "help": (
- "Set to ``no`` if the app / library does **NOT** need to "
- "support runtime introspection of type annotations. "
- "If you use type annotations **exclusively** for type checking "
- "of an application, you're probably fine. For libraries, "
- "evaluate if some users want to access the type hints "
- "at runtime first, e.g., through ``typing.get_type_hints``. "
- "Applies to Python versions 3.7 - 3.9"
- ),
- },
- ),
- )
- _should_check_typing_alias: bool
- """The use of type aliases (PEP 585) requires Python 3.9
- or Python 3.7+ with postponed evaluation.
- """
- _should_check_alternative_union_syntax: bool
- """The use of alternative union syntax (PEP 604) requires Python 3.10
- or Python 3.7+ with postponed evaluation.
- """
- def __init__(self, linter: PyLinter) -> None:
- """Initialize checker instance."""
- super().__init__(linter=linter)
- self._found_broken_callable_location: bool = False
- self._alias_name_collisions: set[str] = set()
- self._deprecated_typing_alias_msgs: list[DeprecatedTypingAliasMsg] = []
- self._consider_using_alias_msgs: list[DeprecatedTypingAliasMsg] = []
- def open(self) -> None:
- py_version = self.linter.config.py_version
- self._py37_plus = py_version >= (3, 7)
- self._py39_plus = py_version >= (3, 9)
- self._py310_plus = py_version >= (3, 10)
- self._should_check_typing_alias = self._py39_plus or (
- self._py37_plus and self.linter.config.runtime_typing is False
- )
- self._should_check_alternative_union_syntax = self._py310_plus or (
- self._py37_plus and self.linter.config.runtime_typing is False
- )
- self._should_check_noreturn = py_version < (3, 7, 2)
- self._should_check_callable = py_version < (3, 9, 2)
- def _msg_postponed_eval_hint(self, node: nodes.NodeNG) -> str:
- """Message hint if postponed evaluation isn't enabled."""
- if self._py310_plus or "annotations" in node.root().future_imports:
- return ""
- return ". Add 'from __future__ import annotations' as well"
- @only_required_for_messages(
- "deprecated-typing-alias",
- "consider-using-alias",
- "consider-alternative-union-syntax",
- "broken-noreturn",
- "broken-collections-callable",
- )
- def visit_name(self, node: nodes.Name) -> None:
- if self._should_check_typing_alias and node.name in ALIAS_NAMES:
- self._check_for_typing_alias(node)
- if self._should_check_alternative_union_syntax and node.name in UNION_NAMES:
- self._check_for_alternative_union_syntax(node, node.name)
- if self._should_check_noreturn and node.name == "NoReturn":
- self._check_broken_noreturn(node)
- if self._should_check_callable and node.name == "Callable":
- self._check_broken_callable(node)
- @only_required_for_messages(
- "deprecated-typing-alias",
- "consider-using-alias",
- "consider-alternative-union-syntax",
- "broken-noreturn",
- "broken-collections-callable",
- )
- def visit_attribute(self, node: nodes.Attribute) -> None:
- if self._should_check_typing_alias and node.attrname in ALIAS_NAMES:
- self._check_for_typing_alias(node)
- if self._should_check_alternative_union_syntax and node.attrname in UNION_NAMES:
- self._check_for_alternative_union_syntax(node, node.attrname)
- if self._should_check_noreturn and node.attrname == "NoReturn":
- self._check_broken_noreturn(node)
- if self._should_check_callable and node.attrname == "Callable":
- self._check_broken_callable(node)
- @only_required_for_messages("redundant-typehint-argument")
- def visit_annassign(self, node: nodes.AnnAssign) -> None:
- annotation = node.annotation
- if self._is_deprecated_union_annotation(annotation, "Optional"):
- if self._is_optional_none_annotation(annotation):
- self.add_message(
- "redundant-typehint-argument",
- node=annotation,
- args="None",
- confidence=HIGH,
- )
- return
- if self._is_deprecated_union_annotation(annotation, "Union") and isinstance(
- annotation.slice, nodes.Tuple
- ):
- types = annotation.slice.elts
- elif self._is_binop_union_annotation(annotation):
- types = self._parse_binops_typehints(annotation)
- else:
- return
- self._check_union_types(types, node)
- @staticmethod
- def _is_deprecated_union_annotation(
- annotation: nodes.NodeNG, union_name: str
- ) -> bool:
- return (
- isinstance(annotation, nodes.Subscript)
- and isinstance(annotation.value, nodes.Name)
- and annotation.value.name == union_name
- )
- def _is_binop_union_annotation(self, annotation: nodes.NodeNG) -> bool:
- return self._should_check_alternative_union_syntax and isinstance(
- annotation, nodes.BinOp
- )
- @staticmethod
- def _is_optional_none_annotation(annotation: nodes.Subscript) -> bool:
- return (
- isinstance(annotation.slice, nodes.Const) and annotation.slice.value is None
- )
- def _parse_binops_typehints(
- self, binop_node: nodes.BinOp, typehints_list: list[nodes.NodeNG] | None = None
- ) -> list[nodes.NodeNG]:
- typehints_list = typehints_list or []
- if isinstance(binop_node.left, nodes.BinOp):
- typehints_list.extend(
- self._parse_binops_typehints(binop_node.left, typehints_list)
- )
- else:
- typehints_list.append(binop_node.left)
- typehints_list.append(binop_node.right)
- return typehints_list
- def _check_union_types(
- self, types: list[nodes.NodeNG], annotation: nodes.NodeNG
- ) -> None:
- types_set = set()
- for typehint in types:
- typehint_str = typehint.as_string()
- if typehint_str in types_set:
- self.add_message(
- "redundant-typehint-argument",
- node=annotation,
- args=(typehint_str),
- confidence=HIGH,
- )
- else:
- types_set.add(typehint_str)
- def _check_for_alternative_union_syntax(
- self,
- node: nodes.Name | nodes.Attribute,
- name: str,
- ) -> None:
- """Check if alternative union syntax could be used.
- Requires
- - Python 3.10
- - OR: Python 3.7+ with postponed evaluation in
- a type annotation context
- """
- inferred = safe_infer(node)
- if not (
- isinstance(inferred, nodes.FunctionDef)
- and inferred.qname() in {"typing.Optional", "typing.Union"}
- or isinstance(inferred, astroid.bases.Instance)
- and inferred.qname() == "typing._SpecialForm"
- ):
- return
- if not (self._py310_plus or is_node_in_type_annotation_context(node)):
- return
- self.add_message(
- "consider-alternative-union-syntax",
- node=node,
- args=(name, self._msg_postponed_eval_hint(node)),
- confidence=INFERENCE,
- )
- def _check_for_typing_alias(
- self,
- node: nodes.Name | nodes.Attribute,
- ) -> None:
- """Check if typing alias is deprecated or could be replaced.
- Requires
- - Python 3.9
- - OR: Python 3.7+ with postponed evaluation in
- a type annotation context
- For Python 3.7+: Only emit message if change doesn't create
- any name collisions, only ever used in a type annotation
- context, and can safely be replaced.
- """
- inferred = safe_infer(node)
- if not isinstance(inferred, nodes.ClassDef):
- return
- alias = DEPRECATED_TYPING_ALIASES.get(inferred.qname(), None)
- if alias is None:
- return
- if self._py39_plus:
- if inferred.qname() == "typing.Callable" and self._broken_callable_location(
- node
- ):
- self._found_broken_callable_location = True
- self._deprecated_typing_alias_msgs.append(
- DeprecatedTypingAliasMsg(
- node,
- inferred.qname(),
- alias.name,
- )
- )
- return
- # For PY37+, check for type annotation context first
- if not is_node_in_type_annotation_context(node) and isinstance(
- node.parent, nodes.Subscript
- ):
- if alias.name_collision is True:
- self._alias_name_collisions.add(inferred.qname())
- return
- self._consider_using_alias_msgs.append(
- DeprecatedTypingAliasMsg(
- node,
- inferred.qname(),
- alias.name,
- isinstance(node.parent, nodes.Subscript),
- )
- )
- @only_required_for_messages("consider-using-alias", "deprecated-typing-alias")
- def leave_module(self, node: nodes.Module) -> None:
- """After parsing of module is complete, add messages for
- 'consider-using-alias' check.
- Make sure results are safe to recommend / collision free.
- """
- if self._py39_plus:
- for msg in self._deprecated_typing_alias_msgs:
- if (
- self._found_broken_callable_location
- and msg.qname == "typing.Callable"
- ):
- continue
- self.add_message(
- "deprecated-typing-alias",
- node=msg.node,
- args=(msg.qname, msg.alias),
- confidence=INFERENCE,
- )
- elif self._py37_plus:
- msg_future_import = self._msg_postponed_eval_hint(node)
- for msg in self._consider_using_alias_msgs:
- if msg.qname in self._alias_name_collisions:
- continue
- self.add_message(
- "consider-using-alias",
- node=msg.node,
- args=(
- msg.qname,
- msg.alias,
- msg_future_import if msg.parent_subscript else "",
- ),
- confidence=INFERENCE,
- )
- # Clear all module cache variables
- self._found_broken_callable_location = False
- self._deprecated_typing_alias_msgs.clear()
- self._alias_name_collisions.clear()
- self._consider_using_alias_msgs.clear()
- def _check_broken_noreturn(self, node: nodes.Name | nodes.Attribute) -> None:
- """Check for 'NoReturn' inside compound types."""
- if not isinstance(node.parent, nodes.BaseContainer):
- # NoReturn not part of a Union or Callable type
- return
- if (
- in_type_checking_block(node)
- or is_postponed_evaluation_enabled(node)
- and is_node_in_type_annotation_context(node)
- ):
- return
- for inferred in node.infer():
- # To deal with typing_extensions, don't use safe_infer
- if (
- isinstance(inferred, (nodes.FunctionDef, nodes.ClassDef))
- and inferred.qname() in TYPING_NORETURN
- # In Python 3.7 - 3.8, NoReturn is alias of '_SpecialForm'
- or isinstance(inferred, astroid.bases.BaseInstance)
- and isinstance(inferred._proxied, nodes.ClassDef)
- and inferred._proxied.qname() == "typing._SpecialForm"
- ):
- self.add_message("broken-noreturn", node=node, confidence=INFERENCE)
- break
- def _check_broken_callable(self, node: nodes.Name | nodes.Attribute) -> None:
- """Check for 'collections.abc.Callable' inside Optional and Union."""
- inferred = safe_infer(node)
- if not (
- isinstance(inferred, nodes.ClassDef)
- and inferred.qname() == "_collections_abc.Callable"
- and self._broken_callable_location(node)
- ):
- return
- self.add_message("broken-collections-callable", node=node, confidence=INFERENCE)
- def _broken_callable_location(self, node: nodes.Name | nodes.Attribute) -> bool:
- """Check if node would be a broken location for collections.abc.Callable."""
- if (
- in_type_checking_block(node)
- or is_postponed_evaluation_enabled(node)
- and is_node_in_type_annotation_context(node)
- ):
- return False
- # Check first Callable arg is a list of arguments -> Callable[[int], None]
- if not (
- isinstance(node.parent, nodes.Subscript)
- and isinstance(node.parent.slice, nodes.Tuple)
- and len(node.parent.slice.elts) == 2
- and isinstance(node.parent.slice.elts[0], nodes.List)
- ):
- return False
- # Check nested inside Optional or Union
- parent_subscript = node.parent.parent
- if isinstance(parent_subscript, nodes.BaseContainer):
- parent_subscript = parent_subscript.parent
- if not (
- isinstance(parent_subscript, nodes.Subscript)
- and isinstance(parent_subscript.value, (nodes.Name, nodes.Attribute))
- ):
- return False
- inferred_parent = safe_infer(parent_subscript.value)
- if not (
- isinstance(inferred_parent, nodes.FunctionDef)
- and inferred_parent.qname() in {"typing.Optional", "typing.Union"}
- or isinstance(inferred_parent, astroid.bases.Instance)
- and inferred_parent.qname() == "typing._SpecialForm"
- ):
- return False
- return True
- def register(linter: PyLinter) -> None:
- linter.register_checker(TypingChecker(linter))
|