Skip to content
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

Bug pylint 4206 #921

Merged
merged 61 commits into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
1beb629
Takes into account the fact that inferring subscript when the node is…
hippo91 Mar 13, 2021
4c85bd0
OrderedDict in the collections module inherit from dict which is C co…
hippo91 Mar 13, 2021
99a078b
check_metaclass becomes a static class method because we need it in t…
hippo91 Mar 13, 2021
32d89e2
The brain_typing module does not add anymore _typing suffixed classes…
hippo91 Mar 13, 2021
65a8f88
The OrderedDict class inherits from C coded dict class and thus doesn…
hippo91 Mar 13, 2021
0dee7f8
When trying to inherit from typing.Pattern the REPL says : TypeError…
hippo91 Mar 13, 2021
450fae1
The REPL says that Derived as ABCMeta for metaclass and the mro is De…
hippo91 Mar 13, 2021
eac3ad1
Adds comments
hippo91 Mar 14, 2021
2c711e7
Starting with Python39 some collections of the collections.abc module…
hippo91 Mar 14, 2021
542eb9c
Thanks to __class_getitem__ method there is no need to hack the metac…
hippo91 Mar 14, 2021
f5aef79
SImplifies the inference system for typing objects before python3.9. …
hippo91 Mar 14, 2021
84c576c
check_metaclass_is_abc become global to be shared among different cla…
hippo91 Mar 14, 2021
20ca3a1
Create a test class dedicated to the Collections brain
hippo91 Mar 14, 2021
66c4c7c
Rewrites and adds test
hippo91 Mar 14, 2021
8c7d344
Corrects syntax error
hippo91 Mar 14, 2021
c392180
Deque, defaultdict and OrderedDict are part of the _collections modul…
hippo91 Mar 14, 2021
b0d13d1
Formatting according to black
hippo91 Mar 14, 2021
ab26675
Adds two entries
hippo91 Mar 14, 2021
230fee4
Extends the filter to determine what is subscriptable to include Orde…
hippo91 Mar 14, 2021
79b502c
Formatting according to black
hippo91 Mar 14, 2021
91ce864
Takes into account the fact that inferring subscript when the node is…
hippo91 Mar 13, 2021
136169b
OrderedDict in the collections module inherit from dict which is C co…
hippo91 Mar 13, 2021
4e4e27f
check_metaclass becomes a static class method because we need it in t…
hippo91 Mar 13, 2021
324f02c
The brain_typing module does not add anymore _typing suffixed classes…
hippo91 Mar 13, 2021
3babaf4
The OrderedDict class inherits from C coded dict class and thus doesn…
hippo91 Mar 13, 2021
a1f1dd6
When trying to inherit from typing.Pattern the REPL says : TypeError…
hippo91 Mar 13, 2021
92a9422
The REPL says that Derived as ABCMeta for metaclass and the mro is De…
hippo91 Mar 13, 2021
7afcd62
Adds comments
hippo91 Mar 14, 2021
7b16e00
Starting with Python39 some collections of the collections.abc module…
hippo91 Mar 14, 2021
5b56836
Thanks to __class_getitem__ method there is no need to hack the metac…
hippo91 Mar 14, 2021
6f4328a
SImplifies the inference system for typing objects before python3.9. …
hippo91 Mar 14, 2021
a38e82d
check_metaclass_is_abc become global to be shared among different cla…
hippo91 Mar 14, 2021
121a3db
Create a test class dedicated to the Collections brain
hippo91 Mar 14, 2021
0654a69
Rewrites and adds test
hippo91 Mar 14, 2021
2e382de
Corrects syntax error
hippo91 Mar 14, 2021
86bd75a
Deque, defaultdict and OrderedDict are part of the _collections modul…
hippo91 Mar 14, 2021
eb87d79
Formatting according to black
hippo91 Mar 14, 2021
5d58132
Adds two entries
hippo91 Mar 14, 2021
fa7b6e7
Extends the filter to determine what is subscriptable to include Orde…
hippo91 Mar 14, 2021
d8759f3
Formatting according to black
hippo91 Mar 14, 2021
d8ee527
Takes into account @AWhetter remarks
hippo91 Mar 21, 2021
8ff2893
Deactivates access to __class_getitem__ method
hippo91 Mar 21, 2021
01c7d3c
Merge branch 'bug_pylint_4206' of https://github.com/hippo91/astroid …
hippo91 Mar 27, 2021
0f875c8
OrderedDict appears in typing module with python3.7.2
hippo91 Mar 27, 2021
edf1503
_alias function in the typing module appears with python3.7
hippo91 Mar 27, 2021
3357204
Formatting according to black
hippo91 Mar 27, 2021
38cf33c
_alias function is used also for builtins type and not only for colle…
hippo91 Mar 27, 2021
15bdc78
Adds tests for both builtins type that are subscriptable and typing b…
hippo91 Mar 29, 2021
c5d1c40
No need to handle builtin types in this brain. It is better suited in…
hippo91 Mar 29, 2021
ede0e00
Adds brain to handle builtin types that are subscriptable starting wi…
hippo91 Mar 29, 2021
a09f838
Formatting according to black
hippo91 Mar 29, 2021
d71a7aa
Uses partial function instead of closure in order pylint acceptance t…
hippo91 Mar 31, 2021
1be6d3f
Handling the __class_getitem__ method associated to EmptyNode for bui…
hippo91 Mar 31, 2021
aaa9543
infer_typing_alias has to be an inference_tip to avoid interferences …
hippo91 Mar 31, 2021
56ab112
Formatting
hippo91 Mar 31, 2021
9c42d28
Merge branch 'master' into bug_pylint_4206
hippo91 Mar 31, 2021
80b8ca9
Removes useless code
hippo91 Mar 31, 2021
30b1e79
Adds comment
hippo91 Mar 31, 2021
0bc7d6b
Takes into account @cdce8p remarks
hippo91 Apr 2, 2021
5cd0fcb
Formatting
hippo91 Apr 4, 2021
2d56141
Style changes
cdce8p Apr 6, 2021
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
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ What's New in astroid 2.5.3?
============================
Release Date: TBA

* Takes into account the fact that subscript inferring for a ClassDef may involve __class_getitem__ method

* Reworks the `collections` and `typing` brain so that `pylint`s acceptance tests are fine.

Closes PyCQA/pylint#4206

What's New in astroid 2.5.2?
============================
Expand Down
48 changes: 47 additions & 1 deletion astroid/brain/brain_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def __rmul__(self, other): pass"""
if PY39:
base_deque_class += """
@classmethod
def __class_getitem__(self, item): pass"""
def __class_getitem__(self, item): return cls"""
return base_deque_class


Expand All @@ -77,7 +77,53 @@ def _ordered_dict_mock():
class OrderedDict(dict):
def __reversed__(self): return self[::-1]
def move_to_end(self, key, last=False): pass"""
if PY39:
base_ordered_dict_class += """
@classmethod
def __class_getitem__(cls, item): return cls"""
return base_ordered_dict_class


astroid.register_module_extender(astroid.MANAGER, "collections", _collections_transform)


def _looks_like_subscriptable(node: astroid.nodes.ClassDef) -> bool:
"""
Returns True if the node corresponds to a ClassDef of the Collections.abc module that
supports subscripting

:param node: ClassDef node
"""
if node.qname().startswith("_collections") or node.qname().startswith(
"collections"
):
try:
node.getattr("__class_getitem__")
return True
except astroid.AttributeInferenceError:
pass
return False


CLASS_GET_ITEM_TEMPLATE = """
@classmethod
def __class_getitem__(cls, item):
return cls
"""


def easy_class_getitem_inference(node, context=None):
# Here __class_getitem__ exists but is quite a mess to infer thus
# put an easy inference tip
func_to_add = astroid.extract_node(CLASS_GET_ITEM_TEMPLATE)
node.locals["__class_getitem__"] = [func_to_add]


if PY39:
# Starting with Python39 some objects of the collection module are subscriptable
# thanks to the __class_getitem__ method but the way it is implemented in
# _collection_abc makes it difficult to infer. (We would have to handle AssignName inference in the
# getitem method of the ClassDef class) Instead we put here a mock of the __class_getitem__ method
astroid.MANAGER.register_transform(
astroid.nodes.ClassDef, easy_class_getitem_inference, _looks_like_subscriptable
)
139 changes: 80 additions & 59 deletions astroid/brain/brain_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"""Astroid hooks for typing.py support."""
import sys
import typing
from functools import lru_cache
from functools import partial

from astroid import (
MANAGER,
Expand All @@ -19,6 +19,7 @@
nodes,
context,
InferenceError,
AttributeInferenceError,
)
import astroid

Expand Down Expand Up @@ -116,37 +117,12 @@ def infer_typedDict( # pylint: disable=invalid-name
node.root().locals["TypedDict"] = [class_def]


GET_ITEM_TEMPLATE = """
CLASS_GETITEM_TEMPLATE = """
@classmethod
def __getitem__(cls, value):
def __class_getitem__(cls, item):
return cls
"""

ABC_METACLASS_TEMPLATE = """
from abc import ABCMeta
ABCMeta
"""


@lru_cache()
def create_typing_metaclass():
#  Needs to mock the __getitem__ class method so that
#  MutableSet[T] is acceptable
func_to_add = extract_node(GET_ITEM_TEMPLATE)

abc_meta = next(extract_node(ABC_METACLASS_TEMPLATE).infer())
typing_meta = nodes.ClassDef(
name="ABCMeta_typing",
lineno=abc_meta.lineno,
col_offset=abc_meta.col_offset,
parent=abc_meta.parent,
)
typing_meta.postinit(
bases=[extract_node(ABC_METACLASS_TEMPLATE)], body=[], decorators=None
)
typing_meta.locals["__getitem__"] = [func_to_add]
return typing_meta


def _looks_like_typing_alias(node: nodes.Call) -> bool:
"""
Expand All @@ -161,10 +137,43 @@ def _looks_like_typing_alias(node: nodes.Call) -> bool:
isinstance(node, nodes.Call)
and isinstance(node.func, nodes.Name)
and node.func.name == "_alias"
and isinstance(node.args[0], nodes.Attribute)
and (
# _alias function works also for builtins object such as list and dict
isinstance(node.args[0], nodes.Attribute)
or isinstance(node.args[0], nodes.Name)
and node.args[0].name != "type"
)
)


def _forbid_class_getitem_access(node: nodes.ClassDef) -> None:
"""
Disable the access to __class_getitem__ method for the node in parameters
"""

def full_raiser(origin_func, attr, *args, **kwargs):
"""
Raises an AttributeInferenceError in case of access to __class_getitem__ method.
Otherwise just call origin_func.
"""
if attr == "__class_getitem__":
raise AttributeInferenceError("__class_getitem__ access is not allowed")
else:
return origin_func(attr, *args, **kwargs)

if not isinstance(node, nodes.ClassDef):
raise TypeError("The parameter type should be ClassDef")
try:
node.getattr("__class_getitem__")
# If we are here, then we are sure to modify object that do have __class_getitem__ method (which origin is one the
# protocol defined in collections module) whereas the typing module consider it should not
# We do not want __class_getitem__ to be found in the classdef
partial_raiser = partial(full_raiser, node.getattr)
node.getattr = partial_raiser
except AttributeInferenceError:
pass


def infer_typing_alias(
node: nodes.Call, ctx: context.InferenceContext = None
) -> typing.Optional[node_classes.NodeNG]:
Expand All @@ -174,38 +183,48 @@ def infer_typing_alias(
:param node: call node
:param context: inference context
"""
if not isinstance(node, nodes.Call):
return None
res = next(node.args[0].infer(context=ctx))
hippo91 marked this conversation as resolved.
Show resolved Hide resolved

if res != astroid.Uninferable and isinstance(res, nodes.ClassDef):
class_def = nodes.ClassDef(
name=f"{res.name}_typing",
lineno=0,
col_offset=0,
parent=res.parent,
)
class_def.postinit(
bases=[res],
body=res.body,
decorators=res.decorators,
metaclass=create_typing_metaclass(),
)
return class_def

if len(node.args) == 2 and isinstance(node.args[0], nodes.Attribute):
class_def = nodes.ClassDef(
name=node.args[0].attrname,
lineno=0,
col_offset=0,
parent=node.parent,
)
class_def.postinit(
bases=[], body=[], decorators=None, metaclass=create_typing_metaclass()
)
return class_def

return None
if not PY39:
# Here the node is a typing object which is an alias toward
# the corresponding object of collection.abc module.
# Before python3.9 there is no subscript allowed for any of the collections.abc objects.
# The subscript ability is given through the typing._GenericAlias class
# which is the metaclass of the typing object but not the metaclass of the inferred
# collections.abc object.
# Thus we fake subscript ability of the collections.abc object
# by mocking the existence of a __class_getitem__ method.
# We can not add `__getitem__` method in the metaclass of the object because
# the metaclass is shared by subscriptable and not subscriptable object
maybe_type_var = node.args[1]
if not (
isinstance(maybe_type_var, node_classes.Tuple)
and not maybe_type_var.elts
):
# The typing object is subscriptable if the second argument of the _alias function
# is a TypeVar or a tuple of TypeVar. We could check the type of the second argument but
# it appears that in the typing module the second argument is only TypeVar or a tuple of TypeVar or empty tuple.
# This last value means the type is not Generic and thus cannot be subscriptable
func_to_add = astroid.extract_node(CLASS_GETITEM_TEMPLATE)
res.locals["__class_getitem__"] = [func_to_add]
else:
# If we are here, then we are sure to modify object that do have __class_getitem__ method (which origin is one the
# protocol defined in collections module) whereas the typing module consider it should not
# We do not want __class_getitem__ to be found in the classdef
_forbid_class_getitem_access(res)
else:
# Within python3.9 discrepencies exist between some collections.abc containers that are subscriptable whereas
# corresponding containers in the typing module are not! This is the case at least for ByteString.
# It is far more to complex and dangerous to try to remove __class_getitem__ method from all the ancestors of the
# current class. Instead we raise an AttributeInferenceError if we try to access it.
maybe_type_var = node.args[1]
if isinstance(maybe_type_var, nodes.Const) and maybe_type_var.value == 0:
# Starting with Python39 the _alias function is in fact instantiation of _SpecialGenericAlias class.
# Thus the type is not Generic if the second argument of the call is equal to zero
_forbid_class_getitem_access(res)
return iter([res])
return iter([astroid.Uninferable])


MANAGER.register_transform(
Expand All @@ -223,4 +242,6 @@ def infer_typing_alias(
)

if PY37:
MANAGER.register_transform(nodes.Call, infer_typing_alias, _looks_like_typing_alias)
MANAGER.register_transform(
nodes.Call, inference_tip(infer_typing_alias), _looks_like_typing_alias
)
32 changes: 31 additions & 1 deletion astroid/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
from astroid import util


PY39 = sys.version_info[:2] >= (3, 9)

BUILTINS = builtins.__name__
ITER_METHODS = ("__iter__", "__getitem__")
EXCEPTION_BASE_CLASSES = frozenset({"Exception", "BaseException"})
Expand Down Expand Up @@ -2617,7 +2619,22 @@ def getitem(self, index, context=None):
try:
methods = dunder_lookup.lookup(self, "__getitem__")
except exceptions.AttributeInferenceError as exc:
raise exceptions.AstroidTypeError(node=self, context=context) from exc
if isinstance(self, ClassDef):
# subscripting a class definition may be
# achieved thanks to __class_getitem__ method
# which is a classmethod defined in the class
# that supports subscript and not in the metaclass
try:
methods = self.getattr("__class_getitem__")
# Here it is assumed that the __class_getitem__ node is
# a FunctionDef. One possible improvement would be to deal
# with more generic inference.
except exceptions.AttributeInferenceError:
raise exceptions.AstroidTypeError(
node=self, context=context
) from exc
else:
raise exceptions.AstroidTypeError(node=self, context=context) from exc

method = methods[0]

Expand All @@ -2627,6 +2644,19 @@ def getitem(self, index, context=None):

try:
return next(method.infer_call_result(self, new_context))
except AttributeError:
# Starting with python3.9, builtin types list, dict etc...
# are subscriptable thanks to __class_getitem___ classmethod.
# However in such case the method is bound to an EmptyNode and
# EmptyNode doesn't have infer_call_result method yielding to
# AttributeError
if (
isinstance(method, node_classes.EmptyNode)
and self.name in ("list", "dict", "set", "tuple", "frozenset")
and PY39
):
return self
raise
except exceptions.InferenceError:
return util.Uninferable

Expand Down
Loading