aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>2021-11-07 19:49:18 +0200
committerGitHub <noreply@github.com>2021-11-07 19:49:18 +0200
commitb62f243b16b7f435c8be869577959e95a7927a91 (patch)
treec0bd0b1919741768bd8090ceaef565ff742a9455
parent62f3cfb18eb5e7d27f69f32c7412972d8bfff5fe (diff)
downloadastroid-b62f243b16b7f435c8be869577959e95a7927a91.tar.gz
Add typing and deprecation warnings to ``NodeNG.statement`` (#1217)
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
-rw-r--r--astroid/exceptions.py18
-rw-r--r--astroid/nodes/node_ng.py51
-rw-r--r--astroid/nodes/scoped_nodes.py45
-rw-r--r--tests/unittest_builder.py7
-rw-r--r--tests/unittest_nodes.py7
-rw-r--r--tests/unittest_scoped_nodes.py2
6 files changed, 120 insertions, 10 deletions
diff --git a/astroid/exceptions.py b/astroid/exceptions.py
index 81d97303..b8838023 100644
--- a/astroid/exceptions.py
+++ b/astroid/exceptions.py
@@ -272,6 +272,24 @@ class ParentMissingError(AstroidError):
super().__init__(message=f"Parent not found on {target!r}.")
+class StatementMissing(ParentMissingError):
+ """Raised when a call to node.statement() does not return a node. This is because
+ a node in the chain does not have a parent attribute and therefore does not
+ return a node for statement().
+
+ Standard attributes:
+ target: The node for which the parent lookup failed.
+ """
+
+ def __init__(self, target: "nodes.NodeNG") -> None:
+ # pylint: disable-next=bad-super-call
+ # https://github.com/PyCQA/pylint/issues/2903
+ # https://github.com/PyCQA/astroid/pull/1217#discussion_r744149027
+ super(ParentMissingError, self).__init__(
+ message=f"Statement not found on {target!r}"
+ )
+
+
# Backwards-compatibility aliases
OperationError = util.BadOperationMessage
UnaryOperationError = util.BadUnaryOperationMessage
diff --git a/astroid/nodes/node_ng.py b/astroid/nodes/node_ng.py
index e6d0d50b..6fb242cd 100644
--- a/astroid/nodes/node_ng.py
+++ b/astroid/nodes/node_ng.py
@@ -1,5 +1,7 @@
import pprint
+import sys
import typing
+import warnings
from functools import singledispatch as _singledispatch
from typing import (
TYPE_CHECKING,
@@ -10,6 +12,7 @@ from typing import (
Type,
TypeVar,
Union,
+ cast,
overload,
)
@@ -18,6 +21,7 @@ from astroid.exceptions import (
AstroidError,
InferenceError,
ParentMissingError,
+ StatementMissing,
UseInferenceDefault,
)
from astroid.manager import AstroidManager
@@ -27,6 +31,17 @@ from astroid.nodes.const import OP_PRECEDENCE
if TYPE_CHECKING:
from astroid import nodes
+if sys.version_info >= (3, 6, 2):
+ from typing import NoReturn
+else:
+ from typing_extensions import NoReturn
+
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from typing_extensions import Literal
+
+
# Types for 'NodeNG.nodes_of_class()'
T_Nodes = TypeVar("T_Nodes", bound="NodeNG")
T_Nodes2 = TypeVar("T_Nodes2", bound="NodeNG")
@@ -248,15 +263,41 @@ class NodeNG:
return True
return False
- def statement(self):
+ @overload
+ def statement(
+ self, *, future: Literal[None] = ...
+ ) -> Union["nodes.Statement", "nodes.Module"]:
+ ...
+
+ @overload
+ def statement(self, *, future: Literal[True]) -> "nodes.Statement":
+ ...
+
+ def statement(
+ self, *, future: Literal[None, True] = None
+ ) -> Union["nodes.Statement", "nodes.Module", NoReturn]:
"""The first parent node, including self, marked as statement node.
- :returns: The first parent statement.
- :rtype: NodeNG
+ TODO: Deprecate the future parameter and only raise StatementMissing and return
+ nodes.Statement
+
+ :raises AttributeError: If self has no parent attribute
+ :raises StatementMissing: If self has no parent attribute and future is True
"""
if self.is_statement:
- return self
- return self.parent.statement()
+ return cast("nodes.Statement", self)
+ if not self.parent:
+ if future:
+ raise StatementMissing(target=self)
+ warnings.warn(
+ "In astroid 3.0.0 NodeNG.statement() will return either a nodes.Statement "
+ "or raise a StatementMissing exception. AttributeError will no longer be raised. "
+ "This behaviour can already be triggered "
+ "by passing 'future=True' to a statement() call.",
+ DeprecationWarning,
+ )
+ raise AttributeError(f"{self} object has no attribute 'parent'")
+ return self.parent.statement(future=future)
def frame(
self,
diff --git a/astroid/nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes.py
index fc4cd240..df153b88 100644
--- a/astroid/nodes/scoped_nodes.py
+++ b/astroid/nodes/scoped_nodes.py
@@ -44,8 +44,10 @@ import builtins
import io
import itertools
import os
+import sys
import typing
-from typing import List, Optional, TypeVar
+import warnings
+from typing import List, Optional, TypeVar, Union, overload
from astroid import bases
from astroid import decorators as decorators_mod
@@ -65,6 +67,7 @@ from astroid.exceptions import (
InconsistentMroError,
InferenceError,
MroError,
+ StatementMissing,
TooManyLevelsError,
)
from astroid.interpreter.dunder_lookup import lookup
@@ -72,6 +75,18 @@ from astroid.interpreter.objectmodel import ClassModel, FunctionModel, ModuleMod
from astroid.manager import AstroidManager
from astroid.nodes import Arguments, Const, node_classes
+if sys.version_info >= (3, 6, 2):
+ from typing import NoReturn
+else:
+ from typing_extensions import NoReturn
+
+
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from typing_extensions import Literal
+
+
ITER_METHODS = ("__iter__", "__getitem__")
EXCEPTION_BASE_CLASSES = frozenset({"Exception", "BaseException"})
objects = util.lazy_import("objects")
@@ -637,12 +652,34 @@ class Module(LocalsDictNodeNG):
"""
return self.file is not None and self.file.endswith(".py")
- def statement(self):
+ @overload
+ def statement(self, *, future: Literal[None] = ...) -> "Module":
+ ...
+
+ @overload
+ def statement(self, *, future: Literal[True]) -> NoReturn:
+ ...
+
+ def statement(
+ self, *, future: Literal[None, True] = None
+ ) -> Union[NoReturn, "Module"]:
"""The first parent node, including self, marked as statement node.
- :returns: The first parent statement.
- :rtype: NodeNG
+ When called on a :class:`Module` with the future parameter this raises an error.
+
+ TODO: Deprecate the future parameter and only raise StatementMissing
+
+ :raises StatementMissing: If no self has no parent attribute and future is True
"""
+ if future:
+ raise StatementMissing(target=self)
+ warnings.warn(
+ "In astroid 3.0.0 NodeNG.statement() will return either a nodes.Statement "
+ "or raise a StatementMissing exception. nodes.Module will no longer be "
+ "considered a statement. This behaviour can already be triggered "
+ "by passing 'future=True' to a statement() call.",
+ DeprecationWarning,
+ )
return self
def previous_sibling(self):
diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py
index 300e4e92..11019e1b 100644
--- a/tests/unittest_builder.py
+++ b/tests/unittest_builder.py
@@ -38,6 +38,7 @@ from astroid.exceptions import (
AstroidSyntaxError,
AttributeInferenceError,
InferenceError,
+ StatementMissing,
)
from astroid.nodes.scoped_nodes import Module
@@ -614,7 +615,11 @@ class FileBuildTest(unittest.TestCase):
self.assertEqual(module.package, 0)
self.assertFalse(module.is_statement)
self.assertEqual(module.statement(), module)
- self.assertEqual(module.statement(), module)
+ with pytest.warns(DeprecationWarning) as records:
+ module.statement()
+ assert len(records) == 1
+ with self.assertRaises(StatementMissing):
+ module.statement(future=True)
def test_module_locals(self) -> None:
"""test the 'locals' dictionary of an astroid module"""
diff --git a/tests/unittest_nodes.py b/tests/unittest_nodes.py
index 73b4cecb..10607fe1 100644
--- a/tests/unittest_nodes.py
+++ b/tests/unittest_nodes.py
@@ -55,6 +55,7 @@ from astroid.exceptions import (
AstroidBuildingError,
AstroidSyntaxError,
AttributeInferenceError,
+ StatementMissing,
)
from astroid.nodes.node_classes import (
AssignAttr,
@@ -626,6 +627,12 @@ class ConstNodeTest(unittest.TestCase):
self.assertIs(node.value, value)
self.assertTrue(node._proxied.parent)
self.assertEqual(node._proxied.root().name, value.__class__.__module__)
+ with self.assertRaises(AttributeError):
+ with pytest.warns(DeprecationWarning) as records:
+ node.statement()
+ assert len(records) == 1
+ with self.assertRaises(StatementMissing):
+ node.statement(future=True)
def test_none(self) -> None:
self._test(None)
diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py
index 2045e173..db673629 100644
--- a/tests/unittest_scoped_nodes.py
+++ b/tests/unittest_scoped_nodes.py
@@ -323,6 +323,7 @@ class FunctionNodeTest(ModuleLoader, unittest.TestCase):
def test_navigation(self) -> None:
function = self.module["global_access"]
self.assertEqual(function.statement(), function)
+ self.assertEqual(function.statement(future=True), function)
l_sibling = function.previous_sibling()
# check taking parent if child is not a stmt
self.assertIsInstance(l_sibling, nodes.Assign)
@@ -821,6 +822,7 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase):
def test_navigation(self) -> None:
klass = self.module["YO"]
self.assertEqual(klass.statement(), klass)
+ self.assertEqual(klass.statement(future=True), klass)
l_sibling = klass.previous_sibling()
self.assertTrue(isinstance(l_sibling, nodes.FunctionDef), l_sibling)
self.assertEqual(l_sibling.name, "global_access")