diff --git a/astroid/exceptions.py b/astroid/exceptions.py index 60220f7af..260657c36 100644 --- a/astroid/exceptions.py +++ b/astroid/exceptions.py @@ -14,8 +14,13 @@ """this module contains exceptions used in the astroid library """ +from typing import TYPE_CHECKING + from astroid import util +if TYPE_CHECKING: + from astroid import nodes + __all__ = ( "AstroidBuildingError", "AstroidBuildingException", @@ -100,7 +105,7 @@ class TooManyLevelsError(AstroidImportError): def __init__( self, message="Relative import with too many levels " "({level}) for module {name!r}", - **kws + **kws, ): super().__init__(message, **kws) @@ -256,6 +261,18 @@ class InferenceOverwriteError(AstroidError): """ +class ParentMissingError(AstroidError): + """Raised when a node which is expected to have a parent attribute is missing one + + Standard attributes: + target: The node for which the parent lookup failed. + """ + + def __init__(self, target: "nodes.NodeNG") -> None: + self.target = target + super().__init__(message=f"Parent not found on {target!r}.") + + # Backwards-compatibility aliases OperationError = util.BadOperationMessage UnaryOperationError = util.BadUnaryOperationMessage diff --git a/astroid/nodes/__init__.py b/astroid/nodes/__init__.py index 26254a0d0..f04261610 100644 --- a/astroid/nodes/__init__.py +++ b/astroid/nodes/__init__.py @@ -113,6 +113,7 @@ GeneratorExp, Lambda, ListComp, + LocalsDictNodeNG, Module, SetComp, builtin_lookup, @@ -176,6 +177,7 @@ Lambda, List, ListComp, + LocalsDictNodeNG, Match, MatchAs, MatchCase, @@ -268,6 +270,7 @@ "Lambda", "List", "ListComp", + "LocalsDictNodeNG", "Match", "MatchAs", "MatchCase", diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index da4775d8c..c506d23bb 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -40,7 +40,7 @@ import sys import typing from functools import lru_cache -from typing import Callable, Generator, Optional +from typing import TYPE_CHECKING, Callable, Generator, Optional from astroid import decorators, mixins, util from astroid.bases import Instance, _infer_stmts @@ -51,6 +51,7 @@ AstroidTypeError, InferenceError, NoDefault, + ParentMissingError, ) from astroid.manager import AstroidManager from astroid.nodes.const import OP_PRECEDENCE @@ -61,6 +62,9 @@ else: from typing_extensions import Literal +if TYPE_CHECKING: + from astroid.nodes import LocalsDictNodeNG + def _is_const(value): return isinstance(value, tuple(CONST_CLS)) @@ -2016,13 +2020,17 @@ def postinit(self, nodes: typing.List[NodeNG]) -> None: """ self.nodes = nodes - def scope(self): + def scope(self) -> "LocalsDictNodeNG": """The first parent node defining a new scope. + These can be Module, FunctionDef, ClassDef, Lambda, or GeneratorExp nodes. :returns: The first parent scope node. - :rtype: Module or FunctionDef or ClassDef or Lambda or GenExpr """ # skip the function node to go directly to the upper level scope + if not self.parent: + raise ParentMissingError(target=self) + if not self.parent.parent: + raise ParentMissingError(target=self.parent) return self.parent.parent.scope() def get_children(self): diff --git a/astroid/nodes/node_ng.py b/astroid/nodes/node_ng.py index 68a6837e6..5aed3ec40 100644 --- a/astroid/nodes/node_ng.py +++ b/astroid/nodes/node_ng.py @@ -1,14 +1,32 @@ import pprint import typing from functools import singledispatch as _singledispatch -from typing import ClassVar, Iterator, Optional, Tuple, Type, TypeVar, Union, overload +from typing import ( + TYPE_CHECKING, + ClassVar, + Iterator, + Optional, + Tuple, + Type, + TypeVar, + Union, + overload, +) from astroid import decorators, util -from astroid.exceptions import AstroidError, InferenceError, UseInferenceDefault +from astroid.exceptions import ( + AstroidError, + InferenceError, + ParentMissingError, + UseInferenceDefault, +) from astroid.manager import AstroidManager from astroid.nodes.as_string import AsStringVisitor from astroid.nodes.const import OP_PRECEDENCE +if TYPE_CHECKING: + from astroid.nodes import LocalsDictNodeNG + # Types for 'NodeNG.nodes_of_class()' T_Nodes = TypeVar("T_Nodes", bound="NodeNG") T_Nodes2 = TypeVar("T_Nodes2", bound="NodeNG") @@ -251,15 +269,15 @@ def frame(self): """ return self.parent.frame() - def scope(self): + def scope(self) -> "LocalsDictNodeNG": """The first parent node defining a new scope. + These can be Module, FunctionDef, ClassDef, Lambda, or GeneratorExp nodes. :returns: The first parent scope node. - :rtype: Module or FunctionDef or ClassDef or Lambda or GenExpr """ - if self.parent: - return self.parent.scope() - return None + if not self.parent: + raise ParentMissingError(target=self) + return self.parent.scope() def root(self): """Return the root node of the syntax tree. diff --git a/astroid/nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes.py index f9b4a6ed9..d32698207 100644 --- a/astroid/nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes.py @@ -44,7 +44,7 @@ import io import itertools import typing -from typing import List, Optional +from typing import List, Optional, TypeVar from astroid import bases from astroid import decorators as decorators_mod @@ -78,6 +78,8 @@ {"classmethod", "staticmethod", "builtins.classmethod", "builtins.staticmethod"} ) +T = TypeVar("T") + def _c3_merge(sequences, cls, context): """Merges MROs in *sequences* to a single MRO using the C3 algorithm. @@ -238,7 +240,7 @@ def frame(self): """ return self - def scope(self): + def scope(self: T) -> T: """The first parent node defining a new scope. :returns: The first parent scope node. diff --git a/doc/api/astroid.exceptions.rst b/doc/api/astroid.exceptions.rst index bb7b1b194..65abeaf81 100644 --- a/doc/api/astroid.exceptions.rst +++ b/doc/api/astroid.exceptions.rst @@ -30,6 +30,7 @@ Exceptions NameInferenceError NoDefault NotFoundError + ParentMissingError ResolveError SuperArgumentTypeError SuperError diff --git a/doc/api/base_nodes.rst b/doc/api/base_nodes.rst index c721cbb28..7b2d4a502 100644 --- a/doc/api/base_nodes.rst +++ b/doc/api/base_nodes.rst @@ -12,7 +12,7 @@ These are abstract node classes that :ref:`other nodes ` inherit from. astroid.mixins.FilterStmtsMixin astroid.mixins.ImportFromMixin astroid.nodes.ListComp - astroid.nodes.scoped_nodes.LocalsDictNodeNG + astroid.nodes.LocalsDictNodeNG astroid.nodes.node_classes.LookupMixIn astroid.nodes.NodeNG astroid.mixins.ParentAssignTypeMixin @@ -34,7 +34,7 @@ These are abstract node classes that :ref:`other nodes ` inherit from. .. autoclass:: astroid.nodes.ListComp -.. autoclass:: astroid.nodes.scoped_nodes.LocalsDictNodeNG +.. autoclass:: astroid.nodes.LocalsDictNodeNG .. autoclass:: astroid.nodes.node_classes.LookupMixIn diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py index 2ae6ce81b..99007483f 100644 --- a/tests/unittest_builder.py +++ b/tests/unittest_builder.py @@ -551,7 +551,6 @@ def func2(a={}): """ builder.parse(code) nonetype = nodes.const_factory(None) - # pylint: disable=no-member; Infers two potential values self.assertNotIn("custom_attr", nonetype.locals) self.assertNotIn("custom_attr", nonetype.instance_attrs) nonetype = nodes.const_factory({}) diff --git a/tests/unittest_lookup.py b/tests/unittest_lookup.py index 8de88b831..37de3b931 100644 --- a/tests/unittest_lookup.py +++ b/tests/unittest_lookup.py @@ -393,7 +393,6 @@ def test_builtin_lookup(self) -> None: self.assertEqual(len(intstmts), 1) self.assertIsInstance(intstmts[0], nodes.ClassDef) self.assertEqual(intstmts[0].name, "int") - # pylint: disable=no-member; Infers two potential values self.assertIs(intstmts[0], nodes.const_factory(1)._proxied) def test_decorator_arguments_lookup(self) -> None: diff --git a/tests/unittest_nodes.py b/tests/unittest_nodes.py index 9cc946565..45fde769e 100644 --- a/tests/unittest_nodes.py +++ b/tests/unittest_nodes.py @@ -615,7 +615,6 @@ def test_as_string(self) -> None: class ConstNodeTest(unittest.TestCase): def _test(self, value: Any) -> None: node = nodes.const_factory(value) - # pylint: disable=no-member; Infers two potential values self.assertIsInstance(node._proxied, nodes.ClassDef) self.assertEqual(node._proxied.name, value.__class__.__name__) self.assertIs(node.value, value) diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py index 040824695..63d36e748 100644 --- a/tests/unittest_scoped_nodes.py +++ b/tests/unittest_scoped_nodes.py @@ -277,7 +277,7 @@ def test_file_stream_api(self) -> None: path = resources.find("data/all.py") file_build = builder.AstroidBuilder().file_build(path, "all") with self.assertRaises(AttributeError): - # pylint: disable=pointless-statement, no-member + # pylint: disable=pointless-statement file_build.file_stream def test_stream_api(self) -> None: