From ae5ac10db8dd1a0480c0c59c4b7509930c5fa0a7 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:20:41 -0400 Subject: [PATCH] Apply model meta options as per-field overrides FlowOptionsOverride.get_options replaced the accumulated options with model.meta.options instead of applying only the fields it sets, as the docstring and the other option layers do. Setting a single field such as cacheable in a model's meta therefore discarded the globally configured evaluator, log level, and any options given to the Flow.call decorator, and the model fell back to the default logging evaluator. Among other effects, its calls lost graph evaluator node reuse, so a dependency marked uncacheable ran twice when its parent called it again. Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com> --- ccflow/callable.py | 2 +- ccflow/tests/test_decorator.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ccflow/callable.py b/ccflow/callable.py index 2f79a2d..4cd3ec0 100644 --- a/ccflow/callable.py +++ b/ccflow/callable.py @@ -440,7 +440,7 @@ def get_options(cls, model: CallableModelType, model_options: FlowOptions | None current_options = cls._apply_options(current_options, model_options) # Then apply the model meta-provided options if model.meta.options: - current_options = model.meta.options + current_options = cls._apply_options(current_options, model.meta.options) # Then apply all model-specific overrides for override in overrides: if any(model is m for m in override.models) or isinstance(model, override.model_types): diff --git a/ccflow/tests/test_decorator.py b/ccflow/tests/test_decorator.py index 5bea104..1d64332 100644 --- a/ccflow/tests/test_decorator.py +++ b/ccflow/tests/test_decorator.py @@ -219,6 +219,23 @@ def test_override_precedence(self): self.assertEqual(get_options(model), new_options) self.assertEqual(get_options(model), options) + def test_meta_keeps_unset_global_options(self): + # meta only overrides the fields it sets, so a globally configured evaluator still applies + evaluator = DummyEvaluator() + model = DefaultModel(meta={"options": {"cacheable": False}}) + get_options = DefaultModel.__call__.get_options + with FlowOptionsOverride(options=FlowOptions(evaluator=evaluator, cacheable=True, log_level=logging.WARNING)): + options = get_options(model) + self.assertIs(options.evaluator, evaluator) + self.assertEqual(options.log_level, logging.WARNING) + self.assertFalse(options.cacheable) + + def test_meta_keeps_unset_decorator_options(self): + model = MyModel(meta={"options": {"cacheable": True}}) + options = MyModel.__call__.get_options(model) + self.assertEqual(options.log_level, logging.WARNING) + self.assertTrue(options.cacheable) + class TestFlowOptionsOverride(TestCase): def setUp(self):