diff options
author | Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> | 2023-04-17 17:13:31 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-17 17:13:31 +0200 |
commit | 1a14b5d2f6d7b16dcb8f50847b517798e8d1d759 (patch) | |
tree | 696f32917b1e03e369176f6989d52ecbd0c4ac8a | |
parent | c40d6c0fcd83af530418f89749020352cc6f5b76 (diff) | |
download | astroid-git-1a14b5d2f6d7b16dcb8f50847b517798e8d1d759.tar.gz |
Decouple ``FunctionDef`` and ``Lambda`` (#2115)
As discussed in #2112 we really need to decouple this nodes.
-rw-r--r-- | ChangeLog | 7 | ||||
-rw-r--r-- | astroid/helpers.py | 7 | ||||
-rw-r--r-- | astroid/nodes/scoped_nodes/scoped_nodes.py | 96 | ||||
-rw-r--r-- | tests/test_scoped_nodes.py | 33 |
4 files changed, 138 insertions, 5 deletions
@@ -10,6 +10,13 @@ Release date: TBA Closes #1780 +* ``nodes.FunctionDef`` no longer inherits from ``nodes.Lambda``. + This is a breaking change but considered a bug fix as the nodes did not share the same + API and were not interchangeable. + + We have tried to minimize the amount of breaking changes caused by this change + but some are unavoidable. + * Improved signature of the ``__init__`` and ``__postinit__`` methods of the following nodes: - ``nodes.AnnAssign`` - ``nodes.Arguments`` diff --git a/astroid/helpers.py b/astroid/helpers.py index 028cf1bc..15eac27a 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -30,7 +30,7 @@ def _build_proxy_class(cls_name: str, builtins: nodes.Module) -> nodes.ClassDef: def _function_type( function: nodes.Lambda | bases.UnboundMethod, builtins: nodes.Module ) -> nodes.ClassDef: - if isinstance(function, scoped_nodes.Lambda): + if isinstance(function, (scoped_nodes.Lambda, scoped_nodes.FunctionDef)): if function.root().name == "builtins": cls_name = "builtin_function_or_method" else: @@ -57,7 +57,10 @@ def _object_type( yield metaclass continue yield builtins.getattr("type")[0] - elif isinstance(inferred, (scoped_nodes.Lambda, bases.UnboundMethod)): + elif isinstance( + inferred, + (scoped_nodes.Lambda, bases.UnboundMethod, scoped_nodes.FunctionDef), + ): yield _function_type(inferred, builtins) elif isinstance(inferred, scoped_nodes.Module): yield _build_proxy_class("module", builtins) diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 8c39129c..6afcc12b 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -1244,7 +1244,12 @@ class Lambda(_base_nodes.FilterStmtsBaseNode, LocalsDictNodeNG): raise AttributeInferenceError(target=self, attribute=name) -class FunctionDef(_base_nodes.MultiLineBlockNode, _base_nodes.Statement, Lambda): +class FunctionDef( + _base_nodes.MultiLineBlockNode, + _base_nodes.FilterStmtsBaseNode, + _base_nodes.Statement, + LocalsDictNodeNG, +): """Class representing an :class:`ast.FunctionDef`. >>> import astroid @@ -1291,6 +1296,13 @@ class FunctionDef(_base_nodes.MultiLineBlockNode, _base_nodes.Statement, Lambda) ) _type = None + name = "<functiondef>" + + is_lambda = True + + special_attributes = FunctionModel() + """The names of special attributes that this function has.""" + @decorators_mod.deprecate_arguments(doc="Use the postinit arg 'doc_node' instead") def __init__( self, @@ -1338,7 +1350,20 @@ class FunctionDef(_base_nodes.MultiLineBlockNode, _base_nodes.Statement, Lambda) self.doc_node: Const | None = None """The doc node associated with this node.""" - self.instance_attrs = {} + self.locals = {} + """A map of the name of a local variable to the node defining it.""" + + self.args: Arguments + """The arguments that the function takes.""" + + self.body = [] + """The contents of the function body. + + :type: list(NodeNG) + """ + + self.instance_attrs: dict[str, list[NodeNG]] = {} + super().__init__( lineno=lineno, col_offset=col_offset, @@ -1454,6 +1479,62 @@ class FunctionDef(_base_nodes.MultiLineBlockNode, _base_nodes.Statement, Lambda) decorators.append(assign.value) return decorators + def pytype(self) -> Literal["builtins.instancemethod", "builtins.function"]: + """Get the name of the type that this node represents. + + :returns: The name of the type. + """ + if "method" in self.type: + return "builtins.instancemethod" + return "builtins.function" + + def display_type(self) -> str: + """A human readable type of this node. + + :returns: The type of this node. + :rtype: str + """ + if "method" in self.type: + return "Method" + return "Function" + + def callable(self) -> Literal[True]: + return True + + def argnames(self) -> list[str]: + """Get the names of each of the arguments, including that + of the collections of variable-length arguments ("args", "kwargs", + etc.), as well as positional-only and keyword-only arguments. + + :returns: The names of the arguments. + :rtype: list(str) + """ + if self.args.arguments: # maybe None with builtin functions + names = _rec_get_names(self.args.arguments) + else: + names = [] + if self.args.vararg: + names.append(self.args.vararg) + names += [elt.name for elt in self.args.kwonlyargs] + if self.args.kwarg: + names.append(self.args.kwarg) + return names + + def getattr( + self, name: str, context: InferenceContext | None = None + ) -> list[NodeNG]: + if not name: + raise AttributeInferenceError(target=self, attribute=name, context=context) + + found_attrs = [] + if name in self.instance_attrs: + found_attrs = self.instance_attrs[name] + if name in self.special_attributes: + found_attrs.append(self.special_attributes.lookup(name)) + if found_attrs: + return found_attrs + raise AttributeInferenceError(target=self, attribute=name) + @cached_property def type(self) -> str: # pylint: disable=too-many-return-statements # noqa: C901 """The function type for this node. @@ -1785,7 +1866,16 @@ class FunctionDef(_base_nodes.MultiLineBlockNode, _base_nodes.Statement, Lambda) frame = self.parent.frame(future=True) if isinstance(frame, ClassDef): return self, [frame] - return super().scope_lookup(node, name, offset) + + if node in self.args.defaults or node in self.args.kw_defaults: + frame = self.parent.frame(future=True) + # line offset to avoid that def func(f=func) resolve the default + # value to the defined function + offset = -1 + else: + # check this is not used in function decorators + frame = self + return frame._scope_lookup(node, name, offset) def frame(self: _T, *, future: Literal[None, True] = None) -> _T: """The node's frame node. diff --git a/tests/test_scoped_nodes.py b/tests/test_scoped_nodes.py index 1a6b8d15..d3a890ab 100644 --- a/tests/test_scoped_nodes.py +++ b/tests/test_scoped_nodes.py @@ -930,6 +930,39 @@ class FunctionNodeTest(ModuleLoader, unittest.TestCase): func: nodes.FunctionDef = builder.extract_node(code) # type: ignore[assignment] assert func.doc_node is None + @staticmethod + def test_display_type() -> None: + code = textwrap.dedent( + """\ + def foo(): + bar = 1 + """ + ) + func: nodes.FunctionDef = builder.extract_node(code) # type: ignore[assignment] + assert func.display_type() == "Function" + + code = textwrap.dedent( + """\ + class A: + def foo(self): #@ + bar = 1 + """ + ) + func: nodes.FunctionDef = builder.extract_node(code) # type: ignore[assignment] + assert func.display_type() == "Method" + + @staticmethod + def test_inference_error() -> None: + code = textwrap.dedent( + """\ + def foo(): + bar = 1 + """ + ) + func: nodes.FunctionDef = builder.extract_node(code) # type: ignore[assignment] + with pytest.raises(AttributeInferenceError): + func.getattr("") + class ClassNodeTest(ModuleLoader, unittest.TestCase): def test_dict_interface(self) -> None: |