| from functools import lru_cache |
| from typing import Dict, List, NamedTuple, Set, Union |
| |
| import astroid |
| import astroid.bases |
| import astroid.node_classes |
| |
| from pylint.checkers import BaseChecker |
| from pylint.checkers.utils import ( |
| check_messages, |
| is_node_in_type_annotation_context, |
| safe_infer, |
| ) |
| from pylint.interfaces import IAstroidChecker |
| 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: Union[astroid.Name, astroid.Attribute] |
| qname: str |
| alias: str |
| parent_subscript: bool |
| |
| |
| class TypingChecker(BaseChecker): |
| """Find issue specifically related to type annotations.""" |
| |
| __implements__ = (IAstroidChecker,) |
| |
| name = "typing" |
| priority = -1 |
| 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'.", |
| ), |
| } |
| options = ( |
| ( |
| "py-version", |
| { |
| "default": (3, 7), |
| "type": "py_version", |
| "metavar": "<py_version>", |
| "help": ( |
| "Min Python version to use for typing related checks, " |
| "e.g. ``3.7``. This should be equal to the min supported Python " |
| "version of the project." |
| ), |
| }, |
| ), |
| ( |
| "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 what to access the type hints " |
| "at runtime first, e.g., through ``typing.get_type_hints``. " |
| "Applies to Python versions 3.7 - 3.9" |
| ), |
| }, |
| ), |
| ) |
| |
| def __init__(self, linter: PyLinter) -> None: |
| """Initialize checker instance.""" |
| super().__init__(linter=linter) |
| self._alias_name_collisions: Set[str] = set() |
| self._consider_using_alias_msgs: List[DeprecatedTypingAliasMsg] = [] |
| |
| @lru_cache() |
| def _py37_plus(self) -> bool: |
| return self.config.py_version >= (3, 7) |
| |
| @lru_cache() |
| def _py39_plus(self) -> bool: |
| return self.config.py_version >= (3, 9) |
| |
| @lru_cache() |
| def _py310_plus(self) -> bool: |
| return self.config.py_version >= (3, 10) |
| |
| @lru_cache() |
| def _should_check_typing_alias(self) -> bool: |
| """The use of type aliases (PEP 585) requires Python 3.9 |
| or Python 3.7+ with postponed evaluation. |
| """ |
| return ( |
| self._py39_plus() |
| or self._py37_plus() |
| and self.config.runtime_typing is False |
| ) |
| |
| @lru_cache() |
| def _should_check_alternative_union_syntax(self) -> bool: |
| """The use of alternative union syntax (PEP 604) requires Python 3.10 |
| or Python 3.7+ with postponed evaluation. |
| """ |
| return ( |
| self._py310_plus() |
| or self._py37_plus() |
| and self.config.runtime_typing is False |
| ) |
| |
| def _msg_postponed_eval_hint(self, node) -> 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" |
| |
| @check_messages( |
| "deprecated-typing-alias", |
| "consider-using-alias", |
| "consider-alternative-union-syntax", |
| ) |
| def visit_name(self, node: astroid.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) |
| |
| @check_messages( |
| "deprecated-typing-alias", |
| "consider-using-alias", |
| "consider-alternative-union-syntax", |
| ) |
| def visit_attribute(self, node: astroid.Attribute): |
| 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) |
| |
| def _check_for_alternative_union_syntax( |
| self, |
| node: Union[astroid.Name, astroid.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, astroid.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)), |
| ) |
| |
| def _check_for_typing_alias( |
| self, |
| node: Union[astroid.Name, astroid.Attribute], |
| ) -> None: |
| """Check if typing alias is depecated 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 emitt 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, astroid.ClassDef): |
| return |
| alias = DEPRECATED_TYPING_ALIASES.get(inferred.qname(), None) |
| if alias is None: |
| return |
| |
| if self._py39_plus(): |
| self.add_message( |
| "deprecated-typing-alias", |
| node=node, |
| args=(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, astroid.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, astroid.Subscript), |
| ) |
| ) |
| |
| @check_messages("consider-using-alias") |
| def leave_module(self, node: astroid.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._py37_plus() and not self._py39_plus(): |
| msg_future_import = self._msg_postponed_eval_hint(node) |
| while True: |
| try: |
| msg = self._consider_using_alias_msgs.pop(0) |
| except IndexError: |
| break |
| 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 "", |
| ), |
| ) |
| # Clear all module cache variables |
| self._alias_name_collisions.clear() |
| self._consider_using_alias_msgs.clear() |
| |
| |
| def register(linter: PyLinter) -> None: |
| linter.register_checker(TypingChecker(linter)) |