diff options
author | Marc Mueller <30130371+cdce8p@users.noreply.github.com> | 2021-02-02 02:37:10 +0100 |
---|---|---|
committer | Pierre Sassoulas <pierre.sassoulas@gmail.com> | 2021-02-15 15:11:25 +0100 |
commit | b6edb600bd044b658f2efad3303f3956859577c9 (patch) | |
tree | 64456c87ce89152370008facc5729ab57b7b818c | |
parent | 005cf2f317144f6503cfc8d00fbe6a201d0a8b8f (diff) | |
download | pylint-git-b6edb600bd044b658f2efad3303f3956859577c9.tar.gz |
Add support for pep585 with postponed evaulation
-rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | pylint/checkers/typecheck.py | 2 | ||||
-rw-r--r-- | pylint/checkers/utils.py | 87 | ||||
-rw-r--r-- | tests/functional/p/postponed_evaluation_pep585.py | 86 | ||||
-rw-r--r-- | tests/functional/p/postponed_evaluation_pep585.rc | 3 | ||||
-rw-r--r-- | tests/functional/p/postponed_evaluation_pep585.txt | 8 | ||||
-rw-r--r-- | tests/functional/p/postponed_evaluation_pep585_error.py | 85 | ||||
-rw-r--r-- | tests/functional/p/postponed_evaluation_pep585_error.rc | 3 | ||||
-rw-r--r-- | tests/functional/p/postponed_evaluation_pep585_error.txt | 20 | ||||
-rw-r--r-- | tests/functional/p/postponed_evaluation_pep585_py39.py | 80 | ||||
-rw-r--r-- | tests/functional/p/postponed_evaluation_pep585_py39.rc | 2 |
12 files changed, 377 insertions, 5 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 779ee86f1..81b1008bb 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -439,3 +439,5 @@ contributors: * Logan Miller (komodo472): contributor * Matthew Suozzo: contributor + +* Marc Mueller (cdce8p): contributor @@ -31,6 +31,10 @@ Pylint's ChangeLog * Drop support for Python 3.5 +* Add support for pep585 with postponed evaluation + + Closes #3320 + What's New in Pylint 2.6.1? =========================== Release date: TBA diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 0c6a5cbf2..2b39293ec 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1732,7 +1732,7 @@ accessed. Python regular expressions are accepted.", return # It would be better to handle function # decorators, but let's start slow. - if not supported_protocol(inferred): + if not supported_protocol(inferred, node): self.add_message(msg, args=node.value.as_string(), node=node.value) @check_messages("dict-items-missing-iter") diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 06606bda2..f9944b20d 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -51,7 +51,18 @@ import numbers import re import string from functools import lru_cache, partial -from typing import Callable, Dict, Iterable, List, Match, Optional, Set, Tuple, Union +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Match, + Optional, + Set, + Tuple, + Union, +) import _string import astroid @@ -215,6 +226,49 @@ SPECIAL_METHODS_PARAMS = { } PYMETHODS = set(SPECIAL_METHODS_PARAMS) +SUBSCRIPTABLE_CLASSES_PEP585 = frozenset( + ( + "tuple", + "list", + "dict", + "set", + "frozenset", + "type", + "deque", # collections + "defaultdict", + "OrderedDict", + "Counter", + "ChainMap", + "Awaitable", # collections.abc + "Coroutine", + "AsyncIterable", + "AsyncIterator", + "AsyncGenerator", + "Iterable", + "Iterator", + "Generator", + "Reversible", + "Container", + "Collection", + "Callable", + "Set # typing.AbstractSet", + "MutableSet", + "Mapping", + "MutableMapping", + "Sequence", + "MutableSequence", + "ByteString", + "MappingView", + "KeysView", + "ItemsView", + "ValuesView", + "AbstractContextManager", # contextlib + "AbstractAsyncContextManager", + "Pattern", # re + "Match", + ) +) + class NoSuchArgumentError(Exception): pass @@ -1107,18 +1161,22 @@ def supports_membership_test(value: astroid.node_classes.NodeNG) -> bool: return supported or is_iterable(value) -def supports_getitem(value: astroid.node_classes.NodeNG) -> bool: +def supports_getitem( + value: astroid.node_classes.NodeNG, node: astroid.node_classes.NodeNG +) -> bool: if isinstance(value, astroid.ClassDef): if _supports_protocol_method(value, CLASS_GETITEM_METHOD): return True + if is_class_subscriptable_pep585_with_postponed_evaluation_enabled(value, node): + return True return _supports_protocol(value, _supports_getitem_protocol) -def supports_setitem(value: astroid.node_classes.NodeNG) -> bool: +def supports_setitem(value: astroid.node_classes.NodeNG, *_: Any) -> bool: return _supports_protocol(value, _supports_setitem_protocol) -def supports_delitem(value: astroid.node_classes.NodeNG) -> bool: +def supports_delitem(value: astroid.node_classes.NodeNG, *_: Any) -> bool: return _supports_protocol(value, _supports_delitem_protocol) @@ -1273,6 +1331,27 @@ def is_postponed_evaluation_enabled(node: astroid.node_classes.NodeNG) -> bool: return "annotations" in module.future_imports +def is_class_subscriptable_pep585_with_postponed_evaluation_enabled( + value: astroid.ClassDef, node: astroid.node_classes.NodeNG +) -> bool: + """Check if class is subscriptable with PEP 585 and + postponed evaluation enabled. + """ + if not is_postponed_evaluation_enabled(node): + return False + + if not isinstance( + node.parent, (astroid.AnnAssign, astroid.Arguments, astroid.FunctionDef) + ): + return False + if value.name in SUBSCRIPTABLE_CLASSES_PEP585: + return True + for name in value.basenames: + if name in SUBSCRIPTABLE_CLASSES_PEP585: + return True + return False + + def is_subclass_of(child: astroid.ClassDef, parent: astroid.ClassDef) -> bool: """ Check if first node is a subclass of second node. diff --git a/tests/functional/p/postponed_evaluation_pep585.py b/tests/functional/p/postponed_evaluation_pep585.py new file mode 100644 index 000000000..a91d5995b --- /dev/null +++ b/tests/functional/p/postponed_evaluation_pep585.py @@ -0,0 +1,86 @@ +"""Test PEP 585 in combination with postponed evaluation PEP 563. + +This check requires Python 3.7 or 3.8! +Testing with 3.8 only, to support TypedDict. +""" +# pylint: disable=missing-docstring,unused-argument,unused-import,too-few-public-methods,invalid-name,inherit-non-class +from __future__ import annotations +import collections +import dataclasses +import typing +from typing import NamedTuple, TypedDict +from dataclasses import dataclass + + +AliasInvalid = list[int] # [unsubscriptable-object] + +class CustomIntList(typing.List[int]): + pass + +class CustomIntListError(list[int]): # [unsubscriptable-object] + pass + +cast_variable = [1, 2, 3] +cast_variable = typing.cast(list[int], cast_variable) # [unsubscriptable-object] + +T = typing.TypeVar("T", list[int], str) # [unsubscriptable-object] + +(lambda x: 2)(list[int]) # [unsubscriptable-object] + + +# Check typing.NamedTuple +CustomNamedTuple = typing.NamedTuple( + "CustomNamedTuple", [("my_var", list[int])]) # [unsubscriptable-object] + +class CustomNamedTuple2(NamedTuple): + my_var: list[int] + +class CustomNamedTuple3(typing.NamedTuple): + my_var: list[int] + + +# Check typing.TypedDict +CustomTypedDict = TypedDict("CustomTypedDict", my_var=list[int]) # [unsubscriptable-object] + +CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": list[int]}) # [unsubscriptable-object] + +class CustomTypedDict3(TypedDict): + my_var: list[int] + +class CustomTypedDict4(typing.TypedDict): + my_var: list[int] + + +# Check dataclasses +def my_decorator(*args, **kwargs): + def wraps(*args, **kwargs): + pass + return wraps + +@dataclass +class CustomDataClass: + my_var: list[int] + +@dataclasses.dataclass +class CustomDataClass2: + my_var: list[int] + +@dataclass() +class CustomDataClass3: + my_var: list[int] + +@my_decorator +@dataclasses.dataclass +class CustomDataClass4: + my_var: list[int] + + +# Allowed use cases +var1: set[int] +var2: collections.OrderedDict[str, int] + +def func(arg: list[int]): + pass + +def func2() -> list[int]: + pass diff --git a/tests/functional/p/postponed_evaluation_pep585.rc b/tests/functional/p/postponed_evaluation_pep585.rc new file mode 100644 index 000000000..35b185fdb --- /dev/null +++ b/tests/functional/p/postponed_evaluation_pep585.rc @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=3.8 +max_pyver=3.9 diff --git a/tests/functional/p/postponed_evaluation_pep585.txt b/tests/functional/p/postponed_evaluation_pep585.txt new file mode 100644 index 000000000..45a93dfec --- /dev/null +++ b/tests/functional/p/postponed_evaluation_pep585.txt @@ -0,0 +1,8 @@ +unsubscriptable-object:15:15::Value 'list' is unsubscriptable +unsubscriptable-object:20:25:CustomIntListError:Value 'list' is unsubscriptable +unsubscriptable-object:24:28::Value 'list' is unsubscriptable +unsubscriptable-object:26:24::Value 'list' is unsubscriptable +unsubscriptable-object:28:14::Value 'list' is unsubscriptable +unsubscriptable-object:33:36::Value 'list' is unsubscriptable +unsubscriptable-object:43:54::Value 'list' is unsubscriptable +unsubscriptable-object:45:60::Value 'list' is unsubscriptable diff --git a/tests/functional/p/postponed_evaluation_pep585_error.py b/tests/functional/p/postponed_evaluation_pep585_error.py new file mode 100644 index 000000000..cbffb135c --- /dev/null +++ b/tests/functional/p/postponed_evaluation_pep585_error.py @@ -0,0 +1,85 @@ +"""Test PEP 585 without postponed evaluation. Everything should fail. + +This check requires Python 3.7 or Python 3.8! +Testing with 3.8 only, to support TypedDict. +""" +# pylint: disable=missing-docstring,unused-argument,unused-import,too-few-public-methods,invalid-name,inherit-non-class +import collections +import dataclasses +import typing +from typing import NamedTuple, TypedDict +from dataclasses import dataclass + + +AliasInvalid = list[int] # [unsubscriptable-object] + +class CustomIntList(typing.List[int]): + pass + +class CustomIntListError(list[int]): # [unsubscriptable-object] + pass + +cast_variable = [1, 2, 3] +cast_variable = typing.cast(list[int], cast_variable) # [unsubscriptable-object] + +T = typing.TypeVar("T", list[int], str) # [unsubscriptable-object] + +(lambda x: 2)(list[int]) # [unsubscriptable-object] + + +# Check typing.NamedTuple +CustomNamedTuple = typing.NamedTuple( + "CustomNamedTuple", [("my_var", list[int])]) # [unsubscriptable-object] + +class CustomNamedTuple2(NamedTuple): + my_var: list[int] # [unsubscriptable-object] + +class CustomNamedTuple3(typing.NamedTuple): + my_var: list[int] # [unsubscriptable-object] + + +# Check typing.TypedDict +CustomTypedDict = TypedDict("CustomTypedDict", my_var=list[int]) # [unsubscriptable-object] + +CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": list[int]}) # [unsubscriptable-object] + +class CustomTypedDict3(TypedDict): + my_var: list[int] # [unsubscriptable-object] + +class CustomTypedDict4(typing.TypedDict): + my_var: list[int] # [unsubscriptable-object] + + +# Check dataclasses +def my_decorator(*args, **kwargs): + def wraps(*args, **kwargs): + pass + return wraps + +@dataclass +class CustomDataClass: + my_var: list[int] # [unsubscriptable-object] + +@dataclasses.dataclass +class CustomDataClass2: + my_var: list[int] # [unsubscriptable-object] + +@dataclass() +class CustomDataClass3: + my_var: list[int] # [unsubscriptable-object] + +@my_decorator +@dataclasses.dataclass +class CustomDataClass4: + my_var: list[int] # [unsubscriptable-object] + + + +var1: set[int] # [unsubscriptable-object] +var2: collections.OrderedDict[str, int] # [unsubscriptable-object] + +def func(arg: list[int]): # [unsubscriptable-object] + pass + +def func2() -> list[int]: # [unsubscriptable-object] + pass diff --git a/tests/functional/p/postponed_evaluation_pep585_error.rc b/tests/functional/p/postponed_evaluation_pep585_error.rc new file mode 100644 index 000000000..35b185fdb --- /dev/null +++ b/tests/functional/p/postponed_evaluation_pep585_error.rc @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=3.8 +max_pyver=3.9 diff --git a/tests/functional/p/postponed_evaluation_pep585_error.txt b/tests/functional/p/postponed_evaluation_pep585_error.txt new file mode 100644 index 000000000..b04634142 --- /dev/null +++ b/tests/functional/p/postponed_evaluation_pep585_error.txt @@ -0,0 +1,20 @@ +unsubscriptable-object:14:15::Value 'list' is unsubscriptable +unsubscriptable-object:19:25:CustomIntListError:Value 'list' is unsubscriptable +unsubscriptable-object:23:28::Value 'list' is unsubscriptable +unsubscriptable-object:25:24::Value 'list' is unsubscriptable +unsubscriptable-object:27:14::Value 'list' is unsubscriptable +unsubscriptable-object:32:36::Value 'list' is unsubscriptable +unsubscriptable-object:35:12:CustomNamedTuple2:Value 'list' is unsubscriptable +unsubscriptable-object:38:12:CustomNamedTuple3:Value 'list' is unsubscriptable +unsubscriptable-object:42:54::Value 'list' is unsubscriptable +unsubscriptable-object:44:60::Value 'list' is unsubscriptable +unsubscriptable-object:47:12:CustomTypedDict3:Value 'list' is unsubscriptable +unsubscriptable-object:50:12:CustomTypedDict4:Value 'list' is unsubscriptable +unsubscriptable-object:61:12:CustomDataClass:Value 'list' is unsubscriptable +unsubscriptable-object:65:12:CustomDataClass2:Value 'list' is unsubscriptable +unsubscriptable-object:69:12:CustomDataClass3:Value 'list' is unsubscriptable +unsubscriptable-object:74:12:CustomDataClass4:Value 'list' is unsubscriptable +unsubscriptable-object:78:6::Value 'set' is unsubscriptable +unsubscriptable-object:79:6::Value 'collections.OrderedDict' is unsubscriptable +unsubscriptable-object:81:14:func:Value 'list' is unsubscriptable +unsubscriptable-object:84:15:func2:Value 'list' is unsubscriptable diff --git a/tests/functional/p/postponed_evaluation_pep585_py39.py b/tests/functional/p/postponed_evaluation_pep585_py39.py new file mode 100644 index 000000000..bcc991292 --- /dev/null +++ b/tests/functional/p/postponed_evaluation_pep585_py39.py @@ -0,0 +1,80 @@ +"""Test PEP 585 works as expected, starting with Python 3.9""" +# pylint: disable=missing-docstring,unused-argument,unused-import,too-few-public-methods,invalid-name,inherit-non-class +import collections +import dataclasses +import typing +from typing import NamedTuple, TypedDict +from dataclasses import dataclass + + +AliasValid = list[int] + +class CustomIntList(typing.List[int]): + pass + +class CustomIntListError(list[int]): + pass + +cast_variable = [1, 2, 3] +cast_variable = typing.cast(list[int], cast_variable) + +T = typing.TypeVar("T", list[int], str) + +(lambda x: 2)(list[int]) + + +# Check typing.NamedTuple +CustomNamedTuple = typing.NamedTuple( + "CustomNamedTuple", [("my_var", list[int])]) + +class CustomNamedTuple2(NamedTuple): + my_var: list[int] + +class CustomNamedTuple3(typing.NamedTuple): + my_var: list[int] + + +# Check typing.TypedDict +CustomTypedDict = TypedDict("CustomTypedDict", my_var=list[int]) + +CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": list[int]}) + +class CustomTypedDict3(TypedDict): + my_var: list[int] + +class CustomTypedDict4(typing.TypedDict): + my_var: list[int] + + +# Check dataclasses +def my_decorator(*args, **kwargs): + def wraps(*args, **kwargs): + pass + return wraps + +@dataclass +class CustomDataClass: + my_var: list[int] + +@dataclasses.dataclass +class CustomDataClass2: + my_var: list[int] + +@dataclass() +class CustomDataClass3: + my_var: list[int] + +@my_decorator +@dataclasses.dataclass +class CustomDataClass4: + my_var: list[int] + + +var1: set[int] +var2: collections.OrderedDict[str, int] + +def func(arg: list[int]): + pass + +def func2() -> list[int]: + pass diff --git a/tests/functional/p/postponed_evaluation_pep585_py39.rc b/tests/functional/p/postponed_evaluation_pep585_py39.rc new file mode 100644 index 000000000..16b75eea7 --- /dev/null +++ b/tests/functional/p/postponed_evaluation_pep585_py39.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.9 |