Skip to content

Commit

Permalink
Require build class parent (#2557)
Browse files Browse the repository at this point in the history
* enforce a non-None parent in build_class

We also remove `add_local_node` to avoid redundancy. Instead we do the
   attachment to the parent scope in the constructor of `ClassDef`.

We append a node to the body of the frame when it is also the
   parent. If it's not a parent, then the node should belong to the
   "body" of the parent if it existed. An example is a definition
   within an "if", where the parent is the If node, but the frame is
   the whole module.

it's a part of the campaign to get rid of non-module roots
  • Loading branch information
temyurchenko authored Sep 13, 2024
1 parent c7b8a2f commit a99967e
Show file tree
Hide file tree
Showing 6 changed files with 27 additions and 20 deletions.
3 changes: 1 addition & 2 deletions astroid/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ def safe_infer(


def _build_proxy_class(cls_name: str, builtins: nodes.Module) -> nodes.ClassDef:
proxy = raw_building.build_class(cls_name)
proxy.parent = builtins
proxy = raw_building.build_class(cls_name, builtins)
return proxy


Expand Down
9 changes: 8 additions & 1 deletion astroid/nodes/scoped_nodes/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ def function_to_method(n, klass):
return n


def _attach_to_parent(node: NodeNG, name: str, parent: NodeNG):
frame = parent.frame()
frame.set_local(name, node)
if frame is parent:
frame._append_node(node)


class Module(LocalsDictNodeNG):
"""Class representing an :class:`ast.Module` node.
Expand Down Expand Up @@ -1935,7 +1942,7 @@ def __init__(
parent=parent,
)
if parent and not isinstance(parent, Unknown):
parent.frame().set_local(name, self)
_attach_to_parent(self, name, parent)

for local_name, node in self.implicit_locals():
self.add_local_node(node, local_name)
Expand Down
25 changes: 12 additions & 13 deletions astroid/raw_building.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ def _attach_local_node(parent, node, name: str) -> None:
parent.add_local_node(node)


def _add_dunder_class(func, member) -> None:
def _add_dunder_class(func, parent: nodes.NodeNG, member) -> None:
"""Add a __class__ member to the given func node, if we can determine it."""
python_cls = member.__class__
cls_name = getattr(python_cls, "__name__", None)
if not cls_name:
return
cls_bases = [ancestor.__name__ for ancestor in python_cls.__bases__]
doc = python_cls.__doc__ if isinstance(python_cls.__doc__, str) else None
ast_klass = build_class(cls_name, cls_bases, doc)
ast_klass = build_class(cls_name, parent, cls_bases, doc)
func.instance_attrs["__class__"] = [ast_klass]


Expand Down Expand Up @@ -97,7 +97,10 @@ def build_module(name: str, doc: str | None = None) -> nodes.Module:


def build_class(
name: str, basenames: Iterable[str] = (), doc: str | None = None
name: str,
parent: nodes.NodeNG,
basenames: Iterable[str] = (),
doc: str | None = None,
) -> nodes.ClassDef:
"""Create and initialize an astroid ClassDef node."""
node = nodes.ClassDef(
Expand All @@ -106,7 +109,7 @@ def build_class(
col_offset=0,
end_lineno=0,
end_col_offset=0,
parent=nodes.Unknown(),
parent=parent,
)
node.postinit(
bases=[
Expand Down Expand Up @@ -343,7 +346,7 @@ def object_build_methoddescriptor(
getattr(member, "__name__", None) or localname, doc=member.__doc__
)
node.add_local_node(func, localname)
_add_dunder_class(func, member)
_add_dunder_class(func, node, member)


def _base_class_object_build(
Expand All @@ -359,9 +362,8 @@ def _base_class_object_build(
class_name = name or getattr(member, "__name__", None) or localname
assert isinstance(class_name, str)
doc = member.__doc__ if isinstance(member.__doc__, str) else None
klass = build_class(class_name, basenames, doc)
klass = build_class(class_name, node, basenames, doc)
klass._newstyle = isinstance(member, type)
node.add_local_node(klass, localname)
try:
# limit the instantiation trick since it's too dangerous
# (such as infinite test execution...)
Expand Down Expand Up @@ -603,14 +605,11 @@ def _astroid_bootstrapping() -> None:

for cls, node_cls in node_classes.CONST_CLS.items():
if cls is TYPE_NONE:
proxy = build_class("NoneType")
proxy.parent = astroid_builtin
proxy = build_class("NoneType", astroid_builtin)
elif cls is TYPE_NOTIMPLEMENTED:
proxy = build_class("NotImplementedType")
proxy.parent = astroid_builtin
proxy = build_class("NotImplementedType", astroid_builtin)
elif cls is TYPE_ELLIPSIS:
proxy = build_class("Ellipsis")
proxy.parent = astroid_builtin
proxy = build_class("Ellipsis", astroid_builtin)
else:
proxy = astroid_builtin.getattr(cls.__name__)[0]
assert isinstance(proxy, nodes.ClassDef)
Expand Down
3 changes: 2 additions & 1 deletion tests/brain/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1705,7 +1705,8 @@ def test_infer_dict_from_keys() -> None:
)
for node in bad_nodes:
with pytest.raises(InferenceError):
next(node.infer())
if isinstance(next(node.infer()), util.UninferableBase):
raise InferenceError

# Test uninferable values
good_nodes = astroid.extract_node(
Expand Down
3 changes: 1 addition & 2 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ def _extract(self, obj_name: str) -> ClassDef:
return self.builtins.getattr(obj_name)[0]

def _build_custom_builtin(self, obj_name: str) -> ClassDef:
proxy = raw_building.build_class(obj_name)
proxy.parent = self.builtins
proxy = raw_building.build_class(obj_name, self.builtins)
return proxy

def assert_classes_equal(self, cls: ClassDef, other: ClassDef) -> None:
Expand Down
4 changes: 3 additions & 1 deletion tests/test_raw_building.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
build_module,
)

DUMMY_MOD = build_module("DUMMY")


class RawBuildingTC(unittest.TestCase):
def test_attach_dummy_node(self) -> None:
Expand All @@ -48,7 +50,7 @@ def test_build_module(self) -> None:
self.assertEqual(node.parent, None)

def test_build_class(self) -> None:
node = build_class("MyClass")
node = build_class("MyClass", DUMMY_MOD)
self.assertEqual(node.name, "MyClass")
self.assertEqual(node.doc_node, None)

Expand Down

0 comments on commit a99967e

Please sign in to comment.