diff --git a/ChangeLog b/ChangeLog index 5bfa2f6856..76c15f0f38 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,10 @@ What's New in astroid 2.9.1? ============================ Release date: TBA +* ``NodeNG.frame()`` and ``NodeNG.statement()`` will start raising ``ParentMissingError`` + instead of ``AttributeError`` in astroid 3.0. This behaviour can already be triggered + by passing ``future=True`` to a ``frame()`` or ``statement()`` call. + * Prefer the module loader get_source() method in AstroidBuilder's module_build() when possible to avoid assumptions about source code being available on a filesystem. Otherwise the source cannot diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index 76b0669720..4e33bb5d1a 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -65,6 +65,7 @@ from typing_extensions import Literal if TYPE_CHECKING: + from astroid import nodes from astroid.nodes import LocalsDictNodeNG @@ -4795,7 +4796,9 @@ def postinit(self, target: NodeNG, value: NodeNG) -> None: See astroid/protocols.py for actual implementation. """ - def frame(self): + def frame( + self, *, future: Literal[None, True] = None + ) -> Union["nodes.FunctionDef", "nodes.Module", "nodes.ClassDef", "nodes.Lambda"]: """The first parent frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, diff --git a/astroid/nodes/node_ng.py b/astroid/nodes/node_ng.py index 5eb6d1985c..3c26f3407a 100644 --- a/astroid/nodes/node_ng.py +++ b/astroid/nodes/node_ng.py @@ -314,7 +314,7 @@ def statement( return self.parent.statement(future=future) def frame( - self, + self, *, future: Literal[None, True] = None ) -> Union["nodes.FunctionDef", "nodes.Module", "nodes.ClassDef", "nodes.Lambda"]: """The first parent frame node. @@ -323,7 +323,19 @@ def frame( :returns: The first parent frame node. """ - return self.parent.frame() + if self.parent is None: + if future: + raise ParentMissingError(target=self) + warnings.warn( + "In astroid 3.0.0 NodeNG.frame() will return either a Frame node, " + "or raise ParentMissingError. AttributeError will no longer be raised. " + "This behaviour can already be triggered " + "by passing 'future=True' to a frame() call.", + DeprecationWarning, + ) + raise AttributeError(f"{self} object has no attribute 'parent'") + + return self.parent.frame(future=future) def scope(self) -> "nodes.LocalsDictNodeNG": """The first parent node defining a new scope. diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index c045c99e1d..f2720c4cce 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -859,7 +859,7 @@ def bool_value(self, context=None): def get_children(self): yield from self.body - def frame(self: T) -> T: + def frame(self: T, *, future: Literal[None, True] = None) -> T: """The node's frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, @@ -1474,7 +1474,7 @@ def get_children(self): yield self.args yield self.body - def frame(self: T) -> T: + def frame(self: T, *, future: Literal[None, True] = None) -> T: """The node's frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, @@ -2004,7 +2004,7 @@ def scope_lookup(self, node, name, offset=0): return self, [frame] return super().scope_lookup(node, name, offset) - def frame(self: T) -> T: + def frame(self: T, *, future: Literal[None, True] = None) -> T: """The node's frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, @@ -3251,7 +3251,7 @@ def _get_assign_nodes(self): ) return list(itertools.chain.from_iterable(children_assign_nodes)) - def frame(self: T) -> T: + def frame(self: T, *, future: Literal[None, True] = None) -> T: """The node's frame node. A frame node is a :class:`Module`, :class:`FunctionDef`, diff --git a/tests/unittest_nodes.py b/tests/unittest_nodes.py index 68e2b33561..aab8c5a123 100644 --- a/tests/unittest_nodes.py +++ b/tests/unittest_nodes.py @@ -55,6 +55,7 @@ AstroidBuildingError, AstroidSyntaxError, AttributeInferenceError, + ParentMissingError, StatementMissing, ) from astroid.nodes.node_classes import ( @@ -641,6 +642,13 @@ def _test(self, value: Any) -> None: with self.assertRaises(StatementMissing): node.statement(future=True) + with self.assertRaises(AttributeError): + with pytest.warns(DeprecationWarning) as records: + node.frame() + assert len(records) == 1 + with self.assertRaises(ParentMissingError): + node.frame(future=True) + def test_none(self) -> None: self._test(None)