From 3079dea36714e75acd0b5004af0fcff4b42530df Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Wed, 25 Feb 2026 18:36:46 +0100 Subject: [PATCH 1/5] Separate transformation to object from ast parsing --- flowrep/models/parsers/object_scope.py | 22 ++++++++++--------- .../unit/models/parsers/test_object_scope.py | 4 ++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/flowrep/models/parsers/object_scope.py b/flowrep/models/parsers/object_scope.py index bcae785f..6830fa27 100644 --- a/flowrep/models/parsers/object_scope.py +++ b/flowrep/models/parsers/object_scope.py @@ -24,6 +24,16 @@ def get_scope(func: FunctionType) -> ScopeProxy: return ScopeProxy(inspect.getmodule(func).__dict__ | vars(builtins)) +def resolve_attribute_to_object(attribute: str, scope: ScopeProxy) -> object: + obj = None + try: + for attr in attribute.split("."): + obj = getattr(obj or scope, attr) + return obj + except AttributeError as e: + raise ValueError(f"Could not find attribute '{attr}' of {attribute}") from e + + def resolve_symbol_to_object( node: ast.expr, # Expecting a Name or Attribute here, and will otherwise TypeError scope: ScopeProxy | object, @@ -31,21 +41,13 @@ def resolve_symbol_to_object( ) -> object: """ """ _chain = _chain or [] - error_suffix = f" while attempting to resolve the symbol chain '{'.'.join(_chain)}'" if isinstance(node, ast.Name): - attr = node.id - try: - obj = getattr(scope, attr) - for attr in _chain: - obj = getattr(obj, attr) - return obj - except AttributeError as e: - raise ValueError(f"Could not find attribute '{attr}' {error_suffix}") from e + return resolve_attribute_to_object(".".join([node.id] + _chain), scope) elif isinstance(node, ast.Attribute): return resolve_symbol_to_object(node.value, scope, [node.attr] + _chain) else: raise TypeError( - f"Cannot resolve symbol {node} {error_suffix}. " + f"Cannot resolve symbol {node} or the symbol chain '{'.'.join(_chain)}'. " f"Expected an ast.Name or chain of ast.Attribute and ast.Name, but got " f"{node}." ) diff --git a/tests/unit/models/parsers/test_object_scope.py b/tests/unit/models/parsers/test_object_scope.py index e442579f..b33d0194 100644 --- a/tests/unit/models/parsers/test_object_scope.py +++ b/tests/unit/models/parsers/test_object_scope.py @@ -74,3 +74,7 @@ def test_unrecognized_node_raises(self): node = ast.Constant(value=42) with self.assertRaises(TypeError): object_scope.resolve_symbol_to_object(node, scope) + + +if __name__ == "__main__": + unittest.main() From c437848072f59394e70cc1cf28342526d5eff42b Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Wed, 25 Feb 2026 18:41:11 +0100 Subject: [PATCH 2/5] mypy --- flowrep/models/parsers/object_scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flowrep/models/parsers/object_scope.py b/flowrep/models/parsers/object_scope.py index 6830fa27..6fdbcb64 100644 --- a/flowrep/models/parsers/object_scope.py +++ b/flowrep/models/parsers/object_scope.py @@ -24,7 +24,7 @@ def get_scope(func: FunctionType) -> ScopeProxy: return ScopeProxy(inspect.getmodule(func).__dict__ | vars(builtins)) -def resolve_attribute_to_object(attribute: str, scope: ScopeProxy) -> object: +def resolve_attribute_to_object(attribute: str, scope: ScopeProxy | object) -> object: obj = None try: for attr in attribute.split("."): From 333d96778f5527c834cee153fb94cb38a611e358 Mon Sep 17 00:00:00 2001 From: Sam Dareska <37879103+samwaseda@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:44:52 +0100 Subject: [PATCH 3/5] Update flowrep/models/parsers/object_scope.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- flowrep/models/parsers/object_scope.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flowrep/models/parsers/object_scope.py b/flowrep/models/parsers/object_scope.py index 6fdbcb64..68a9d68c 100644 --- a/flowrep/models/parsers/object_scope.py +++ b/flowrep/models/parsers/object_scope.py @@ -47,7 +47,7 @@ def resolve_symbol_to_object( return resolve_symbol_to_object(node.value, scope, [node.attr] + _chain) else: raise TypeError( - f"Cannot resolve symbol {node} or the symbol chain '{'.'.join(_chain)}'. " - f"Expected an ast.Name or chain of ast.Attribute and ast.Name, but got " - f"{node}." + f"Cannot resolve symbol {node} while building the symbol chain " + f"'{'.'.join(_chain)}'. Expected an ast.Name or chain of ast.Attribute " + f"and ast.Name, but got {node}." ) From e6d83050c25323df7dd3cd2d5a920e27f90b2593 Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Thu, 26 Feb 2026 15:05:38 +0100 Subject: [PATCH 4/5] add docstring --- flowrep/models/parsers/object_scope.py | 29 +++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/flowrep/models/parsers/object_scope.py b/flowrep/models/parsers/object_scope.py index 68a9d68c..54dc8edc 100644 --- a/flowrep/models/parsers/object_scope.py +++ b/flowrep/models/parsers/object_scope.py @@ -25,6 +25,19 @@ def get_scope(func: FunctionType) -> ScopeProxy: def resolve_attribute_to_object(attribute: str, scope: ScopeProxy | object) -> object: + """ + Resolve a dot-separated attribute string to the actual object it references in the + given scope. For example, if attribute is "os.path.join", this function will + return the actual join function from the os.path module. + + Args: + attribute: A dot-separated string representing the attribute to resolve. + scope: The scope in which to resolve the attribute. This can be a ScopeProxy + or any object that supports attribute access. + + Returns: + The object that the attribute resolves to in the given scope. + """ obj = None try: for attr in attribute.split("."): @@ -39,7 +52,21 @@ def resolve_symbol_to_object( scope: ScopeProxy | object, _chain: list[str] | None = None, ) -> object: - """ """ + """ + Recursively resolve a symbol in the form of an ast.Name or ast.Attribute to the + actual object it references in the given scope. The _chain parameter is used + internally to keep track of the attribute chain being resolved, and should not + be provided by the caller. + + Args: + node: An ast.expr representing the symbol to resolve. Expected to be an + ast.Name or ast.Attribute. + scope: The scope in which to resolve the symbol. This can be a ScopeProxy + or any object that supports attribute access. + + Returns: + The object that the symbol resolves to in the given scope. + """ _chain = _chain or [] if isinstance(node, ast.Name): return resolve_attribute_to_object(".".join([node.id] + _chain), scope) From d75752c18c40c7fab442521197ee3a75a41c86dc Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Thu, 26 Feb 2026 15:35:36 +0100 Subject: [PATCH 5/5] Add one test --- tests/unit/models/parsers/test_object_scope.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/models/parsers/test_object_scope.py b/tests/unit/models/parsers/test_object_scope.py index b33d0194..7fc2bcb0 100644 --- a/tests/unit/models/parsers/test_object_scope.py +++ b/tests/unit/models/parsers/test_object_scope.py @@ -75,6 +75,11 @@ def test_unrecognized_node_raises(self): with self.assertRaises(TypeError): object_scope.resolve_symbol_to_object(node, scope) + def test_resolve_attribute_to_object(self): + scope = object_scope.ScopeProxy({"ast": ast}) + f = object_scope.resolve_attribute_to_object("ast.literal_eval", scope) + self.assertIs(f, ast.literal_eval) + if __name__ == "__main__": unittest.main()