modulearn init scaffoldA build-it-yourself guide. You assembled ~/modTest by hand —
a directory, a venv, an app.py you pasted from the README. This adds a third
subcommand that does it in one line, so a newcomer goes from
pip install modulearn to a running editor without copying anything.
modulearn init myproj writes a runnable
starter project, modulearn run myproj/app.py serves it immediately, and a
second init over an existing project refuses rather than clobbering it.Everything lives in one file — modulearn/cli.py — which
already has the demo and run subcommands. You're adding a third the
same way: templates + an _init() function + a subparser + one dispatch line.
Because the package installs as a console script from source, an editable install
(pip install -e .) picks up your edits with no reinstall.
cli.py: --help lists {demo,run,init}, both
init myproj and bare init scaffold correctly, the generated app
serves (6 nodes) and trains to completion, and two pytest cases pass.
_load_appThe starter app is a toy loss curve so modulearn run works with zero extra
deps — the user swaps on_train for the real thing. __TITLE__ is
replaced with the project name (brace-safe, unlike str.format).
# The starter app the scaffold writes. __TITLE__ is replaced with the project
# name. A toy loss curve so `modulearn run app.py` works with zero extra deps.
_APP_TEMPLATE = '''\
"""__TITLE__ — a ModuLearn project.
Serve it:
modulearn run app.py # open http://localhost:8000
python app.py # same thing, directly
"""
import math
import random
import time
from modulearn import Registry, Param, create_app
reg = Registry()
reg.add_dataset("demo", title="Demo data", kind="tabular",
features=["x1", "x2", "x3"], targets=["y"])
reg.add_model("mlp", title="MLP", requires_kind="tabular",
params=[Param("hidden", "hidden layers", "int_list", [32, 16])])
reg.add_hyperparameter("lr", label="learning rate", default=1e-3, min=1e-6, max=1.0)
reg.add_hyperparameter("epochs", label="epochs", kind="int", default=50, min=1, max=1000)
reg.add_loss([Param("loss", "kind", "enum", "mse", choices=["mse", "cross_entropy"])])
def on_train(compiled, reporter):
epochs = int(compiled.hyperparameters.get("epochs", 50))
lr = float(compiled.hyperparameters.get("lr", 1e-3))
reporter.state(epochs=epochs)
best = float("inf")
for e in range(epochs):
t = e / max(1, epochs - 1)
train = 0.2 + 1.3 * math.exp(-4 * lr * 100 * t) + random.uniform(-0.02, 0.02)
val = train + 0.05 + random.uniform(0, 0.03)
best = min(best, val)
reporter.metric(epoch=e, train=round(train, 4), val=round(val, 4))
reporter.state(epoch=e, best_val=round(best, 4))
time.sleep(0.03)
reporter.state(phase="done", test_score=round(best + 0.02, 4))
app = create_app(reg, on_train, title="__TITLE__")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
'''
_README_TEMPLATE = '''\
# __TITLE__
A [ModuLearn](https://pypi.org/project/modulearn/) project.
```bash
pip install modulearn
modulearn run app.py # open http://localhost:8000
```
Edit `app.py`: declare datasets/models/hyperparameters in the `Registry`, and put
your training loop in `on_train`.
'''
_GITIGNORE = "runs/\n__pycache__/\n*.pyc\n.venv/\n"
__TITLE__ and not {title}: the app
template is full of {} — dict literals, no less — so str.format
would choke. A plain .replace() sidesteps it.It creates the directory (or uses the current one for .), writes the
three files, and — critically — refuses to overwrite an existing app.py. README
and .gitignore are only written if absent, so re-running in a real project is safe.
def _init(name: str):
"""Scaffold a starter project. ``name`` is a directory (created if needed);
``.`` scaffolds into the current directory. Never overwrites an existing app.py."""
if name in (".", "./"):
target = Path.cwd()
else:
target = Path(name).resolve()
target.mkdir(parents=True, exist_ok=True)
title = target.name
app_py = target / "app.py"
if app_py.exists():
sys.exit(f"modulearn: {app_py} already exists — refusing to overwrite")
app_py.write_text(_APP_TEMPLATE.replace("__TITLE__", title))
for fname, body in (("README.md", _README_TEMPLATE.replace("__TITLE__", title)),
(".gitignore", _GITIGNORE)):
p = target / fname
if not p.exists(): # respect files the user already has
p.write_text(body)
where = "." if target == Path.cwd() else name
print(f"Created ModuLearn project in {where}/")
print("Next:")
if where != ".":
print(f" cd {where}")
print(" modulearn run app.py # open http://localhost:8000")
main(), after the run subparsername is optional and defaults to ., so a bare
modulearn init scaffolds into the current directory.
p_init = sub.add_parser("init", help="scaffold a new ModuLearn project")
p_init.add_argument("name", nargs="?", default=".",
help="project directory to create (default: current dir)")
main(), alongside the other commands elif args.command == "init":
_init(args.name)
An editable install means no reinstall — the command reflects your edits immediately.
modulearn --help # {demo,run,init}
modulearn init /tmp/mltest # scaffolds the project
modulearn run /tmp/mltest/app.py # serves at http://localhost:8000
modulearn init /tmp/mltest # run again → refuses to overwrite
Open the served editor, drop Demo data → MLP → Train, and hit ▶ to confirm the scaffolded app actually trains.
tmp_path keeps it hermetic — no files touched outside the test.
"""Test the init scaffold: it writes a runnable app and won't clobber existing files."""
import pytest
from modulearn.cli import main
def test_init_scaffolds(tmp_path):
proj = tmp_path / "myproj"
main(["init", str(proj)])
assert (proj / "app.py").is_file()
assert (proj / "README.md").is_file()
assert 'title="myproj"' in (proj / "app.py").read_text()
def test_init_refuses_overwrite(tmp_path):
proj = tmp_path / "p2"
main(["init", str(proj)])
with pytest.raises(SystemExit):
main(["init", str(proj)]) # app.py already exists
pytest tests/test_cli_init.py -q # 2 passed
pytest -q # whole suite
Two small touches so the feature is discoverable: mention modulearn init in
the cli.py module docstring at the top, and add a line to the README's install
section — modulearn init myproject as the fastest way to start a real app.
git add modulearn/cli.py tests/test_cli_init.py README.md
git commit -m "Add `modulearn init` project scaffold"
git push
gh run list --branch main --limit 1 # expect: success
In docs/roadmap.html, add done:true, to the
{area:"dx", … title:"modulearn init scaffold"} item. Developer UX ticks to 1/4.
on_train is a toy curve, not scikit-learn, so
init → run → ▶ Train works the instant it's created, with nothing to install.
A newcomer sees a live learning curve in under a minute; the comment tells them exactly which
function to replace. If you'd rather ship a real-model starter, add a
--template regression flag later — but the zero-friction default is the right
first impression.