Skip to content

LazyRegistry: path lookups don't materialize a pending head segment #257

Description

@ptomecek

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions