Skip to content

Add support for pep585 with postponed evaluation #4059

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,5 @@ contributors:
* Logan Miller (komodo472): contributor

* Matthew Suozzo: contributor

* Marc Mueller (cdce8p): contributor
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pylint/checkers/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -1732,7 +1732,7 @@ def visit_subscript(self, node):
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")
Expand Down
87 changes: 83 additions & 4 deletions pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,18 @@
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
Expand Down Expand Up @@ -215,6 +226,49 @@
}
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
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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.
Expand Down
86 changes: 86 additions & 0 deletions tests/functional/p/postponed_evaluation_pep585.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/functional/p/postponed_evaluation_pep585.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[testoptions]
min_pyver=3.8
max_pyver=3.9
8 changes: 8 additions & 0 deletions tests/functional/p/postponed_evaluation_pep585.txt
Original file line number Diff line number Diff line change
@@ -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
85 changes: 85 additions & 0 deletions tests/functional/p/postponed_evaluation_pep585_error.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/functional/p/postponed_evaluation_pep585_error.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[testoptions]
min_pyver=3.8
max_pyver=3.9
20 changes: 20 additions & 0 deletions tests/functional/p/postponed_evaluation_pep585_error.txt
Original file line number Diff line number Diff line change
@@ -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
Loading