Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ apidoc/
.ipynb_checkpoints/
test_times.dat
core.*
flowrep/_version.py
19 changes: 16 additions & 3 deletions flowrep/models/parsers/object_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import ast
import builtins
import inspect
from types import FunctionType
import sys
from collections.abc import Callable
from typing import Any


class ScopeProxy:
Expand Down Expand Up @@ -43,8 +45,19 @@ def fork(self) -> ScopeProxy:
return ScopeProxy(dict(self._d))


def get_scope(func: FunctionType) -> ScopeProxy:
return ScopeProxy(inspect.getmodule(func).__dict__ | vars(builtins))
def get_scope(func: Callable[..., Any] | type[Any]) -> ScopeProxy:
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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codecov is showing that this line is not covered. I'd like to get it fully covered, but I'm a little confused because it looks to me like test_none_module_fallback_via_dunder_module should be hitting this?

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:
Expand Down
89 changes: 89 additions & 0 deletions tests/unit/models/parsers/test_object_scope.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import ast
import sys
import types
import unittest
from unittest.mock import patch

from flowrep.models.parsers import object_scope

Expand All @@ -8,6 +11,9 @@ def add(x: float = 2.0, y: float = 1) -> float:
return x + y


identity = lambda x: x # noqa: E731


class Outer:
class Inner:
@staticmethod
Expand Down Expand Up @@ -42,6 +48,89 @@ 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_sys_modules_fallback_when_getmodule_returns_none(self):
"""Cover line 53: sys.modules.get(module_name) is reached when inspect.getmodule
returns None but the object's __module__ is registered in sys.modules."""
mod = types.ModuleType("_test_fallback_mod")
mod.__dict__["marker"] = object()
sys.modules["_test_fallback_mod"] = mod
try:
func = types.FunctionType(
(lambda: None).__code__,
{},
"_test_func",
)
func.__module__ = "_test_fallback_mod"
# Patch inspect.getmodule to return None, simulating objects (e.g.
# C-extension types) where the module cannot be determined from the
# object directly, so the fallback via sys.modules is exercised.
with patch.object(object_scope.inspect, "getmodule", return_value=None):
scope = object_scope.get_scope(func)
self.assertIs(scope.marker, mod.__dict__["marker"])
finally:
del sys.modules["_test_fallback_mod"]

def test_builtin_type(self):
"""get_scope works for a builtin type such as ``int``."""
scope = object_scope.get_scope(int)
# The builtins module is always merged in, so int and len must be present.
self.assertIs(scope.int, int)
self.assertIs(scope.len, len)

def test_builtin_function(self):
"""get_scope works for a builtin function such as ``len``."""
scope = object_scope.get_scope(len)
self.assertIs(scope.len, len)
self.assertIs(scope.int, int)

def test_user_defined_class(self):
"""get_scope works for a user-defined class object."""
scope = object_scope.get_scope(Outer)
# Module-level names from this test module should be visible.
self.assertIs(scope.Outer, Outer)
self.assertIs(scope.add, add)

def test_static_method(self):
"""get_scope works for a static method."""
scope = object_scope.get_scope(Outer.Inner.nested_func)
self.assertIs(scope.Outer, Outer)
self.assertIs(scope.add, add)

def test_lambda(self):
"""get_scope works for a module-level lambda."""
scope = object_scope.get_scope(identity)
self.assertIs(scope.identity, identity)

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):
Expand Down
Loading