Roadmap · Developer UX · P2 · S

--reload for live dev

A build-it-yourself guide. Iterating on a registry today means kill-the-server, edit, restart. This adds --reload to modulearn run so saving app.py restarts it automatically — the same loop web devs expect.

Done when: modulearn run app.py --reload serves the app and, when you edit and save app.py, the server restarts and the change is live without you touching the terminal.
0 / 0 steps

The gotcha

Plain modulearn run hands uvicorn a live app object (uvicorn.run(app, …)). Reload can't work that way: uvicorn's reloader restarts your program in a fresh subprocess, so it needs an import string ("app:app") it can re-import from scratch — and that subprocess must be able to find your module. Two things make it work:

  1. Import string, not objectf"{path.stem}:{attr}".
  2. PYTHONPATH — put the file's directory on it so the reloaded subprocess can import the module by name; reload_dirs tells uvicorn which folder to watch.
Verified. Run against the live server: editing app.py's title while --reload was active flipped /api/meta from reloadcheckRELOADED with no restart, and the log showed StatReload detected changes in 'app.py'. Reloading…

Steps

  1. Add a reload-aware serve function
    modulearn/cli.py — next to _serve

    It resolves the target to an import string, exports PYTHONPATH so the subprocess can import it, and hands uvicorn the string with reload=True.

    def _serve_reload(target, host, port):
        """Serve with uvicorn's autoreloader. Unlike _serve, this passes an import
        STRING (uvicorn re-imports in a fresh subprocess on every change), so the
        module's directory must be importable there — hence PYTHONPATH."""
        import os
        import uvicorn
        path_str, _, attr = target.partition(":")
        attr = attr or "app"
        path = Path(path_str).resolve()
        if not path.exists():
            sys.exit(f"modulearn: no such file: {path_str}")
        os.environ["PYTHONPATH"] = (
            str(path.parent) + os.pathsep + os.environ.get("PYTHONPATH", ""))
        url = f"http://{'localhost' if host in ('0.0.0.0', '') else host}:{port}"
        print(f"ModuLearn (reload) → {url}  watching {path.parent}  (Ctrl-C to stop)")
        uvicorn.run(f"{path.stem}:{attr}", host=host, port=port,
                    reload=True, reload_dirs=[str(path.parent)])
  2. Add the --reload flag
    modulearn/cli.py — on the run subparser
        p_run.add_argument("--reload", action="store_true",
                           help="restart the server when the app file changes (dev)")
  3. Branch in the dispatch
    modulearn/cli.py — the run arm of main()
        elif args.command == "run":
            if args.reload:
                _serve_reload(args.target, args.host, args.port)
            else:
                _serve(_load_app(args.target), args.host, args.port)
  4. Verify the live loop
    modulearn run app.py --reload      # serves, prints "watching <dir>"
    # in the app, change  title="..."  and save
    # terminal logs: StatReload detected changes in 'app.py'. Reloading...
    # refresh the browser — the new title is live
    Watch the whole project, not one file. reload_dirs is the file's directory, so edits to sibling modules your app imports also trigger a restart — which is what you want once an app grows past one file.
  5. Commit + note it in the docstring

    Add --reload to the usage lines in the cli.py module docstring, then:

    git add modulearn/cli.py
    git commit -m "Add --reload to modulearn run"
    git push
    gh run list --branch main --limit 1   # expect: success
  6. Mark it shipped on the roadmap

    Add done:true, to the {area:"dx", … title:"--reload for live development"} item in docs/roadmap.html.

Worth knowing

Reload is a dev tool, not a prod one. The reloader polls the filesystem and holds a supervising process — fine for local iteration, wasteful in production. Keep the plain modulearn run (no flag) as the default so nobody ships with it on. For faster change detection, pip install watchfiles and uvicorn uses it automatically instead of stat-polling.