Skip to content

Commit b6edb60

Browse files
cdce8pPierre-Sassoulas
authored andcommitted
Add support for pep585 with postponed evaulation
1 parent 005cf2f commit b6edb60

12 files changed

+377
-5
lines changed

CONTRIBUTORS.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,5 @@ contributors:
439439
* Logan Miller (komodo472): contributor
440440

441441
* Matthew Suozzo: contributor
442+
443+
* Marc Mueller (cdce8p): contributor

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Pylint's ChangeLog
3131

3232
* Drop support for Python 3.5
3333

34+
* Add support for pep585 with postponed evaluation
35+
36+
Closes #3320
37+
3438
What's New in Pylint 2.6.1?
3539
===========================
3640
Release date: TBA

pylint/checkers/typecheck.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1732,7 +1732,7 @@ def visit_subscript(self, node):
17321732
return # It would be better to handle function
17331733
# decorators, but let's start slow.
17341734

1735-
if not supported_protocol(inferred):
1735+
if not supported_protocol(inferred, node):
17361736
self.add_message(msg, args=node.value.as_string(), node=node.value)
17371737

17381738
@check_messages("dict-items-missing-iter")

pylint/checkers/utils.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,18 @@
5151
import re
5252
import string
5353
from functools import lru_cache, partial
54-
from typing import Callable, Dict, Iterable, List, Match, Optional, Set, Tuple, Union
54+
from typing import (
55+
Any,
56+
Callable,
57+
Dict,
58+
Iterable,
59+
List,
60+
Match,
61+
Optional,
62+
Set,
63+
Tuple,
64+
Union,
65+
)
5566

5667
import _string
5768
import astroid
@@ -215,6 +226,49 @@
215226
}
216227
PYMETHODS = set(SPECIAL_METHODS_PARAMS)
217228

229+
SUBSCRIPTABLE_CLASSES_PEP585 = frozenset(
230+
(
231+
"tuple",
232+
"list",
233+
"dict",
234+
"set",
235+
"frozenset",
236+
"type",
237+
"deque", # collections
238+
"defaultdict",
239+
"OrderedDict",
240+
"Counter",
241+
"ChainMap",
242+
"Awaitable", # collections.abc
243+
"Coroutine",
244+
"AsyncIterable",
245+
"AsyncIterator",
246+
"AsyncGenerator",
247+
"Iterable",
248+
"Iterator",
249+
"Generator",
250+
"Reversible",
251+
"Container",
252+
"Collection",
253+
"Callable",
254+
"Set # typing.AbstractSet",
255+
"MutableSet",
256+
"Mapping",
257+
"MutableMapping",
258+
"Sequence",
259+
"MutableSequence",
260+
"ByteString",
261+
"MappingView",
262+
"KeysView",
263+
"ItemsView",
264+
"ValuesView",
265+
"AbstractContextManager", # contextlib
266+
"AbstractAsyncContextManager",
267+
"Pattern", # re
268+
"Match",
269+
)
270+
)
271+
218272

219273
class NoSuchArgumentError(Exception):
220274
pass
@@ -1107,18 +1161,22 @@ def supports_membership_test(value: astroid.node_classes.NodeNG) -> bool:
11071161
return supported or is_iterable(value)
11081162

11091163

1110-
def supports_getitem(value: astroid.node_classes.NodeNG) -> bool:
1164+
def supports_getitem(
1165+
value: astroid.node_classes.NodeNG, node: astroid.node_classes.NodeNG
1166+
) -> bool:
11111167
if isinstance(value, astroid.ClassDef):
11121168
if _supports_protocol_method(value, CLASS_GETITEM_METHOD):
11131169
return True
1170+
if is_class_subscriptable_pep585_with_postponed_evaluation_enabled(value, node):
1171+
return True
11141172
return _supports_protocol(value, _supports_getitem_protocol)
11151173

11161174

1117-
def supports_setitem(value: astroid.node_classes.NodeNG) -> bool:
1175+
def supports_setitem(value: astroid.node_classes.NodeNG, *_: Any) -> bool:
11181176
return _supports_protocol(value, _supports_setitem_protocol)
11191177

11201178

1121-
def supports_delitem(value: astroid.node_classes.NodeNG) -> bool:
1179+
def supports_delitem(value: astroid.node_classes.NodeNG, *_: Any) -> bool:
11221180
return _supports_protocol(value, _supports_delitem_protocol)
11231181

11241182

@@ -1273,6 +1331,27 @@ def is_postponed_evaluation_enabled(node: astroid.node_classes.NodeNG) -> bool:
12731331
return "annotations" in module.future_imports
12741332

12751333

1334+
def is_class_subscriptable_pep585_with_postponed_evaluation_enabled(
1335+
value: astroid.ClassDef, node: astroid.node_classes.NodeNG
1336+
) -> bool:
1337+
"""Check if class is subscriptable with PEP 585 and
1338+
postponed evaluation enabled.
1339+
"""
1340+
if not is_postponed_evaluation_enabled(node):
1341+
return False
1342+
1343+
if not isinstance(
1344+
node.parent, (astroid.AnnAssign, astroid.Arguments, astroid.FunctionDef)
1345+
):
1346+
return False
1347+
if value.name in SUBSCRIPTABLE_CLASSES_PEP585:
1348+
return True
1349+
for name in value.basenames:
1350+
if name in SUBSCRIPTABLE_CLASSES_PEP585:
1351+
return True
1352+
return False
1353+
1354+
12761355
def is_subclass_of(child: astroid.ClassDef, parent: astroid.ClassDef) -> bool:
12771356
"""
12781357
Check if first node is a subclass of second node.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Test PEP 585 in combination with postponed evaluation PEP 563.
2+
3+
This check requires Python 3.7 or 3.8!
4+
Testing with 3.8 only, to support TypedDict.
5+
"""
6+
# pylint: disable=missing-docstring,unused-argument,unused-import,too-few-public-methods,invalid-name,inherit-non-class
7+
from __future__ import annotations
8+
import collections
9+
import dataclasses
10+
import typing
11+
from typing import NamedTuple, TypedDict
12+
from dataclasses import dataclass
13+
14+
15+
AliasInvalid = list[int] # [unsubscriptable-object]
16+
17+
class CustomIntList(typing.List[int]):
18+
pass
19+
20+
class CustomIntListError(list[int]): # [unsubscriptable-object]
21+
pass
22+
23+
cast_variable = [1, 2, 3]
24+
cast_variable = typing.cast(list[int], cast_variable) # [unsubscriptable-object]
25+
26+
T = typing.TypeVar("T", list[int], str) # [unsubscriptable-object]
27+
28+
(lambda x: 2)(list[int]) # [unsubscriptable-object]
29+
30+
31+
# Check typing.NamedTuple
32+
CustomNamedTuple = typing.NamedTuple(
33+
"CustomNamedTuple", [("my_var", list[int])]) # [unsubscriptable-object]
34+
35+
class CustomNamedTuple2(NamedTuple):
36+
my_var: list[int]
37+
38+
class CustomNamedTuple3(typing.NamedTuple):
39+
my_var: list[int]
40+
41+
42+
# Check typing.TypedDict
43+
CustomTypedDict = TypedDict("CustomTypedDict", my_var=list[int]) # [unsubscriptable-object]
44+
45+
CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": list[int]}) # [unsubscriptable-object]
46+
47+
class CustomTypedDict3(TypedDict):
48+
my_var: list[int]
49+
50+
class CustomTypedDict4(typing.TypedDict):
51+
my_var: list[int]
52+
53+
54+
# Check dataclasses
55+
def my_decorator(*args, **kwargs):
56+
def wraps(*args, **kwargs):
57+
pass
58+
return wraps
59+
60+
@dataclass
61+
class CustomDataClass:
62+
my_var: list[int]
63+
64+
@dataclasses.dataclass
65+
class CustomDataClass2:
66+
my_var: list[int]
67+
68+
@dataclass()
69+
class CustomDataClass3:
70+
my_var: list[int]
71+
72+
@my_decorator
73+
@dataclasses.dataclass
74+
class CustomDataClass4:
75+
my_var: list[int]
76+
77+
78+
# Allowed use cases
79+
var1: set[int]
80+
var2: collections.OrderedDict[str, int]
81+
82+
def func(arg: list[int]):
83+
pass
84+
85+
def func2() -> list[int]:
86+
pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[testoptions]
2+
min_pyver=3.8
3+
max_pyver=3.9
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
unsubscriptable-object:15:15::Value 'list' is unsubscriptable
2+
unsubscriptable-object:20:25:CustomIntListError:Value 'list' is unsubscriptable
3+
unsubscriptable-object:24:28::Value 'list' is unsubscriptable
4+
unsubscriptable-object:26:24::Value 'list' is unsubscriptable
5+
unsubscriptable-object:28:14::Value 'list' is unsubscriptable
6+
unsubscriptable-object:33:36::Value 'list' is unsubscriptable
7+
unsubscriptable-object:43:54::Value 'list' is unsubscriptable
8+
unsubscriptable-object:45:60::Value 'list' is unsubscriptable
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Test PEP 585 without postponed evaluation. Everything should fail.
2+
3+
This check requires Python 3.7 or Python 3.8!
4+
Testing with 3.8 only, to support TypedDict.
5+
"""
6+
# pylint: disable=missing-docstring,unused-argument,unused-import,too-few-public-methods,invalid-name,inherit-non-class
7+
import collections
8+
import dataclasses
9+
import typing
10+
from typing import NamedTuple, TypedDict
11+
from dataclasses import dataclass
12+
13+
14+
AliasInvalid = list[int] # [unsubscriptable-object]
15+
16+
class CustomIntList(typing.List[int]):
17+
pass
18+
19+
class CustomIntListError(list[int]): # [unsubscriptable-object]
20+
pass
21+
22+
cast_variable = [1, 2, 3]
23+
cast_variable = typing.cast(list[int], cast_variable) # [unsubscriptable-object]
24+
25+
T = typing.TypeVar("T", list[int], str) # [unsubscriptable-object]
26+
27+
(lambda x: 2)(list[int]) # [unsubscriptable-object]
28+
29+
30+
# Check typing.NamedTuple
31+
CustomNamedTuple = typing.NamedTuple(
32+
"CustomNamedTuple", [("my_var", list[int])]) # [unsubscriptable-object]
33+
34+
class CustomNamedTuple2(NamedTuple):
35+
my_var: list[int] # [unsubscriptable-object]
36+
37+
class CustomNamedTuple3(typing.NamedTuple):
38+
my_var: list[int] # [unsubscriptable-object]
39+
40+
41+
# Check typing.TypedDict
42+
CustomTypedDict = TypedDict("CustomTypedDict", my_var=list[int]) # [unsubscriptable-object]
43+
44+
CustomTypedDict2 = TypedDict("CustomTypedDict2", {"my_var": list[int]}) # [unsubscriptable-object]
45+
46+
class CustomTypedDict3(TypedDict):
47+
my_var: list[int] # [unsubscriptable-object]
48+
49+
class CustomTypedDict4(typing.TypedDict):
50+
my_var: list[int] # [unsubscriptable-object]
51+
52+
53+
# Check dataclasses
54+
def my_decorator(*args, **kwargs):
55+
def wraps(*args, **kwargs):
56+
pass
57+
return wraps
58+
59+
@dataclass
60+
class CustomDataClass:
61+
my_var: list[int] # [unsubscriptable-object]
62+
63+
@dataclasses.dataclass
64+
class CustomDataClass2:
65+
my_var: list[int] # [unsubscriptable-object]
66+
67+
@dataclass()
68+
class CustomDataClass3:
69+
my_var: list[int] # [unsubscriptable-object]
70+
71+
@my_decorator
72+
@dataclasses.dataclass
73+
class CustomDataClass4:
74+
my_var: list[int] # [unsubscriptable-object]
75+
76+
77+
78+
var1: set[int] # [unsubscriptable-object]
79+
var2: collections.OrderedDict[str, int] # [unsubscriptable-object]
80+
81+
def func(arg: list[int]): # [unsubscriptable-object]
82+
pass
83+
84+
def func2() -> list[int]: # [unsubscriptable-object]
85+
pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[testoptions]
2+
min_pyver=3.8
3+
max_pyver=3.9
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
unsubscriptable-object:14:15::Value 'list' is unsubscriptable
2+
unsubscriptable-object:19:25:CustomIntListError:Value 'list' is unsubscriptable
3+
unsubscriptable-object:23:28::Value 'list' is unsubscriptable
4+
unsubscriptable-object:25:24::Value 'list' is unsubscriptable
5+
unsubscriptable-object:27:14::Value 'list' is unsubscriptable
6+
unsubscriptable-object:32:36::Value 'list' is unsubscriptable
7+
unsubscriptable-object:35:12:CustomNamedTuple2:Value 'list' is unsubscriptable
8+
unsubscriptable-object:38:12:CustomNamedTuple3:Value 'list' is unsubscriptable
9+
unsubscriptable-object:42:54::Value 'list' is unsubscriptable
10+
unsubscriptable-object:44:60::Value 'list' is unsubscriptable
11+
unsubscriptable-object:47:12:CustomTypedDict3:Value 'list' is unsubscriptable
12+
unsubscriptable-object:50:12:CustomTypedDict4:Value 'list' is unsubscriptable
13+
unsubscriptable-object:61:12:CustomDataClass:Value 'list' is unsubscriptable
14+
unsubscriptable-object:65:12:CustomDataClass2:Value 'list' is unsubscriptable
15+
unsubscriptable-object:69:12:CustomDataClass3:Value 'list' is unsubscriptable
16+
unsubscriptable-object:74:12:CustomDataClass4:Value 'list' is unsubscriptable
17+
unsubscriptable-object:78:6::Value 'set' is unsubscriptable
18+
unsubscriptable-object:79:6::Value 'collections.OrderedDict' is unsubscriptable
19+
unsubscriptable-object:81:14:func:Value 'list' is unsubscriptable
20+
unsubscriptable-object:84:15:func2:Value 'list' is unsubscriptable

0 commit comments

Comments
 (0)