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

Fix typing alias #916

Merged
merged 16 commits into from
Feb 28, 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
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really hope all the tests pass now. Was a bit more difficult to fix than I expected.

Copy link
Member Author

@cdce8p cdce8p Feb 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Pierre-Sassoulas Can you reproduce the Travis issue. I unfortunately can't 😞
https://travis-ci.org/github/PyCQA/astroid/jobs/760830892

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm it's strange, on 2391061 I have an error but not the same than for Travis

____ ExceptionModelTest.test_oserror __

self = <tests.unittest_object_model.ExceptionModelTest testMethod=test_oserror>

    def test_oserror(self):
        ast_nodes = builder.extract_node(
            """
        try:
            raise OSError("a")
        except OSError as err:
           err.filename #@
           err.filename2 #@
           err.errno #@
        """
        )
        expected_values = ["", "", 0]
        for node, value in zip(ast_nodes, expected_values):
            inferred = next(node.infer())
>           assert isinstance(inferred, astroid.Const)
E           AssertionError: assert False
E            +  where False = isinstance(Uninferable, <class 'astroid.node_classes.Const'>)
E            +    where <class 'astroid.node_classes.Const'> = astroid.Const

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Should I just change the required version to 3.8?
Since it works with AppVeyor Link, I would assume that this is a Travis issue.

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