summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc Mueller <30130371+cdce8p@users.noreply.github.com>2021-02-02 02:37:10 +0100
committerPierre Sassoulas <pierre.sassoulas@gmail.com>2021-02-15 15:11:25 +0100
commitb6edb600bd044b658f2efad3303f3956859577c9 (patch)
tree64456c87ce89152370008facc5729ab57b7b818c
parent005cf2f317144f6503cfc8d00fbe6a201d0a8b8f (diff)
downloadpylint-git-b6edb600bd044b658f2efad3303f3956859577c9.tar.gz
Add support for pep585 with postponed evaulation
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--ChangeLog4
-rw-r--r--pylint/checkers/typecheck.py2
-rw-r--r--pylint/checkers/utils.py87
-rw-r--r--tests/functional/p/postponed_evaluation_pep585.py86
-rw-r--r--tests/functional/p/postponed_evaluation_pep585.rc3
-rw-r--r--tests/functional/p/postponed_evaluation_pep585.txt8
-rw-r--r--tests/functional/p/postponed_evaluation_pep585_error.py85
-rw-r--r--tests/functional/p/postponed_evaluation_pep585_error.rc3
-rw-r--r--tests/functional/p/postponed_evaluation_pep585_error.txt20
-rw-r--r--tests/functional/p/postponed_evaluation_pep585_py39.py80
-rw-r--r--tests/functional/p/postponed_evaluation_pep585_py39.rc2
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
diff --git a/ChangeLog b/ChangeLog
index b21d4986d..0b50cb0e3 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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