Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions ccflow/evaluators/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"MultiEvaluator",
"cache_key",
"combine_evaluators",
"effective_cache_key",
"get_dependency_graph",
]

Expand Down Expand Up @@ -336,6 +337,31 @@ def cache_key(flow_obj: ModelEvaluationContext | ContextBase | CallableModel, *,
raise TypeError(f"object of type {type(flow_obj)} cannot be serialized by this function!")


def effective_cache_key(model: CallableModel, context: ContextBase) -> bytes:
"""Returns the effective identity key for evaluating ``model`` on ``context``.

This is the ``(model, context)`` counterpart to ``cache_key(mec, effective=True)``, for callers
that hold a model and a context directly rather than a ``ModelEvaluationContext``. Models that
declare an effective identity (by returning a non-``None`` identity payload) are keyed by it;
models that do not fall back to the structural ``cache_key(model)``, so the result is byte-for-byte
identical to ``cache_key(model)`` for such models.

Unlike ``cache_key(mec, effective=True)``, this returns the model-level effective key without the
surrounding evaluation-context envelope (``fn`` and ``options``), and it uses the structural
``cache_key(model)`` (rather than a context-dependent key) for models that do not opt in.

Args:
model: The model whose identity is being tokenized.
context: The context the model would be evaluated on; it is passed to the model's identity
payload. Models that do not opt into effective identity ignore it.
"""
try:
key = _effective_model_key(model, context, {}, set())
except _EffectiveEvaluationKeyUnavailable:
key = None
return key if key is not None else cache_key(model)


class MemoryCacheEvaluator(EvaluatorBase):
"""Evaluator that caches results in memory."""

Expand Down
49 changes: 49 additions & 0 deletions ccflow/tests/evaluators/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
MultiEvaluator,
cache_key,
combine_evaluators,
effective_cache_key,
get_dependency_graph,
)

Expand Down Expand Up @@ -739,3 +740,51 @@ def test_graph_evaluator_circular(self):
evaluator = GraphEvaluator()
with FlowOptionsOverride(options={"evaluator": evaluator}):
self.assertRaises(Exception, root, context) # noqa: B017


class _OffsetIdentityCallable(MyDateCallable):
"""Opt-in model whose effective identity depends only on ``offset``, not on the context date."""

def _evaluation_identity_payload(self, context):
return {"offset": self.offset}


class TestEffectiveCacheKey(TestCase):
def test_opt_out_matches_structural_cache_key(self):
"""Models that do not opt in fall back byte-for-byte to cache_key(model)."""
model = MyDateCallable(offset=1)
context = DateContext(date=date(2022, 1, 1))
self.assertEqual(effective_cache_key(model, context), cache_key(model))

def test_opt_out_is_context_independent(self):
model = MyDateCallable(offset=1)
c1 = DateContext(date=date(2022, 1, 1))
c2 = DateContext(date=date(2022, 1, 2))
self.assertEqual(effective_cache_key(model, c1), effective_cache_key(model, c2))

def test_opt_in_uses_identity_payload(self):
"""An opt-in model is keyed by its identity payload, not the structural key."""
model = _OffsetIdentityCallable(offset=1)
c1 = DateContext(date=date(2022, 1, 1))
c2 = DateContext(date=date(2022, 1, 2))
# The payload ignores the date, so the two contexts collapse to one key ...
self.assertEqual(effective_cache_key(model, c1), effective_cache_key(model, c2))
# ... and that key differs from the structural cache_key(model).
self.assertNotEqual(effective_cache_key(model, c1), cache_key(model))

def test_opt_in_distinguishes_payload_relevant_change(self):
context = DateContext(date=date(2022, 1, 1))
self.assertNotEqual(
effective_cache_key(_OffsetIdentityCallable(offset=1), context),
effective_cache_key(_OffsetIdentityCallable(offset=2), context),
)

def test_identity_payload_errors_propagate(self):
"""A failing identity payload surfaces rather than being hidden by the structural fallback."""

class _BadIdentityCallable(MyDateCallable):
def _evaluation_identity_payload(self, context):
raise ValueError("identity broke")

with self.assertRaisesRegex(ValueError, "identity broke"):
effective_cache_key(_BadIdentityCallable(offset=1), DateContext(date=date(2022, 1, 1)))