Skip to content

Create AssignName nodes for ClassDef and FunctionDef #1390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions astroid/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def _post_build(self, module, encoding):
module = self._manager.visit_transforms(module)
return module

def _data_build(self, data, modname, path):
def _data_build(self, data: str, modname, path):
"""Build tree node from data and add some information"""
try:
node, parser_module = _parse_string(data, type_comments=True)
Expand All @@ -200,7 +200,7 @@ def _data_build(self, data, modname, path):
path is not None
and os.path.splitext(os.path.basename(path))[0] == "__init__"
)
builder = rebuilder.TreeRebuilder(self._manager, parser_module)
builder = rebuilder.TreeRebuilder(self._manager, parser_module, data)
module = builder.visit_module(node, modname, node_file, package)
module._import_from_nodes = builder._import_from_nodes
module._delayed_assattr = builder._delayed_assattr
Expand Down
18 changes: 17 additions & 1 deletion astroid/nodes/scoped_nodes/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,8 @@ def __init__(
:type doc: str or None
"""

self.name_node: Optional[node_classes.AssignName] = None

self.instance_attrs = {}
super().__init__(
lineno=lineno,
Expand All @@ -1567,6 +1569,8 @@ def postinit(
returns=None,
type_comment_returns=None,
type_comment_args=None,
*,
name_node: Optional[node_classes.AssignName] = None,
):
"""Do some setup after initialisation.

Expand All @@ -1589,6 +1593,7 @@ def postinit(
self.returns = returns
self.type_comment_returns = type_comment_returns
self.type_comment_args = type_comment_args
self.name_node = name_node

@decorators_mod.cachedproperty
def extra_decorators(self) -> List[node_classes.Call]:
Expand Down Expand Up @@ -2212,6 +2217,8 @@ def __init__(
:type doc: str or None
"""

self.name_node: Optional[node_classes.AssignName] = None

super().__init__(
lineno=lineno,
col_offset=col_offset,
Expand Down Expand Up @@ -2241,7 +2248,15 @@ def implicit_locals(self):

# pylint: disable=redefined-outer-name
def postinit(
self, bases, body, decorators, newstyle=None, metaclass=None, keywords=None
self,
bases,
body,
decorators,
newstyle=None,
metaclass=None,
keywords=None,
*,
name_node: Optional[node_classes.AssignName] = None,
):
"""Do some setup after initialisation.

Expand Down Expand Up @@ -2272,6 +2287,7 @@ def postinit(
self._newstyle = newstyle
if metaclass is not None:
self._metaclass = metaclass
self.name_node = name_node

def _newstyle_impl(self, context=None):
if context is None:
Expand Down
50 changes: 49 additions & 1 deletion astroid/rebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"""

import sys
import token
from io import StringIO
from tokenize import generate_tokens
from typing import (
TYPE_CHECKING,
Callable,
Expand Down Expand Up @@ -88,9 +91,13 @@ class TreeRebuilder:
"""Rebuilds the _ast tree to become an Astroid tree"""

def __init__(
self, manager: AstroidManager, parser_module: Optional[ParserModule] = None
self,
manager: AstroidManager,
parser_module: Optional[ParserModule] = None,
data: Optional[str] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the typing you added in astroid/builder.py this should be str right? I don't see any other place where TreeRebuilder is instantiated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, inside astroid it will always be str. Technically, you could consider TreeRebuilder part of the public interface however. Adding a new required argument would then be a breaking change.

Defining it as Optional isn't too bad considering everything. The name_node / position needs to be Optional, so a small check if not self._data doesn't really hurt.

):
self._manager = manager
self._data = data.split("\n") if data else None
self._global_names: List[Dict[str, List[nodes.Global]]] = []
self._import_from_nodes: List[nodes.ImportFrom] = []
self._delayed_assattr: List[nodes.AssignAttr] = []
Expand Down Expand Up @@ -133,6 +140,45 @@ def _get_context(
) -> Context:
return self._parser_module.context_classes.get(type(node.ctx), Context.Load)

def _create_name_node(
self,
node: Union["ast.ClassDef", "ast.FunctionDef", "ast.AsyncFunctionDef"],
parent: Union[nodes.ClassDef, nodes.FunctionDef, nodes.AsyncFunctionDef],
) -> Optional[nodes.AssignName]:
if not self._data:
return None
end_lineno: Optional[int] = getattr(node, "end_lineno", None)
if node.body:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will need to see how this interacts with docstrings etc. Those are part of the body in ast but not in astroid. You probably thought about this though :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node here is the ast one. So that should work. AFAIK each function / class body needs at least one node to be valid Python (either a docstring, pass, or something else). In any case if body should somehow be empty, it would fallback to the end_lineno of the node itself. Or if that also didn't work None, which is essentially self._data[node.lineno - 1 : ].

end_lineno = node.body[0].lineno
# pylint: disable-next=unsubscriptable-object
data = "\n".join(self._data[node.lineno - 1 : end_lineno])

if isinstance(parent, nodes.FunctionDef):
search_token_name = "def"
else:
search_token_name = "class"
token_found: bool = False
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is bool necessary here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary, I find it helpful never the less :)


for t in generate_tokens(StringIO(data).readline):
if token_found and t.type == token.NAME and t.string == node.name:
break
if t.type == token.NAME and t.string == search_token_name:
token_found = True
continue
token_found = False
else:
return None

# pylint: disable=undefined-loop-variable
return nodes.AssignName(
name=t.string,
lineno=node.lineno - 1 + t.start[0],
col_offset=t.start[1],
end_lineno=node.lineno - 1 + t.end[0],
end_col_offset=t.end[1],
parent=parent,
)

def visit_module(
self, node: "ast.Module", modname: str, modpath: str, package: bool
) -> nodes.Module:
Expand Down Expand Up @@ -1203,6 +1249,7 @@ def visit_classdef(
for kwd in node.keywords
if kwd.arg != "metaclass"
],
name_node=self._create_name_node(node, newnode),
)
return newnode

Expand Down Expand Up @@ -1551,6 +1598,7 @@ def _visit_functiondef(
returns=returns,
type_comment_returns=type_comment_returns,
type_comment_args=type_comment_args,
name_node=self._create_name_node(node, newnode),
)
self._global_names.pop()
return newnode
Expand Down