Skip to content

Commit

Permalink
Fix RuntimeError caused by analyzing live objects with `__getattribut…
Browse files Browse the repository at this point in the history
…e__` or descriptors (#2687)

Avoid some attribute accesses in const_factory

(cherry picked from commit 71dc6b2)
  • Loading branch information
aatle authored and jacobtylerwalls committed Feb 20, 2025
1 parent d52799b commit 3e37275
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 3 deletions.
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ What's New in astroid 3.3.9?
Release date: TBA


* Fix crash when `sys.modules` contains lazy loader objects during checking.

Closes #2686
Closes pylint-dev/pylint#8589


What's New in astroid 3.3.8?
============================
Expand Down
13 changes: 10 additions & 3 deletions astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5488,6 +5488,7 @@ def _create_basic_elements(
"""Create a list of nodes to function as the elements of a new node."""
elements: list[NodeNG] = []
for element in value:
# NOTE: avoid accessing any attributes of element in the loop.
element_node = const_factory(element)
element_node.parent = node
elements.append(element_node)
Expand All @@ -5500,6 +5501,7 @@ def _create_dict_items(
"""Create a list of node pairs to function as the items of a new dict node."""
elements: list[tuple[SuccessfulInferenceResult, SuccessfulInferenceResult]] = []
for key, value in values.items():
# NOTE: avoid accessing any attributes of both key and value in the loop.
key_node = const_factory(key)
key_node.parent = node
value_node = const_factory(value)
Expand All @@ -5510,18 +5512,23 @@ def _create_dict_items(

def const_factory(value: Any) -> ConstFactoryResult:
"""Return an astroid node for a python value."""
assert not isinstance(value, NodeNG)
# NOTE: avoid accessing any attributes of value until it is known that value
# is of a const type, to avoid possibly triggering code for a live object.
# Accesses include value.__class__ and isinstance(value, ...), but not type(value).
# See: https://github.com/pylint-dev/astroid/issues/2686
value_type = type(value)
assert not issubclass(value_type, NodeNG)

# This only handles instances of the CONST types. Any
# subclasses get inferred as EmptyNode.
# TODO: See if we should revisit these with the normal builder.
if value.__class__ not in CONST_CLS:
if value_type not in CONST_CLS:
node = EmptyNode()
node.object = value
return node

instance: List | Set | Tuple | Dict
initializer_cls = CONST_CLS[value.__class__]
initializer_cls = CONST_CLS[value_type]
if issubclass(initializer_cls, (List, Set, Tuple)):
instance = initializer_cls(
lineno=None,
Expand Down
9 changes: 9 additions & 0 deletions tests/test_raw_building.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import pytest

import tests.testdata.python3.data.fake_module_with_broken_getattr as fm_getattr
import tests.testdata.python3.data.fake_module_with_collection_getattribute as fm_collection
import tests.testdata.python3.data.fake_module_with_warnings as fm
from astroid.builder import AstroidBuilder
from astroid.const import IS_PYPY, PY312_PLUS
Expand Down Expand Up @@ -118,6 +119,14 @@ def test_module_object_with_broken_getattr(self) -> None:
# This should not raise an exception
AstroidBuilder().inspect_build(fm_getattr, "test")

def test_module_collection_with_object_getattribute(self) -> None:
# Tests https://github.com/pylint-dev/astroid/issues/2686
# When astroid live inspection of module's collection raises
# error when element __getattribute__ causes collection to change size.

# This should not raise an exception
AstroidBuilder().inspect_build(fm_collection, "test")


@pytest.mark.skipif(
"posix" not in sys.builtin_module_names, reason="Platform doesn't support posix"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Changer:
def __getattribute__(self, name):
list_collection.append(self)
set_collection.add(self)
dict_collection[self] = self
return object.__getattribute__(self, name)


list_collection = [Changer()]
set_collection = {Changer()}
dict_collection = {Changer(): Changer()}

0 comments on commit 3e37275

Please sign in to comment.