diff --git a/flowrep/models/parsers/object_scope.py b/flowrep/models/parsers/object_scope.py index 3923cad7..ea28cb90 100644 --- a/flowrep/models/parsers/object_scope.py +++ b/flowrep/models/parsers/object_scope.py @@ -3,6 +3,7 @@ import ast import builtins import inspect +import sys from collections.abc import Callable from typing import Any @@ -45,7 +46,18 @@ def fork(self) -> ScopeProxy: def get_scope(func: Callable[..., Any] | type[Any]) -> ScopeProxy: - return ScopeProxy(inspect.getmodule(func).__dict__ | vars(builtins)) + module = inspect.getmodule(func) + if module is None: + module_name = getattr(func, "__module__", None) + if module_name is not None: + module = sys.modules.get(module_name) + if module is None: + raise ValueError( + f"Cannot determine the module for {func!r}. " + "inspect.getmodule() returned None and no resolvable __module__ " + "attribute was found." + ) + return ScopeProxy(module.__dict__ | vars(builtins)) def resolve_attribute_to_object(attribute: str, scope: ScopeProxy | object) -> object: diff --git a/tests/unit/models/parsers/test_object_scope.py b/tests/unit/models/parsers/test_object_scope.py index 7fc2bcb0..a10c0e70 100644 --- a/tests/unit/models/parsers/test_object_scope.py +++ b/tests/unit/models/parsers/test_object_scope.py @@ -1,4 +1,6 @@ import ast +import sys +import types import unittest from flowrep.models.parsers import object_scope @@ -42,6 +44,36 @@ def test_includes_builtins(self): self.assertIs(scope.int, int) self.assertIs(scope.ValueError, ValueError) + def test_none_module_fallback_via_dunder_module(self): + """When inspect.getmodule returns None but __module__ is set, fall back.""" + mod = types.ModuleType("_test_dynamic_mod") + mod.__dict__["sentinel"] = object() + sys.modules["_test_dynamic_mod"] = mod + try: + func = types.FunctionType( + (lambda: None).__code__, + {}, + "_test_func", + ) + # Manually set __module__ but keep the function out of a real module + # so inspect.getmodule() returns None. + func.__module__ = "_test_dynamic_mod" + scope = object_scope.get_scope(func) + self.assertIs(scope.sentinel, mod.__dict__["sentinel"]) + finally: + del sys.modules["_test_dynamic_mod"] + + def test_no_resolvable_module_raises_value_error(self): + """When neither inspect.getmodule nor __module__ resolves, raise ValueError.""" + func = types.FunctionType( + (lambda: None).__code__, + {}, + "_orphan_func", + ) + func.__module__ = None # type: ignore[assignment] + with self.assertRaises(ValueError): + object_scope.get_scope(func) + class TestResolveSymbolToObject(unittest.TestCase): def test_simple_name(self):