Skip to content

DelayEvaluator.__copy__/__deepcopy__ drop saved_globals, breaking config_expr() with custom functions #3345

Description

@SahilKumar75

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions