Skip to content

Commit 36094ed

Browse files
authored
control setting nodes as local outside of the constructor (#2588)
1. The main reason is that a node might be assigned to its parent via an «alias»: Sometimes a class accesses a member by a different name than "__name__" of that member: in pypy3 `list.__mul__.__name__ == "__rmul__"`. As a result, in the example above we weren't able to find "list.__mul__", because it was recorded only as "list.__rmul__". 2. Sometimes we want to have a parent semantically, but we don't want to add it to the list of locals. For example, when inferring properties we are creating ad-hoc properties. We wouldn't want to add another symbol to the locals every time we do an inference. (actually, there's a very good question as to why we are doing those ad-hoc properties but that's out of scope) it's a part of the campaign to get rid of non-module roots
1 parent 32cb29e commit 36094ed

File tree

5 files changed

+60
-96
lines changed

5 files changed

+60
-96
lines changed

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ What's New in astroid 3.3.5?
1313
============================
1414
Release date: TBA
1515

16+
* Control setting local nodes outside of the supposed local's constructor.
17+
18+
Closes pylint-dev/astroid/issues/1490
1619

1720

1821
What's New in astroid 3.3.4?

astroid/nodes/scoped_nodes/scoped_nodes.py

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,6 @@ def function_to_method(n, klass):
178178
return n
179179

180180

181-
def _attach_to_parent(node: NodeNG, name: str, parent: NodeNG):
182-
frame = parent.frame()
183-
frame.set_local(name, node)
184-
if frame is parent:
185-
frame._append_node(node)
186-
187-
188181
class Module(LocalsDictNodeNG):
189182
"""Class representing an :class:`ast.Module` node.
190183
@@ -1173,9 +1166,6 @@ def __init__(
11731166
end_col_offset=end_col_offset,
11741167
parent=parent,
11751168
)
1176-
if parent and not isinstance(parent, Unknown):
1177-
frame = parent.frame()
1178-
frame.set_local(name, self)
11791169

11801170
def postinit(
11811171
self,
@@ -1528,33 +1518,15 @@ def _infer(
15281518
yield self
15291519
return InferenceErrorInfo(node=self, context=context)
15301520

1531-
# When inferring a property, we instantiate a new `objects.Property` object,
1532-
# which in turn, because it inherits from `FunctionDef`, sets itself in the locals
1533-
# of the wrapping frame. This means that every time we infer a property, the locals
1534-
# are mutated with a new instance of the property. To avoid this, we detect this
1535-
# scenario and avoid passing the `parent` argument to the constructor.
15361521
if not self.parent:
15371522
raise ParentMissingError(target=self)
1538-
parent_frame = self.parent.frame()
1539-
property_already_in_parent_locals = self.name in parent_frame.locals and any(
1540-
isinstance(val, objects.Property) for val in parent_frame.locals[self.name]
1541-
)
1542-
# We also don't want to pass parent if the definition is within a Try node
1543-
if isinstance(
1544-
self.parent,
1545-
(node_classes.Try, node_classes.If),
1546-
):
1547-
property_already_in_parent_locals = True
1548-
15491523
prop_func = objects.Property(
15501524
function=self,
15511525
name=self.name,
15521526
lineno=self.lineno,
1553-
parent=self.parent if not property_already_in_parent_locals else None,
1527+
parent=self.parent,
15541528
col_offset=self.col_offset,
15551529
)
1556-
if property_already_in_parent_locals:
1557-
prop_func.parent = self.parent
15581530
prop_func.postinit(body=[], args=self.args, doc_node=self.doc_node)
15591531
yield prop_func
15601532
return InferenceErrorInfo(node=self, context=context)
@@ -1941,9 +1913,6 @@ def __init__(
19411913
end_col_offset=end_col_offset,
19421914
parent=parent,
19431915
)
1944-
if parent and not isinstance(parent, Unknown):
1945-
_attach_to_parent(self, name, parent)
1946-
19471916
for local_name, node in self.implicit_locals():
19481917
self.add_local_node(node, local_name)
19491918

astroid/raw_building.py

Lines changed: 51 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,17 @@ def _add_dunder_class(func, parent: nodes.NodeNG, member) -> None:
6161
func.instance_attrs["__class__"] = [ast_klass]
6262

6363

64+
def build_dummy(runtime_object) -> nodes.EmptyNode:
65+
enode = nodes.EmptyNode()
66+
enode.object = runtime_object
67+
return enode
68+
69+
6470
def attach_dummy_node(node, name: str, runtime_object=_EMPTY_OBJECT_MARKER) -> None:
6571
"""create a dummy node and register it in the locals of the given
6672
node with the specified name
6773
"""
68-
enode = nodes.EmptyNode()
69-
enode.object = runtime_object
70-
_attach_local_node(node, enode, name)
74+
_attach_local_node(node, build_dummy(runtime_object), name)
7175

7276

7377
def attach_const_node(node, name: str, value) -> None:
@@ -262,11 +266,11 @@ def register_arguments(func: nodes.FunctionDef, args: list | None = None) -> Non
262266

263267

264268
def object_build_class(
265-
node: nodes.Module | nodes.ClassDef, member: type, localname: str
269+
node: nodes.Module | nodes.ClassDef, member: type
266270
) -> nodes.ClassDef:
267271
"""create astroid for a living class object"""
268272
basenames = [base.__name__ for base in member.__bases__]
269-
return _base_class_object_build(node, member, basenames, localname=localname)
273+
return _base_class_object_build(node, member, basenames)
270274

271275

272276
def _get_args_info_from_callable(
@@ -304,8 +308,8 @@ def _get_args_info_from_callable(
304308

305309

306310
def object_build_function(
307-
node: nodes.Module | nodes.ClassDef, member: _FunctionTypes, localname: str
308-
) -> None:
311+
node: nodes.Module | nodes.ClassDef, member: _FunctionTypes
312+
) -> nodes.FunctionDef:
309313
"""create astroid for a living function object"""
310314
(
311315
args,
@@ -315,8 +319,8 @@ def object_build_function(
315319
kwonly_defaults,
316320
) = _get_args_info_from_callable(member)
317321

318-
func = build_function(
319-
getattr(member, "__name__", None) or localname,
322+
return build_function(
323+
getattr(member, "__name__", "<no-name>"),
320324
args,
321325
posonlyargs,
322326
defaults,
@@ -325,44 +329,37 @@ def object_build_function(
325329
kwonlydefaults=kwonly_defaults,
326330
)
327331

328-
node.add_local_node(func, localname)
329-
330332

331333
def object_build_datadescriptor(
332-
node: nodes.Module | nodes.ClassDef, member: type, name: str
334+
node: nodes.Module | nodes.ClassDef, member: type
333335
) -> nodes.ClassDef:
334336
"""create astroid for a living data descriptor object"""
335-
return _base_class_object_build(node, member, [], name)
337+
return _base_class_object_build(node, member, [])
336338

337339

338340
def object_build_methoddescriptor(
339341
node: nodes.Module | nodes.ClassDef,
340342
member: _FunctionTypes,
341-
localname: str,
342-
) -> None:
343+
) -> nodes.FunctionDef:
343344
"""create astroid for a living method descriptor object"""
344345
# FIXME get arguments ?
345-
func = build_function(
346-
getattr(member, "__name__", None) or localname, doc=member.__doc__
347-
)
348-
node.add_local_node(func, localname)
346+
name = getattr(member, "__name__", "<no-name>")
347+
func = build_function(name, doc=member.__doc__)
349348
_add_dunder_class(func, node, member)
349+
return func
350350

351351

352352
def _base_class_object_build(
353353
node: nodes.Module | nodes.ClassDef,
354354
member: type,
355355
basenames: list[str],
356-
name: str | None = None,
357-
localname: str | None = None,
358356
) -> nodes.ClassDef:
359357
"""create astroid for a living class object, with a given set of base names
360358
(e.g. ancestors)
361359
"""
362-
class_name = name or getattr(member, "__name__", None) or localname
363-
assert isinstance(class_name, str)
360+
name = getattr(member, "__name__", "<no-name>")
364361
doc = member.__doc__ if isinstance(member.__doc__, str) else None
365-
klass = build_class(class_name, node, basenames, doc)
362+
klass = build_class(name, node, basenames, doc)
366363
klass._newstyle = isinstance(member, type)
367364
try:
368365
# limit the instantiation trick since it's too dangerous
@@ -387,10 +384,9 @@ def _base_class_object_build(
387384

388385
def _build_from_function(
389386
node: nodes.Module | nodes.ClassDef,
390-
name: str,
391387
member: _FunctionTypes,
392388
module: types.ModuleType,
393-
) -> None:
389+
) -> nodes.FunctionDef | nodes.EmptyNode:
394390
# verify this is not an imported function
395391
try:
396392
code = member.__code__ # type: ignore[union-attr]
@@ -400,12 +396,10 @@ def _build_from_function(
400396
code = None
401397
filename = getattr(code, "co_filename", None)
402398
if filename is None:
403-
assert isinstance(member, object)
404-
object_build_methoddescriptor(node, member, name)
405-
elif filename != getattr(module, "__file__", None):
406-
attach_dummy_node(node, name, member)
407-
else:
408-
object_build_function(node, member, name)
399+
return object_build_methoddescriptor(node, member)
400+
if filename == getattr(module, "__file__", None):
401+
return object_build_function(node, member)
402+
return build_dummy(member)
409403

410404

411405
def _safe_has_attribute(obj, member: str) -> bool:
@@ -472,58 +466,57 @@ def object_build(
472466
if obj in self._done:
473467
return None
474468
self._done[obj] = node
475-
for name in dir(obj):
469+
for alias in dir(obj):
476470
# inspect.ismethod() and inspect.isbuiltin() in PyPy return
477471
# the opposite of what they do in CPython for __class_getitem__.
478-
pypy__class_getitem__ = IS_PYPY and name == "__class_getitem__"
472+
pypy__class_getitem__ = IS_PYPY and alias == "__class_getitem__"
479473
try:
480474
with warnings.catch_warnings():
481475
warnings.simplefilter("ignore")
482-
member = getattr(obj, name)
476+
member = getattr(obj, alias)
483477
except AttributeError:
484478
# damned ExtensionClass.Base, I know you're there !
485-
attach_dummy_node(node, name)
479+
attach_dummy_node(node, alias)
486480
continue
487481
if inspect.ismethod(member) and not pypy__class_getitem__:
488482
member = member.__func__
489483
if inspect.isfunction(member):
490-
_build_from_function(node, name, member, self._module)
484+
child = _build_from_function(node, member, self._module)
491485
elif inspect.isbuiltin(member) or pypy__class_getitem__:
492-
if self.imported_member(node, member, name):
486+
if self.imported_member(node, member, alias):
493487
continue
494-
object_build_methoddescriptor(node, member, name)
488+
child = object_build_methoddescriptor(node, member)
495489
elif inspect.isclass(member):
496-
if self.imported_member(node, member, name):
490+
if self.imported_member(node, member, alias):
497491
continue
498492
if member in self._done:
499-
class_node = self._done[member]
500-
assert isinstance(class_node, nodes.ClassDef)
501-
if class_node not in node.locals.get(name, ()):
502-
node.add_local_node(class_node, name)
493+
child = self._done[member]
494+
assert isinstance(child, nodes.ClassDef)
503495
else:
504-
class_node = object_build_class(node, member, name)
496+
child = object_build_class(node, member)
505497
# recursion
506-
self.object_build(class_node, member)
507-
if name == "__class__" and class_node.parent is None:
508-
class_node.parent = self._done[self._module]
498+
self.object_build(child, member)
509499
elif inspect.ismethoddescriptor(member):
510-
object_build_methoddescriptor(node, member, name)
500+
child: nodes.NodeNG = object_build_methoddescriptor(node, member)
511501
elif inspect.isdatadescriptor(member):
512-
object_build_datadescriptor(node, member, name)
502+
child = object_build_datadescriptor(node, member)
513503
elif isinstance(member, _CONSTANTS):
514-
attach_const_node(node, name, member)
504+
if alias in node.special_attributes:
505+
continue
506+
child = nodes.const_factory(member)
515507
elif inspect.isroutine(member):
516508
# This should be called for Jython, where some builtin
517509
# methods aren't caught by isbuiltin branch.
518-
_build_from_function(node, name, member, self._module)
510+
child = _build_from_function(node, member, self._module)
519511
elif _safe_has_attribute(member, "__all__"):
520-
module = build_module(name)
521-
_attach_local_node(node, module, name)
512+
child: nodes.NodeNG = build_module(alias)
522513
# recursion
523-
self.object_build(module, member)
514+
self.object_build(child, member)
524515
else:
525516
# create an empty node so that the name is actually defined
526-
attach_dummy_node(node, name, member)
517+
child: nodes.NodeNG = build_dummy(member)
518+
if child not in node.locals.get(alias, ()):
519+
node.add_local_node(child, alias)
527520
return None
528521

529522
def imported_member(self, node, member, name: str) -> bool:
@@ -629,6 +622,7 @@ def _astroid_bootstrapping() -> None:
629622
end_col_offset=0,
630623
parent=astroid_builtin,
631624
)
625+
astroid_builtin.set_local(_GeneratorType.name, _GeneratorType)
632626
generator_doc_node = (
633627
nodes.Const(value=types.GeneratorType.__doc__)
634628
if types.GeneratorType.__doc__
@@ -652,6 +646,7 @@ def _astroid_bootstrapping() -> None:
652646
end_col_offset=0,
653647
parent=astroid_builtin,
654648
)
649+
astroid_builtin.set_local(_AsyncGeneratorType.name, _AsyncGeneratorType)
655650
async_generator_doc_node = (
656651
nodes.Const(value=types.AsyncGeneratorType.__doc__)
657652
if types.AsyncGeneratorType.__doc__

astroid/rebuilder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,7 @@ def visit_classdef(
815815
else []
816816
),
817817
)
818+
parent.set_local(newnode.name, newnode)
818819
return newnode
819820

820821
def visit_continue(self, node: ast.Continue, parent: NodeNG) -> nodes.Continue:
@@ -1112,6 +1113,7 @@ def _visit_functiondef(
11121113
),
11131114
)
11141115
self._global_names.pop()
1116+
parent.set_local(newnode.name, newnode)
11151117
return newnode
11161118

11171119
def visit_functiondef(

tests/test_inference.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6880,20 +6880,15 @@ class A:
68806880
@property
68816881
def a(self):
68826882
return 42
6883-
6884-
A()
68856883
"""
6886-
node = extract_node(code)
6887-
# Infer the class
6888-
cls = next(node.infer())
6884+
cls = extract_node(code)
68896885
(prop,) = cls.getattr("a")
68906886

6891-
# Try to infer the property function *multiple* times. `A.locals` should be modified only once
6887+
assert len(cls.locals["a"]) == 1
68926888
for _ in range(3):
68936889
prop.inferred()
68946890
a_locals = cls.locals["a"]
6895-
# [FunctionDef, Property]
6896-
assert len(a_locals) == 2
6891+
assert len(a_locals) == 1
68976892

68986893

68996894
def test_getattr_fails_on_empty_values() -> None:

0 commit comments

Comments
 (0)