From cf1d77bd00c1fdc2d2b125f81500d56c82793269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:24:10 +0100 Subject: [PATCH 1/8] Define ``generators`` on ``ComprehensionScope`` --- astroid/nodes/scoped_nodes/mixin.py | 3 ++ astroid/nodes/scoped_nodes/scoped_nodes.py | 42 +++++++--------------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/astroid/nodes/scoped_nodes/mixin.py b/astroid/nodes/scoped_nodes/mixin.py index a6c23b5c07..1d1146d1d7 100644 --- a/astroid/nodes/scoped_nodes/mixin.py +++ b/astroid/nodes/scoped_nodes/mixin.py @@ -170,3 +170,6 @@ class ComprehensionScope(LocalsDictNodeNG): """Scoping for different types of comprehensions.""" scope_lookup = LocalsDictNodeNG._scope_lookup + + generators: List["nodes.Comprehension"] + """The generators that are looped through.""" diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 07ca28b8b6..5d2ac2d041 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -61,6 +61,9 @@ # pylint: disable-next=ungrouped-imports from astroid.decorators import cachedproperty as cached_property +if TYPE_CHECKING: + from astroid import nodes + ITER_METHODS = ("__iter__", "__getitem__") EXCEPTION_BASE_CLASSES = frozenset({"Exception", "BaseException"}) @@ -677,11 +680,6 @@ class GeneratorExp(ComprehensionScope): :type: NodeNG or None """ - generators = None - """The generators that are looped through. - - :type: list(Comprehension) or None - """ def __init__( self, @@ -724,14 +722,13 @@ def __init__( parent=parent, ) - def postinit(self, elt=None, generators=None): + def postinit(self, elt=None, generators: Optional["nodes.Comprehension"] = None): """Do some setup after initialisation. :param elt: The element that forms the output of the expression. :type elt: NodeNG or None :param generators: The generators that are looped through. - :type generators: list(Comprehension) or None """ self.elt = elt if generators is None: @@ -775,11 +772,6 @@ class DictComp(ComprehensionScope): :type: NodeNG or None """ - generators = None - """The generators that are looped through. - - :type: list(Comprehension) or None - """ def __init__( self, @@ -822,7 +814,9 @@ def __init__( parent=parent, ) - def postinit(self, key=None, value=None, generators=None): + def postinit( + self, key=None, value=None, generators: Optional["nodes.Comprehension"] = None + ): """Do some setup after initialisation. :param key: What produces the keys. @@ -832,7 +826,6 @@ def postinit(self, key=None, value=None, generators=None): :type value: NodeNG or None :param generators: The generators that are looped through. - :type generators: list(Comprehension) or None """ self.key = key self.value = value @@ -873,11 +866,6 @@ class SetComp(ComprehensionScope): :type: NodeNG or None """ - generators = None - """The generators that are looped through. - - :type: list(Comprehension) or None - """ def __init__( self, @@ -920,14 +908,13 @@ def __init__( parent=parent, ) - def postinit(self, elt=None, generators=None): + def postinit(self, elt=None, generators: Optional["nodes.Comprehension"] = None): """Do some setup after initialisation. :param elt: The element that forms the output of the expression. :type elt: NodeNG or None :param generators: The generators that are looped through. - :type generators: list(Comprehension) or None """ self.elt = elt if generators is None: @@ -968,12 +955,6 @@ class ListComp(ComprehensionScope): :type: NodeNG or None """ - generators = None - """The generators that are looped through. - - :type: list(Comprehension) or None - """ - def __init__( self, lineno=None, @@ -997,7 +978,7 @@ def __init__( parent=parent, ) - def postinit(self, elt=None, generators=None): + def postinit(self, elt=None, generators: Optional["nodes.Comprehension"] = None): """Do some setup after initialisation. :param elt: The element that forms the output of the expression. @@ -1007,7 +988,10 @@ def postinit(self, elt=None, generators=None): :type generators: list(Comprehension) or None """ self.elt = elt - self.generators = generators + if generators is None: + self.generators = [] + else: + self.generators = generators def bool_value(self, context=None): """Determine the boolean value of this node. From db2fbaa0b55989f1f84344dd266ec41dc91e0284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:28:13 +0100 Subject: [PATCH 2/8] Define ``pytype`` on ``ComprehensionScope`` --- astroid/nodes/scoped_nodes/mixin.py | 5 +++++ astroid/nodes/scoped_nodes/scoped_nodes.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/astroid/nodes/scoped_nodes/mixin.py b/astroid/nodes/scoped_nodes/mixin.py index 1d1146d1d7..336744cb0c 100644 --- a/astroid/nodes/scoped_nodes/mixin.py +++ b/astroid/nodes/scoped_nodes/mixin.py @@ -4,6 +4,7 @@ """This module contains mixin classes for scoped nodes.""" +import abc from typing import TYPE_CHECKING, Dict, List, TypeVar from astroid.filter_statements import _filter_stmts @@ -173,3 +174,7 @@ class ComprehensionScope(LocalsDictNodeNG): generators: List["nodes.Comprehension"] """The generators that are looped through.""" + + @abc.abstractmethod + def pytype(self) -> str: + pass diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 5d2ac2d041..6c38f8a5d7 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -750,6 +750,9 @@ def get_children(self): yield from self.generators + def pytype(self) -> str: + return "builtins.generator" + class DictComp(ComprehensionScope): """Class representing an :class:`ast.DictComp` node. @@ -849,6 +852,9 @@ def get_children(self): yield from self.generators + def pytype(self) -> str: + return "builtins.dict" + class SetComp(ComprehensionScope): """Class representing an :class:`ast.SetComp` node. @@ -936,6 +942,9 @@ def get_children(self): yield from self.generators + def pytype(self) -> str: + return "builtins.set" + class ListComp(ComprehensionScope): """Class representing an :class:`ast.ListComp` node. @@ -1007,6 +1016,9 @@ def get_children(self): yield from self.generators + def pytype(self) -> str: + return "builtins.list" + def _infer_decorator_callchain(node): """Detect decorator call chaining and see if the end result is a From ab48be34ee77d3905ea4f1d03df39b8e8c494441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:51:30 +0100 Subject: [PATCH 3/8] Make ComprehensionScope infer as self --- astroid/helpers.py | 3 +++ astroid/nodes/scoped_nodes/mixin.py | 13 ++++++++++++- tests/unittest_inference.py | 9 ++++----- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/astroid/helpers.py b/astroid/helpers.py index 527ac1f18d..a94abfd0ad 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -55,6 +55,9 @@ def _object_type(node, context=None): yield _function_type(inferred, builtins) elif isinstance(inferred, scoped_nodes.Module): yield _build_proxy_class("module", builtins) + # TODO: Implement type() lookup for ComprehenscopScope, perhaps through _proxied + elif isinstance(inferred, nodes.ComprehensionScope): + raise InferenceError else: yield inferred._proxied diff --git a/astroid/nodes/scoped_nodes/mixin.py b/astroid/nodes/scoped_nodes/mixin.py index 336744cb0c..6dbf95a968 100644 --- a/astroid/nodes/scoped_nodes/mixin.py +++ b/astroid/nodes/scoped_nodes/mixin.py @@ -5,7 +5,7 @@ """This module contains mixin classes for scoped nodes.""" import abc -from typing import TYPE_CHECKING, Dict, List, TypeVar +from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, TypeVar from astroid.filter_statements import _filter_stmts from astroid.nodes import node_classes, scoped_nodes @@ -13,6 +13,8 @@ if TYPE_CHECKING: from astroid import nodes + from astroid.context import InferenceContext + _T = TypeVar("_T") @@ -175,6 +177,15 @@ class ComprehensionScope(LocalsDictNodeNG): generators: List["nodes.Comprehension"] """The generators that are looped through.""" + def qname(self): + """Get the 'qualified' name of the node.""" + return self.pytype() + + def infer( + self: _T, context: Optional["InferenceContext"] = None, **kwargs: Any + ) -> Iterator[_T]: + yield self + @abc.abstractmethod def pytype(self) -> str: pass diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py index fac44d23ca..a1bc87844a 100644 --- a/tests/unittest_inference.py +++ b/tests/unittest_inference.py @@ -2645,11 +2645,11 @@ def true_value(): klass = module["Class"] self.assertTrue(klass.bool_value()) dict_comp = next(module["dict_comp"].infer()) - self.assertEqual(dict_comp, util.Uninferable) + assert isinstance(dict_comp, nodes.DictComp) set_comp = next(module["set_comp"].infer()) - self.assertEqual(set_comp, util.Uninferable) + assert isinstance(set_comp, nodes.SetComp) list_comp = next(module["list_comp"].infer()) - self.assertEqual(list_comp, util.Uninferable) + assert isinstance(list_comp, nodes.ListComp) lambda_func = next(module["lambda_func"].infer()) self.assertTrue(lambda_func) unbound_method = next(module["unbound_method"].infer()) @@ -4198,8 +4198,7 @@ class Test(Parent): def test_uninferable_type_subscript(self) -> None: node = extract_node("[type for type in [] if type['id']]") - with self.assertRaises(InferenceError): - _ = next(node.infer()) + assert isinstance(node.inferred()[0], nodes.ListComp) class GetattrTest(unittest.TestCase): From 0614091bc9f401d29f3f3657560b04e25f4c0706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:55:24 +0100 Subject: [PATCH 4/8] Add changelog --- ChangeLog | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog b/ChangeLog index 0ecb2593f4..389fedde9d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,10 @@ What's New in astroid 2.12.0? ============================= Release date: TBA +* Infer comprehensions and generators as their respective nodes instead of + as ``Uninferable``. + + Closes #135, #1404 What's New in astroid 2.11.1? From 84c1df3f6c69661f8b3bc29e3495e50467575103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Fri, 25 Mar 2022 16:34:45 +0100 Subject: [PATCH 5/8] Fixes --- astroid/helpers.py | 4 +++- astroid/nodes/scoped_nodes/scoped_nodes.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/astroid/helpers.py b/astroid/helpers.py index a94abfd0ad..26234a1efa 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -7,6 +7,8 @@ """ +from typing import Sequence + from astroid import bases, manager, nodes, raw_building, util from astroid.context import CallContext, InferenceContext from astroid.exceptions import ( @@ -83,7 +85,7 @@ def object_type(node, context=None): def _object_type_is_subclass(obj_type, class_or_seq, context=None): if not isinstance(class_or_seq, (tuple, list)): - class_seq = (class_or_seq,) + class_seq: Sequence = (class_or_seq,) else: class_seq = class_or_seq diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 7a3797fc38..b30b9c767d 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -13,7 +13,7 @@ import sys import typing import warnings -from typing import Dict, List, Optional, Set, TypeVar, Union, overload +from typing import TYPE_CHECKING, Dict, List, Optional, Set, TypeVar, Union, overload from astroid import bases from astroid import decorators as decorators_mod From df71eec1109aab2b3a1d0138658cdae0a57ced46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Fri, 25 Mar 2022 16:38:19 +0100 Subject: [PATCH 6/8] Update astroid/nodes/scoped_nodes/mixin.py --- astroid/nodes/scoped_nodes/mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astroid/nodes/scoped_nodes/mixin.py b/astroid/nodes/scoped_nodes/mixin.py index 6dbf95a968..35b39ded0a 100644 --- a/astroid/nodes/scoped_nodes/mixin.py +++ b/astroid/nodes/scoped_nodes/mixin.py @@ -177,7 +177,7 @@ class ComprehensionScope(LocalsDictNodeNG): generators: List["nodes.Comprehension"] """The generators that are looped through.""" - def qname(self): + def qname(self) -> str: """Get the 'qualified' name of the node.""" return self.pytype() From a30d3788839e51f50aa034e8ae5f247c401677f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sat, 26 Mar 2022 23:04:03 +0100 Subject: [PATCH 7/8] Fix mistake --- astroid/nodes/scoped_nodes/scoped_nodes.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index b30b9c767d..337cea3ec4 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -719,7 +719,9 @@ def __init__( parent=parent, ) - def postinit(self, elt=None, generators: Optional["nodes.Comprehension"] = None): + def postinit( + self, elt=None, generators: Optional[List["nodes.Comprehension"]] = None + ): """Do some setup after initialisation. :param elt: The element that forms the output of the expression. @@ -815,7 +817,10 @@ def __init__( ) def postinit( - self, key=None, value=None, generators: Optional["nodes.Comprehension"] = None + self, + key=None, + value=None, + generators: Optional[List["nodes.Comprehension"]] = None, ): """Do some setup after initialisation. @@ -911,7 +916,9 @@ def __init__( parent=parent, ) - def postinit(self, elt=None, generators: Optional["nodes.Comprehension"] = None): + def postinit( + self, elt=None, generators: Optional[List["nodes.Comprehension"]] = None + ): """Do some setup after initialisation. :param elt: The element that forms the output of the expression. @@ -984,14 +991,15 @@ def __init__( parent=parent, ) - def postinit(self, elt=None, generators: Optional["nodes.Comprehension"] = None): + def postinit( + self, elt=None, generators: Optional[List["nodes.Comprehension"]] = None + ): """Do some setup after initialisation. :param elt: The element that forms the output of the expression. :type elt: NodeNG or None :param generators: The generators that are looped through. - :type generators: list(Comprehension) or None """ self.elt = elt if generators is None: From fc971a9119d0f32a93cdb0bc1eb1c81a8c408694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 4 May 2022 11:10:36 +0200 Subject: [PATCH 8/8] Use `_infer` instead --- astroid/nodes/scoped_nodes/mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astroid/nodes/scoped_nodes/mixin.py b/astroid/nodes/scoped_nodes/mixin.py index da9c194da5..06b7e2894d 100644 --- a/astroid/nodes/scoped_nodes/mixin.py +++ b/astroid/nodes/scoped_nodes/mixin.py @@ -180,7 +180,7 @@ def qname(self) -> str: """Get the 'qualified' name of the node.""" return self.pytype() - def infer( + def _infer( self: _T, context: Optional["InferenceContext"] = None, **kwargs: Any ) -> Iterator[_T]: yield self