The roadmap's own note on this item: "only worth it once real usage shows which
parts of the pattern actually chafe." So this isn't a build-it recipe like the others — it's
an honest look at whether the current add_* API is actually a problem, three
concrete alternatives with working prototypes, and a recommendation.
Be specific before changing anything. Here's a real registry today:
reg = Registry()
reg.add_dataset("iris", title="Iris", kind="tabular",
features=["a", "b", "c", "d"], targets=["species"])
reg.add_model("mlp", title="MLP", requires_kind="tabular",
params=[Param("hidden", "hidden layers", "int_list", [64, 32])])
reg.add_hyperparameter("lr", label="learning rate", default=1e-3, min=1e-6, max=1.0)
reg.add_loss([Param("loss", "kind", "enum", "mse", choices=["mse", "cross_entropy"])])
Honestly? Not much. It's linear, greppable, and every keyword tells you what it means. The one real friction is that it's imperative Python — you can't hand a registry to a non-programmer as data, generate it from a config file, or diff two registries as text. That's a narrow but real use case, and it points at exactly one addition.
@reg.dataset("iris", kind="tabular", features=[...], targets=["species"])
def iris(): ... # the loader IS the declaration
add_*, i.e. two ways to do itLOADERS[compiled.dataset]) already solves co-location without new APIadd_* methods stay,
this just calls them for you.@classmethod
def from_config(cls, config: dict) -> "Registry":
"""Build a Registry from a declarative dict (e.g. loaded from YAML)."""
reg = cls()
for name, d in config.get("datasets", {}).items():
reg.add_dataset(name, **d)
for name, d in config.get("models", {}).items():
params = [Param(**p) for p in d.pop("params", [])]
reg.add_model(name, params=params, **d)
for name, d in config.get("hyperparameters", {}).items():
reg.add_hyperparameter(name, **d)
if "loss" in config:
reg.add_loss([Param(**p) for p in config["loss"]])
for name, d in config.get("transforms", {}).items():
reg.add_transform(name, **d)
return reg
reg = Registry.from_config({
"datasets": {"iris": {"title": "Iris", "kind": "tabular",
"features": ["a","b","c","d"], "targets": ["species"]}},
"models": {"mlp": {"title": "MLP", "requires_kind": "tabular",
"params": [{"name":"hidden","label":"hidden layers",
"kind":"int_list","default":[64,32]}]}},
"hyperparameters": {"lr": {"label":"learning rate","default":1e-3,"min":1e-6,"max":1.0}},
"loss": [{"name":"loss","label":"kind","kind":"enum","default":"mse",
"choices":["mse","cross_entropy"]}],
})
json.dumps(a, sort_keys=True) ==
json.dumps(b, …)).Param-as-dict is its own small verbosityadd_* API is explicit, uniform across all node
types, greppable, and type-checked by your editor. "Verbose" and "bad" aren't the same thing
— a registry read top-to-bottom is a feature. Until a real user hits a wall, adding API is
pure surface area to maintain and document.from_config classmethod into
modulearn/registry.py, add a test that asserts a small config's
catalog() equals the equivalent imperative build, document it in the README beside
the "two things" section, and mark the roadmap item done:true. Keep the
add_* methods exactly as they are — from_config is sugar on top, never
a replacement.