Summary
LazyRegistry (from #256) only materializes pending entries when the lookup key has no / separator. Any path containing a separator is delegated straight to ModelRegistry.__getitem__, which reads self._models directly and never materializes a pending head segment. As a result, a registry path whose leading segment is still pending fails with a KeyError, even though the same entry resolves fine when accessed by its bare name.
This breaks a common pattern: a model that exposes derived children through its own __getitem__ (so registry["/a/b/model/child"] resolves child through model.__getitem__). If model is a pending lazy entry, the trailing-segment lookup can't reach it.
Reproduction
from omegaconf import OmegaConf
from ccflow import ModelRegistry
class Leaf(BaseModel):
tag: str = "leaf"
class Parent(BaseModel):
child: Leaf
def __getitem__(self, key):
if key == "derived":
return self.child
raise KeyError(key)
cfg = OmegaConf.create({
"grp": {
"_target_": "ccflow.base.LazyRegistry",
"_recursive_": False,
"leaf": {"_target_": "...Leaf"},
"parent": {"_target_": "...Parent", "child": "/grp/leaf"},
}
})
root = ModelRegistry.root()
root.load_config(cfg)
# Works: bare-name access materializes the pending entry
root["/grp/parent"] # OK
# Fails on a *cold* registry (parent still pending):
root.clear(); root.load_config(cfg)
root["/grp/parent/derived"]
# KeyError: No sub-registry found by the name 'parent' in registry '/grp' ...
Accessing /grp/parent first, then /grp/parent/derived, succeeds — so it's specifically the cold path (pending head segment) that breaks.
Cause
def __getitem__(self, item) -> ModelType:
if REGISTRY_SEPARATOR in item:
return super().__getitem__(item) # <-- never materializes a pending head
return self._materialize(item) if item in self._pending else super().__getitem__(item)
When item contains /, the leading segment isn't materialized before delegating to the base lookup.
Suggested fix
Split off the leading segment; if it's a pending entry, materialize it (which registers it in _models) before delegating the remainder:
def __getitem__(self, item) -> ModelType:
if REGISTRY_SEPARATOR in item:
head, _, rest = item.partition(REGISTRY_SEPARATOR)
if head != "" and head in self._pending:
self._materialize(head)
return super().__getitem__(item)
return self._materialize(item) if item in self._pending else super().__getitem__(item)
(The head != "" guard preserves the absolute /... → root behavior.) A regression test covering registry["/grp/parent/derived"] on a freshly loaded registry would lock this in.
Impact
Any lazy registry whose entries expose derived sub-paths via __getitem__, or any nested-path access where the parent hasn't been touched yet, is affected. Iteration/in on the lazy registry are consistent with the pending model (they just don't enumerate not-yet-materialized children), so this only bites on direct path access.
Summary
LazyRegistry(from #256) only materializes pending entries when the lookup key has no/separator. Any path containing a separator is delegated straight toModelRegistry.__getitem__, which readsself._modelsdirectly and never materializes a pending head segment. As a result, a registry path whose leading segment is still pending fails with aKeyError, even though the same entry resolves fine when accessed by its bare name.This breaks a common pattern: a model that exposes derived children through its own
__getitem__(soregistry["/a/b/model/child"]resolveschildthroughmodel.__getitem__). Ifmodelis a pending lazy entry, the trailing-segment lookup can't reach it.Reproduction
Accessing
/grp/parentfirst, then/grp/parent/derived, succeeds — so it's specifically the cold path (pending head segment) that breaks.Cause
When
itemcontains/, the leading segment isn't materialized before delegating to the base lookup.Suggested fix
Split off the leading segment; if it's a pending entry, materialize it (which registers it in
_models) before delegating the remainder:(The
head != ""guard preserves the absolute/...→ root behavior.) A regression test coveringregistry["/grp/parent/derived"]on a freshly loaded registry would lock this in.Impact
Any lazy registry whose entries expose derived sub-paths via
__getitem__, or any nested-path access where the parent hasn't been touched yet, is affected. Iteration/inon the lazy registry are consistent with the pending model (they just don't enumerate not-yet-materialized children), so this only bites on direct path access.