|
| 1 | +from functools import lru_cache |
| 2 | +from typing import Dict, List, NamedTuple, Set, Union |
| 3 | + |
| 4 | +import astroid |
| 5 | +import astroid.bases |
| 6 | +import astroid.node_classes |
| 7 | + |
| 8 | +from pylint.checkers import BaseChecker |
| 9 | +from pylint.checkers.utils import ( |
| 10 | + check_messages, |
| 11 | + is_node_in_type_annotation_context, |
| 12 | + safe_infer, |
| 13 | +) |
| 14 | +from pylint.interfaces import IAstroidChecker |
| 15 | +from pylint.lint import PyLinter |
| 16 | + |
| 17 | + |
| 18 | +class TypingAlias(NamedTuple): |
| 19 | + name: str |
| 20 | + name_collision: bool |
| 21 | + |
| 22 | + |
| 23 | +DEPRECATED_TYPING_ALIASES: Dict[str, TypingAlias] = { |
| 24 | + "typing.Tuple": TypingAlias("tuple", False), |
| 25 | + "typing.List": TypingAlias("list", False), |
| 26 | + "typing.Dict": TypingAlias("dict", False), |
| 27 | + "typing.Set": TypingAlias("set", False), |
| 28 | + "typing.FrozenSet": TypingAlias("frozenset", False), |
| 29 | + "typing.Type": TypingAlias("type", False), |
| 30 | + "typing.Deque": TypingAlias("collections.deque", True), |
| 31 | + "typing.DefaultDict": TypingAlias("collections.defaultdict", True), |
| 32 | + "typing.OrderedDict": TypingAlias("collections.OrderedDict", True), |
| 33 | + "typing.Counter": TypingAlias("collections.Counter", True), |
| 34 | + "typing.ChainMap": TypingAlias("collections.ChainMap", True), |
| 35 | + "typing.Awaitable": TypingAlias("collections.abc.Awaitable", True), |
| 36 | + "typing.Coroutine": TypingAlias("collections.abc.Coroutine", True), |
| 37 | + "typing.AsyncIterable": TypingAlias("collections.abc.AsyncIterable", True), |
| 38 | + "typing.AsyncIterator": TypingAlias("collections.abc.AsyncIterator", True), |
| 39 | + "typing.AsyncGenerator": TypingAlias("collections.abc.AsyncGenerator", True), |
| 40 | + "typing.Iterable": TypingAlias("collections.abc.Iterable", True), |
| 41 | + "typing.Iterator": TypingAlias("collections.abc.Iterator", True), |
| 42 | + "typing.Generator": TypingAlias("collections.abc.Generator", True), |
| 43 | + "typing.Reversible": TypingAlias("collections.abc.Reversible", True), |
| 44 | + "typing.Container": TypingAlias("collections.abc.Container", True), |
| 45 | + "typing.Collection": TypingAlias("collections.abc.Collection", True), |
| 46 | + "typing.Callable": TypingAlias("collections.abc.Callable", True), |
| 47 | + "typing.AbstractSet": TypingAlias("collections.abc.Set", False), |
| 48 | + "typing.MutableSet": TypingAlias("collections.abc.MutableSet", True), |
| 49 | + "typing.Mapping": TypingAlias("collections.abc.Mapping", True), |
| 50 | + "typing.MutableMapping": TypingAlias("collections.abc.MutableMapping", True), |
| 51 | + "typing.Sequence": TypingAlias("collections.abc.Sequence", True), |
| 52 | + "typing.MutableSequence": TypingAlias("collections.abc.MutableSequence", True), |
| 53 | + "typing.ByteString": TypingAlias("collections.abc.ByteString", True), |
| 54 | + "typing.MappingView": TypingAlias("collections.abc.MappingView", True), |
| 55 | + "typing.KeysView": TypingAlias("collections.abc.KeysView", True), |
| 56 | + "typing.ItemsView": TypingAlias("collections.abc.ItemsView", True), |
| 57 | + "typing.ValuesView": TypingAlias("collections.abc.ValuesView", True), |
| 58 | + "typing.ContextManager": TypingAlias("contextlib.AbstractContextManager", False), |
| 59 | + "typing.AsyncContextManager": TypingAlias( |
| 60 | + "contextlib.AbstractAsyncContextManager", False |
| 61 | + ), |
| 62 | + "typing.Pattern": TypingAlias("re.Pattern", True), |
| 63 | + "typing.Match": TypingAlias("re.Match", True), |
| 64 | + "typing.Hashable": TypingAlias("collections.abc.Hashable", True), |
| 65 | + "typing.Sized": TypingAlias("collections.abc.Sized", True), |
| 66 | +} |
| 67 | + |
| 68 | +ALIAS_NAMES = frozenset(key.split(".")[1] for key in DEPRECATED_TYPING_ALIASES) |
| 69 | +UNION_NAMES = ("Optional", "Union") |
| 70 | + |
| 71 | + |
| 72 | +class DeprecatedTypingAliasMsg(NamedTuple): |
| 73 | + node: Union[astroid.Name, astroid.Attribute] |
| 74 | + qname: str |
| 75 | + alias: str |
| 76 | + parent_subscript: bool |
| 77 | + |
| 78 | + |
| 79 | +class TypingChecker(BaseChecker): |
| 80 | + """Find issue specifically related to type annotations.""" |
| 81 | + |
| 82 | + __implements__ = (IAstroidChecker,) |
| 83 | + |
| 84 | + name = "typing" |
| 85 | + priority = -1 |
| 86 | + msgs = { |
| 87 | + "W6001": ( |
| 88 | + "'%s' is deprecated, use '%s' instead", |
| 89 | + "deprecated-typing-alias", |
| 90 | + "Emitted when a deprecated typing alias is used.", |
| 91 | + ), |
| 92 | + "R6002": ( |
| 93 | + "'%s' will be deprecated with PY39, consider using '%s' instead%s", |
| 94 | + "consider-using-alias", |
| 95 | + "Only emitted if 'runtime-typing=no' and a deprecated " |
| 96 | + "typing alias is used in a type annotation context in " |
| 97 | + "Python 3.7 or 3.8.", |
| 98 | + ), |
| 99 | + "R6003": ( |
| 100 | + "Consider using alternative Union syntax instead of '%s'%s", |
| 101 | + "consider-alternative-union-syntax", |
| 102 | + "Emitted when 'typing.Union' or 'typing.Optional' is used " |
| 103 | + "instead of the alternative Union syntax 'int | None'.", |
| 104 | + ), |
| 105 | + } |
| 106 | + options = ( |
| 107 | + ( |
| 108 | + "py-version", |
| 109 | + { |
| 110 | + "default": (3, 7), |
| 111 | + "type": "py_version", |
| 112 | + "metavar": "<py_version>", |
| 113 | + "help": ( |
| 114 | + "Min Python version to use for typing related checks, " |
| 115 | + "e.g. ``3.7``. This should be equal to the min supported Python " |
| 116 | + "version of the project." |
| 117 | + ), |
| 118 | + }, |
| 119 | + ), |
| 120 | + ( |
| 121 | + "runtime-typing", |
| 122 | + { |
| 123 | + "default": True, |
| 124 | + "type": "yn", |
| 125 | + "metavar": "<y_or_n>", |
| 126 | + "help": ( |
| 127 | + "Set to ``no`` if the app / libary does NOT need to " |
| 128 | + "support runtime introspection of type " |
| 129 | + "annotations. Only applies to Python version " |
| 130 | + "3.7 - 3.9" |
| 131 | + ), |
| 132 | + }, |
| 133 | + ), |
| 134 | + ) |
| 135 | + |
| 136 | + def __init__(self, linter: PyLinter) -> None: |
| 137 | + """Initialize checker instance.""" |
| 138 | + super().__init__(linter=linter) |
| 139 | + self._alias_name_collisions: Set[str] = set() |
| 140 | + self._consider_using_alias_msgs: List[DeprecatedTypingAliasMsg] = [] |
| 141 | + |
| 142 | + @lru_cache() |
| 143 | + def _py37_plus(self) -> bool: |
| 144 | + return self.config.py_version >= (3, 7) |
| 145 | + |
| 146 | + @lru_cache() |
| 147 | + def _py39_plus(self) -> bool: |
| 148 | + return self.config.py_version >= (3, 9) |
| 149 | + |
| 150 | + @lru_cache() |
| 151 | + def _py310_plus(self) -> bool: |
| 152 | + return self.config.py_version >= (3, 10) |
| 153 | + |
| 154 | + @lru_cache() |
| 155 | + def _should_check_typing_alias(self) -> bool: |
| 156 | + """The use of type aliases (PEP 585) requires Python 3.9 |
| 157 | + or Python 3.7+ with postponed evaluation. |
| 158 | + """ |
| 159 | + return ( |
| 160 | + self._py39_plus() |
| 161 | + or self._py37_plus() |
| 162 | + and self.config.runtime_typing is False |
| 163 | + ) |
| 164 | + |
| 165 | + @lru_cache() |
| 166 | + def _should_check_alternative_union_syntax(self) -> bool: |
| 167 | + """The use of alternative union syntax (PEP 604) requires Python 3.10 |
| 168 | + or Python 3.7+ with postponed evaluation. |
| 169 | + """ |
| 170 | + return ( |
| 171 | + self._py310_plus() |
| 172 | + or self._py37_plus() |
| 173 | + and self.config.runtime_typing is False |
| 174 | + ) |
| 175 | + |
| 176 | + def _msg_postponed_eval_hint(self, node) -> str: |
| 177 | + """Message hint if postponed evaluation isn't enabled.""" |
| 178 | + if self._py310_plus() or "annotations" in node.root().future_imports: |
| 179 | + return "" |
| 180 | + return ". Add 'from __future__ import annotations' as well" |
| 181 | + |
| 182 | + @check_messages( |
| 183 | + "deprecated-typing-alias", |
| 184 | + "consider-using-alias", |
| 185 | + "consider-alternative-union-syntax", |
| 186 | + ) |
| 187 | + def visit_name(self, node: astroid.Name) -> None: |
| 188 | + if self._should_check_typing_alias() and node.name in ALIAS_NAMES: |
| 189 | + self._check_for_typing_alias(node) |
| 190 | + if self._should_check_alternative_union_syntax() and node.name in UNION_NAMES: |
| 191 | + self._check_for_alternative_union_syntax(node, node.name) |
| 192 | + |
| 193 | + @check_messages( |
| 194 | + "deprecated-typing-alias", |
| 195 | + "consider-using-alias", |
| 196 | + "consider-alternative-union-syntax", |
| 197 | + ) |
| 198 | + def visit_attribute(self, node: astroid.Attribute): |
| 199 | + if self._should_check_typing_alias() and node.attrname in ALIAS_NAMES: |
| 200 | + self._check_for_typing_alias(node) |
| 201 | + if ( |
| 202 | + self._should_check_alternative_union_syntax() |
| 203 | + and node.attrname in UNION_NAMES |
| 204 | + ): |
| 205 | + self._check_for_alternative_union_syntax(node, node.attrname) |
| 206 | + |
| 207 | + def _check_for_alternative_union_syntax( |
| 208 | + self, |
| 209 | + node: Union[astroid.Name, astroid.Attribute], |
| 210 | + name: str, |
| 211 | + ) -> None: |
| 212 | + """Check if alternative union syntax could be used. |
| 213 | +
|
| 214 | + Requires |
| 215 | + - Python 3.10 |
| 216 | + - OR: Python 3.7+ with postponed evaluation in |
| 217 | + a type annotation context |
| 218 | + """ |
| 219 | + inferred = safe_infer(node) |
| 220 | + if not ( |
| 221 | + isinstance(inferred, astroid.FunctionDef) |
| 222 | + and inferred.qname() |
| 223 | + in ( |
| 224 | + "typing.Optional", |
| 225 | + "typing.Union", |
| 226 | + ) |
| 227 | + or isinstance(inferred, astroid.bases.Instance) |
| 228 | + and inferred.qname() == "typing._SpecialForm" |
| 229 | + ): |
| 230 | + return |
| 231 | + if not (self._py310_plus() or is_node_in_type_annotation_context(node)): |
| 232 | + return |
| 233 | + self.add_message( |
| 234 | + "consider-alternative-union-syntax", |
| 235 | + node=node, |
| 236 | + args=(name, self._msg_postponed_eval_hint(node)), |
| 237 | + ) |
| 238 | + |
| 239 | + def _check_for_typing_alias( |
| 240 | + self, |
| 241 | + node: Union[astroid.Name, astroid.Attribute], |
| 242 | + ) -> None: |
| 243 | + """Check if typing alias is depecated or could be replaced. |
| 244 | +
|
| 245 | + Requires |
| 246 | + - Python 3.9 |
| 247 | + - OR: Python 3.7+ with postponed evaluation in |
| 248 | + a type annotation context |
| 249 | +
|
| 250 | + For Python 3.7+: Only emitt message if change doesn't create |
| 251 | + any name collisions, only ever used in a type annotation |
| 252 | + context, and can safely be replaced. |
| 253 | + """ |
| 254 | + inferred = safe_infer(node) |
| 255 | + if not isinstance(inferred, astroid.ClassDef): |
| 256 | + return |
| 257 | + alias = DEPRECATED_TYPING_ALIASES.get(inferred.qname(), None) |
| 258 | + if alias is None: |
| 259 | + return |
| 260 | + |
| 261 | + if self._py39_plus(): |
| 262 | + self.add_message( |
| 263 | + "deprecated-typing-alias", |
| 264 | + node=node, |
| 265 | + args=(inferred.qname(), alias.name), |
| 266 | + ) |
| 267 | + return |
| 268 | + |
| 269 | + # For PY37+, check for type annotation context first |
| 270 | + if not is_node_in_type_annotation_context(node) and isinstance( |
| 271 | + node.parent, astroid.Subscript |
| 272 | + ): |
| 273 | + if alias.name_collision is True: |
| 274 | + self._alias_name_collisions.add(inferred.qname()) |
| 275 | + return |
| 276 | + self._consider_using_alias_msgs.append( |
| 277 | + DeprecatedTypingAliasMsg( |
| 278 | + node, |
| 279 | + inferred.qname(), |
| 280 | + alias.name, |
| 281 | + isinstance(node.parent, astroid.Subscript), |
| 282 | + ) |
| 283 | + ) |
| 284 | + |
| 285 | + @check_messages("consider-using-alias") |
| 286 | + def leave_module(self, node: astroid.Module) -> None: |
| 287 | + """After parsing of module is complete, add messages for |
| 288 | + 'consider-using-alias' check. Make sure results are safe |
| 289 | + to recommend / collision free. |
| 290 | + """ |
| 291 | + if self._py37_plus() and not self._py39_plus(): |
| 292 | + msg_future_import = self._msg_postponed_eval_hint(node) |
| 293 | + while True: |
| 294 | + try: |
| 295 | + msg = self._consider_using_alias_msgs.pop(0) |
| 296 | + except IndexError: |
| 297 | + break |
| 298 | + if msg.qname in self._alias_name_collisions: |
| 299 | + continue |
| 300 | + self.add_message( |
| 301 | + "consider-using-alias", |
| 302 | + node=msg.node, |
| 303 | + args=( |
| 304 | + msg.qname, |
| 305 | + msg.alias, |
| 306 | + msg_future_import if msg.parent_subscript else "", |
| 307 | + ), |
| 308 | + ) |
| 309 | + # Clear all module cache variables |
| 310 | + self._alias_name_collisions.clear() |
| 311 | + self._consider_using_alias_msgs.clear() |
| 312 | + |
| 313 | + |
| 314 | +def register(linter: PyLinter) -> None: |
| 315 | + linter.register_checker(TypingChecker(linter)) |
0 commit comments