From a99967e0bf7cc89dd3677ec58a6861529d269f5d Mon Sep 17 00:00:00 2001 From: temyurchenko <44875844+temyurchenko@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:35:02 -0400 Subject: [PATCH] Require build class parent (#2557) * 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 --- astroid/helpers.py | 3 +-- astroid/nodes/scoped_nodes/scoped_nodes.py | 9 +++++++- astroid/raw_building.py | 25 +++++++++++----------- tests/brain/test_brain.py | 3 ++- tests/test_helpers.py | 3 +-- tests/test_raw_building.py | 4 +++- 6 files changed, 27 insertions(+), 20 deletions(-) diff --git a/astroid/helpers.py b/astroid/helpers.py index a8e564543..ea7523b94 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -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 diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 6b7b8de33..629d524f9 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -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. @@ -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) diff --git a/astroid/raw_building.py b/astroid/raw_building.py index d65e3762c..130683806 100644 --- a/astroid/raw_building.py +++ b/astroid/raw_building.py @@ -49,7 +49,7 @@ 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) @@ -57,7 +57,7 @@ def _add_dunder_class(func, member) -> None: 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] @@ -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( @@ -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=[ @@ -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( @@ -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...) @@ -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) diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index 447c4cde2..14d4bd5d8 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -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( diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 2dd94a6ae..170176f93 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -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: diff --git a/tests/test_raw_building.py b/tests/test_raw_building.py index 951bf09d9..1325dbc3e 100644 --- a/tests/test_raw_building.py +++ b/tests/test_raw_building.py @@ -33,6 +33,8 @@ build_module, ) +DUMMY_MOD = build_module("DUMMY") + class RawBuildingTC(unittest.TestCase): def test_attach_dummy_node(self) -> None: @@ -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)