What happened?
config_expr() is documented to support calling the user's own module level functions inside the expression, for example config_expr("my_func()"). It captures the caller's globals for this purpose:
parent_globals = inspect.currentframe().f_back.f_globals
...
saved_globals=parent_globals
DelayEvaluator.copy and deepcopy both construct a fresh DelayEvaluator using only the expression string, without forwarding the saved globals:
def __copy__(self):
c = DelayEvaluator(self._config_expr)
c._access = self._access.copy() if self._access is not None else None
# Globals are not copied -- always kept as a reference
return c
Since saved_globals defaults to None in init, the copy's _globals becomes None. getattr and getitem both call self.copy() on every attribute or item access, which is the common way DelayEvaluator is actually used, for example config_expr("config").project.name. So the moment any attribute or item is chained onto a DelayEvaluator built with config_expr(), the captured caller globals are lost.
What did you expect to happen?
A custom function referenced in a config_expr() expression should still resolve correctly after any attribute or item access on the returned DelayEvaluator, since the comment right above the affected line says globals are "always kept as a reference."
Reproduction
# in a flow's module
def my_func():
return "hello"
class MyFlow(FlowSpec):
config = Config("config")
@environment(vars={"foo": config_expr("my_func()")})
@step
def start(self):
...
This works. But:
@project(name=config_expr("my_func()").upper())
raises NameError: name 'my_func' is not defined, because .upper() triggers getattr, which calls copy(), which drops _globals.
Proposed fix
Pass self._globals through in both copy and deepcopy:
def __copy__(self):
c = DelayEvaluator(self._config_expr, saved_globals=self._globals)
...
def __deepcopy__(self, memo):
c = DelayEvaluator(self._config_expr, saved_globals=self._globals)
...
I have this fix ready locally along with a couple of unit tests reproducing the issue before the fix and passing after. Happy to open a PR once this is acknowledged.
What happened?
config_expr() is documented to support calling the user's own module level functions inside the expression, for example config_expr("my_func()"). It captures the caller's globals for this purpose:
DelayEvaluator.copy and deepcopy both construct a fresh DelayEvaluator using only the expression string, without forwarding the saved globals:
Since saved_globals defaults to None in init, the copy's _globals becomes None. getattr and getitem both call self.copy() on every attribute or item access, which is the common way DelayEvaluator is actually used, for example config_expr("config").project.name. So the moment any attribute or item is chained onto a DelayEvaluator built with config_expr(), the captured caller globals are lost.
What did you expect to happen?
A custom function referenced in a config_expr() expression should still resolve correctly after any attribute or item access on the returned DelayEvaluator, since the comment right above the affected line says globals are "always kept as a reference."
Reproduction
This works. But:
raises NameError: name 'my_func' is not defined, because .upper() triggers getattr, which calls copy(), which drops _globals.
Proposed fix
Pass self._globals through in both copy and deepcopy:
I have this fix ready locally along with a couple of unit tests reproducing the issue before the fix and passing after. Happy to open a PR once this is acknowledged.