Skip to content

Commit

Permalink
Solves "Duplicates found in MROs" false positives. (#905, #916)
Browse files Browse the repository at this point in the history
* Adds inference support for all typing types that are defined through _alias function
* Instead of creating a new class (by the mean of TYPING_TYPE_TEMPLATE) infer the origin class : i.e MutableSet = _alias(collections.MutableSet ...) origin is the class in collections module. Needs to add __getitem method on its metaclass so that is support indexing (MutableSet[T]).
* Enable _alias mocking and testing only if python version is at least 3.7

Co-authored-by: hippo91 <guillaume.peillex@gmail.com>
  • Loading branch information
cdce8p and hippo91 committed Feb 28, 2021
1 parent 8c2a13f commit 181642f
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 0 deletions.
9 changes: 9 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ Release Date: 2021-02-28

* Improve typing.TypedDict inference

* Fix the `Duplicates found in MROs` false positive.

Closes #905
Closes PyCQA/pylint#2717
Closes PyCQA/pylint#3247
Closes PyCQA/pylint#4093
Closes PyCQA/pylint#4131
Closes PyCQA/pylint#4145


What's New in astroid 2.5?
============================
Expand Down
99 changes: 99 additions & 0 deletions astroid/brain/brain_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@
"""Astroid hooks for typing.py support."""
import sys
import typing
from functools import lru_cache

from astroid import (
MANAGER,
UseInferenceDefault,
extract_node,
inference_tip,
node_classes,
nodes,
context,
InferenceError,
)
import astroid

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

TYPING_NAMEDTUPLE_BASENAMES = {"NamedTuple", "typing.NamedTuple"}
Expand Down Expand Up @@ -112,6 +116,98 @@ def infer_typedDict( # pylint: disable=invalid-name
node.root().locals["TypedDict"] = [class_def]


GET_ITEM_TEMPLATE = """
@classmethod
def __getitem__(cls, value):
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:
"""
Returns True if the node corresponds to a call to _alias function.
For example :
MutableSet = _alias(collections.abc.MutableSet, T)
:param node: call node
"""
return (
isinstance(node, nodes.Call)
and isinstance(node.func, nodes.Name)
and node.func.name == "_alias"
and isinstance(node.args[0], nodes.Attribute)
)


def infer_typing_alias(
node: nodes.Call, ctx: context.InferenceContext = None
) -> typing.Optional[node_classes.NodeNG]:
"""
Infers the call to _alias function
:param node: call node
:param context: inference context
"""
if not isinstance(node, nodes.Call):
return None
res = next(node.args[0].infer(context=ctx))

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


MANAGER.register_transform(
nodes.Call,
inference_tip(infer_typing_typevar_or_newtype),
Expand All @@ -125,3 +221,6 @@ def infer_typedDict( # pylint: disable=invalid-name
MANAGER.register_transform(
nodes.FunctionDef, infer_typedDict, _looks_like_typedDict
)

if PY37:
MANAGER.register_transform(nodes.Call, infer_typing_alias, _looks_like_typing_alias)
115 changes: 115 additions & 0 deletions tests/unittest_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@
import astroid.test_utils as test_utils


def assertEqualMro(klass, expected_mro):
"""Check mro names."""
assert [member.name for member in klass.mro()] == expected_mro


class HashlibTest(unittest.TestCase):
def _assert_hashlib_class(self, class_obj):
self.assertIn("update", class_obj)
Expand Down Expand Up @@ -1206,6 +1211,116 @@ class CustomTD(TypedDict):
assert len(typing_module.locals["TypedDict"]) == 1
assert inferred_base == typing_module.locals["TypedDict"][0]

@test_utils.require_version("3.8")
def test_typing_alias_type(self):
"""
Test that the type aliased thanks to typing._alias function are
correctly inferred.
"""

def check_metaclass(node: nodes.ClassDef):
meta = node.metaclass()
assert isinstance(meta, nodes.ClassDef)
assert meta.name == "ABCMeta_typing"
assert "ABCMeta" == meta.basenames[0]
assert meta.locals.get("__getitem__") is not None

abc_meta = next(meta.bases[0].infer())
assert isinstance(abc_meta, nodes.ClassDef)
assert abc_meta.name == "ABCMeta"
assert abc_meta.locals.get("__getitem__") is None

node = builder.extract_node(
"""
from typing import TypeVar, MutableSet
T = TypeVar("T")
MutableSet[T]
class Derived1(MutableSet[T]):
pass
"""
)
inferred = next(node.infer())
check_metaclass(inferred)
assertEqualMro(
inferred,
[
"Derived1",
"MutableSet_typing",
"MutableSet",
"Set",
"Collection",
"Sized",
"Iterable",
"Container",
"object",
],
)

node = builder.extract_node(
"""
import typing
class Derived2(typing.OrderedDict[int, str]):
pass
"""
)
inferred = next(node.infer())
check_metaclass(inferred)
assertEqualMro(
inferred,
[
"Derived2",
"OrderedDict_typing",
"OrderedDict",
"dict",
"object",
],
)

node = builder.extract_node(
"""
import typing
class Derived3(typing.Pattern[str]):
pass
"""
)
inferred = next(node.infer())
check_metaclass(inferred)
assertEqualMro(
inferred,
[
"Derived3",
"Pattern",
"object",
],
)

@test_utils.require_version("3.8")
def test_typing_alias_side_effects(self):
"""Test that typing._alias changes doesn't have unwanted consequences."""
node = builder.extract_node(
"""
import typing
import collections.abc
class Derived(collections.abc.Iterator[int]):
pass
"""
)
inferred = next(node.infer())
assert inferred.metaclass() is None # Should this be ABCMeta?
assertEqualMro(
inferred,
[
"Derived",
# Should this be more?
# "Iterator_typing"?
# "Iterator",
# "object",
],
)


class ReBrainTest(unittest.TestCase):
def test_regex_flags(self):
Expand Down

0 comments on commit 181642f

Please sign in to comment.