Skip to content

Conversation

@romain-intel
Copy link
Contributor

No description provided.

@romain-intel romain-intel changed the base branch from master to fix/support_editable_pkgs_more October 16, 2025 09:52
@romain-intel romain-intel force-pushed the fix/multi_inheritance branch from ad214ed to cc637df Compare October 17, 2025 20:13
Base automatically changed from fix/support_editable_pkgs_more to master October 21, 2025 17:00
Flow decorators were not properly merged in the case of multiple
inheritance. For example:

@project(name="foobar")
class A(FlowSpec):
  pass

class B(A):
  pass

class C(B):
  pass

Would result in the `project` decorator being included twice.

This PR fixes this behavior and should support any arbitrary inheritance
pattern. It also cleans up _flow_state (moving _flow_decorators into it)
and fixes some other corner cases (I think). In particular, configs are now
no longer merged as they are naturally inherited.
@romain-intel romain-intel force-pushed the fix/multi_inheritance branch from cc637df to bd13ddf Compare October 30, 2025 02:48
Copy link
Collaborator

@valayDave valayDave left a comment

Choose a reason for hiding this comment

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

Okay I went and I test drove this. There are some changes needed since this currently actively breaks current the inheritance. The bug was very subtle but the suggestion I have added in the PR explains the only line of code we need to add to fix it!

I have currently tested the basic inheritance model. I will add one more review on thorougher testing with deployer / runner APIS alongside inheritance.

Comment on lines 421 to 454
if cls._flow_state[FlowStateItems.CACHED_PARAMETERS] is not None:
cls._flow_state[FlowStateItems.CACHED_PARAMETERS] = None
else:
raise MetaflowInternalError(
"A non FlowMutator found in flow custom decorators"
)

for step in cls._steps:
for deco in step.config_decorators:
if isinstance(deco, StepMutator):
inserted_by_value = [deco.decorator_name] + (deco.inserted_by or [])
debug.userconf_exec(
"Evaluating step level decorator %s for %s (pre-mutate)"
% (deco.__class__.__name__, step.name)
)
deco.pre_mutate(
MutableStep(
cls,
step,
pre_mutate=True,
statically_defined=deco.statically_defined,
inserted_by=inserted_by_value,
)
)
else:
raise MetaflowInternalError(
"A non StepMutator found in step custom decorators"
)

# Process parameters to allow them to also use config values easily
for var, param in cls._get_parameters():
if param.IS_CONFIG_PARAMETER:
continue
param.init()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Simple inheritance based flows crashing in the _process_config_decorators. trivial Example to repro:

# inheritted.py
from metaflow import (
    FlowSpec,
    Config,
    pyproject_toml_parser,
    FlowMutator,
    pypi_base,
)


class project_pypi(FlowMutator):
    def pre_mutate(self, mutable_flow):
        project_deps = dict(mutable_flow.configs).get("project_deps")
        if project_deps["packages"]:
            mutable_flow.add_decorator(
                pypi_base, deco_kwargs=project_deps, duplicates=mutable_flow.ERROR
            )



@project_pypi
class FBBase(FlowSpec):
    project_deps = Config(
        "project_deps", default="pyproject.toml", parser=pyproject_toml_parser
    )
# flow.py 
from inheritted import FBBase
from metaflow import step

class MyFlow(FBBase):
    
    @step
    def start(self):
        print("Hello World!")
        self.next(self.end)

    @step
    def end(self):
        print("Flow completed successfully!")
    
if __name__ == "__main__":
    MyFlow()

Crashing with stack trace:

Traceback (most recent call last):
  File "/home/ubuntu/metaflow/metaflow/cli.py", line 723, in main
    start(auto_envvar_prefix="METAFLOW", obj=state)
  File "/home/ubuntu/metaflow/metaflow/_vendor/click/core.py", line 829, in __call__
    return self.main(args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/metaflow/metaflow/_vendor/click/core.py", line 782, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/home/ubuntu/metaflow/metaflow/cli_components/utils.py", line 65, in invoke
    click.Command.invoke(self, ctx)
  File "/home/ubuntu/metaflow/metaflow/_vendor/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/metaflow/metaflow/_vendor/click/core.py", line 610, in invoke
    return callback(args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/metaflow/metaflow/_vendor/click/decorators.py", line 21, in new_func
    return f(get_current_context(), args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/metaflow/metaflow/cli.py", line 477, in start
    new_cls = ctx.obj.flow._process_config_decorators(config_options)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/metaflow/metaflow/flowspec.py", line 452, in _process_config_decorators
    if param.IS_CONFIG_PARAMETER:
       ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ubuntu/metaflow/metaflow/user_configs/config_parameters.py", line 164, in __getattr__
    raise AttributeError(key)
AttributeError: IS_CONFIG_PARAMETER

setattr(cls, var, val)

cls._flow_state[_FlowState.SET_CONFIG_PARAMETERS] = to_save_configs
cls._flow_state[FlowStateItems.SET_CONFIG_PARAMETERS] = to_save_configs
Copy link
Collaborator

Choose a reason for hiding this comment

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

Somehow cls._flow_state[FlowStateItems.SET_CONFIG_PARAMETERS] is returning [] even when to_save_configs has a list of tuples. Weird!

Comment on lines +101 to +118
def __getitem__(self, key):
# ORDER IS IMPORTANT: we use inherited first and extend by whatever is in
# the flowspec
if key in self._merged_data:
return self._merged_data[key]

# We haven't accessed this yet so compute it for the first time
self_value = self._self_data.get(key)
inherited_value = self._inherited.get(key)

if self_value is not None:
self._merged_data[key] = self._merge_value(inherited_value, self_value)
return self._merged_data[key]
elif key in self._self_data:
# Case of CACHED_PARAMETERS; a valid value is None. It is never inherited
self._merged_data[key] = None
return None
raise KeyError(key)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The root cause of this bug is here . This bug is triggered because self._merged_data can be set when the user does something like in L545 (ie doing a getter like cls._flow_state[_FlowState.SET_CONFIG_PARAMETERS] ).

This pattern is a little funky because based on how __getitem__ is written, if a key is accessed once then it will be set in the self._merge_data and once it is set in the self._merge_data then even if we call __setitem__, the __getitem__ will ONLY return the older value since it's in self._merge_data

setattr(cls, var, val)

cls._flow_state[_FlowState.SET_CONFIG_PARAMETERS] = to_save_configs
cls._flow_state[FlowStateItems.SET_CONFIG_PARAMETERS] = to_save_configs
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
cls._flow_state[FlowStateItems.SET_CONFIG_PARAMETERS] = to_save_configs
del cls._flow_state[FlowStateItems.SET_CONFIG_PARAMETERS]
cls._flow_state[FlowStateItems.SET_CONFIG_PARAMETERS] = to_save_configs

This suggestion fixes the bug I have mentioned here. The root cause of the bug is explained here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants