Skip to content

Commit ba60397

Browse files
authored
Merge pull request #161 from pyiron/resolve
[refactor] Separate transformation to object from ast parsing
2 parents c277da5 + d75752c commit ba60397

2 files changed

Lines changed: 51 additions & 13 deletions

File tree

flowrep/models/parsers/object_scope.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,57 @@ def get_scope(func: FunctionType) -> ScopeProxy:
2424
return ScopeProxy(inspect.getmodule(func).__dict__ | vars(builtins))
2525

2626

27+
def resolve_attribute_to_object(attribute: str, scope: ScopeProxy | object) -> object:
28+
"""
29+
Resolve a dot-separated attribute string to the actual object it references in the
30+
given scope. For example, if attribute is "os.path.join", this function will
31+
return the actual join function from the os.path module.
32+
33+
Args:
34+
attribute: A dot-separated string representing the attribute to resolve.
35+
scope: The scope in which to resolve the attribute. This can be a ScopeProxy
36+
or any object that supports attribute access.
37+
38+
Returns:
39+
The object that the attribute resolves to in the given scope.
40+
"""
41+
obj = None
42+
try:
43+
for attr in attribute.split("."):
44+
obj = getattr(obj or scope, attr)
45+
return obj
46+
except AttributeError as e:
47+
raise ValueError(f"Could not find attribute '{attr}' of {attribute}") from e
48+
49+
2750
def resolve_symbol_to_object(
2851
node: ast.expr, # Expecting a Name or Attribute here, and will otherwise TypeError
2952
scope: ScopeProxy | object,
3053
_chain: list[str] | None = None,
3154
) -> object:
32-
""" """
55+
"""
56+
Recursively resolve a symbol in the form of an ast.Name or ast.Attribute to the
57+
actual object it references in the given scope. The _chain parameter is used
58+
internally to keep track of the attribute chain being resolved, and should not
59+
be provided by the caller.
60+
61+
Args:
62+
node: An ast.expr representing the symbol to resolve. Expected to be an
63+
ast.Name or ast.Attribute.
64+
scope: The scope in which to resolve the symbol. This can be a ScopeProxy
65+
or any object that supports attribute access.
66+
67+
Returns:
68+
The object that the symbol resolves to in the given scope.
69+
"""
3370
_chain = _chain or []
34-
error_suffix = f" while attempting to resolve the symbol chain '{'.'.join(_chain)}'"
3571
if isinstance(node, ast.Name):
36-
attr = node.id
37-
try:
38-
obj = getattr(scope, attr)
39-
for attr in _chain:
40-
obj = getattr(obj, attr)
41-
return obj
42-
except AttributeError as e:
43-
raise ValueError(f"Could not find attribute '{attr}' {error_suffix}") from e
72+
return resolve_attribute_to_object(".".join([node.id] + _chain), scope)
4473
elif isinstance(node, ast.Attribute):
4574
return resolve_symbol_to_object(node.value, scope, [node.attr] + _chain)
4675
else:
4776
raise TypeError(
48-
f"Cannot resolve symbol {node} {error_suffix}. "
49-
f"Expected an ast.Name or chain of ast.Attribute and ast.Name, but got "
50-
f"{node}."
77+
f"Cannot resolve symbol {node} while building the symbol chain "
78+
f"'{'.'.join(_chain)}'. Expected an ast.Name or chain of ast.Attribute "
79+
f"and ast.Name, but got {node}."
5180
)

tests/unit/models/parsers/test_object_scope.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,12 @@ def test_unrecognized_node_raises(self):
7474
node = ast.Constant(value=42)
7575
with self.assertRaises(TypeError):
7676
object_scope.resolve_symbol_to_object(node, scope)
77+
78+
def test_resolve_attribute_to_object(self):
79+
scope = object_scope.ScopeProxy({"ast": ast})
80+
f = object_scope.resolve_attribute_to_object("ast.literal_eval", scope)
81+
self.assertIs(f, ast.literal_eval)
82+
83+
84+
if __name__ == "__main__":
85+
unittest.main()

0 commit comments

Comments
 (0)