feat: support focused dev reload scopes

This commit is contained in:
2026-07-29 18:52:55 +02:00
parent 790790ab37
commit a80caf7933
3 changed files with 174 additions and 5 deletions

View File

@@ -37,6 +37,7 @@ class DevserverState:
config: GovoplanServerConfig
registry: PlatformRegistry
reload_dirs: list[str]
reload_module_ids: tuple[str, ...] | None
def _config_module_runtime_root(config_path: str | None) -> Path | None:
@@ -237,20 +238,44 @@ def build_reload_dirs(
config_path: str | None = None,
registry: PlatformRegistry | None = None,
extra_dirs: Sequence[str] = (),
module_ids: Sequence[str] | None = None,
) -> list[str]:
active_registry = registry or build_platform_registry(config.enabled_modules, manifest_factories=config.manifest_factories)
manifests = active_registry.manifests()
enabled_module_ids = {manifest.id for manifest in manifests}
selected_module_ids = (
enabled_module_ids
if module_ids is None
else {module_id.strip() for module_id in module_ids if module_id.strip()}
)
unknown_module_ids = selected_module_ids - enabled_module_ids
if unknown_module_ids:
raise SystemExit(
"Reload modules are not enabled: "
+ ", ".join(sorted(unknown_module_ids))
)
roots: list[Path | str] = []
roots.extend(_config_source_roots(config_path))
for factory in config.manifest_factories:
try:
manifest = factory()
except TypeError:
manifest = None
if (
module_ids is not None
and isinstance(manifest, ModuleManifest)
and manifest.id not in selected_module_ids
):
continue
roots.extend(_source_roots_for_object(factory))
roots.extend(_entry_point_source_roots(enabled_module_ids))
roots.extend(_entry_point_source_roots(selected_module_ids))
for manifest in manifests:
if manifest.id not in selected_module_ids:
continue
roots.extend(_manifest_source_roots(manifest))
roots.extend(extra_dirs)
@@ -265,11 +290,32 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser.add_argument("--port", type=int, default=8000, help="Port to bind. Default: 8000.")
parser.add_argument("--no-reload", action="store_true", help="Disable uvicorn reload.")
parser.add_argument("--reload-dir", action="append", default=[], help="Additional directory to watch. May be passed multiple times.")
reload_scope = parser.add_mutually_exclusive_group()
reload_scope.add_argument(
"--reload-module",
action="append",
default=None,
metavar="MODULE_ID",
help=(
"Watch only this enabled module in addition to core/config sources. "
"May be passed multiple times."
),
)
reload_scope.add_argument(
"--reload-core-only",
action="store_true",
help="Watch core/config sources but no optional module source trees.",
)
parser.add_argument("--smoke", action="store_true", help="Prepare runtime paths, run app startup, print effective paths, and exit without uvicorn.")
return parser.parse_args(argv)
def prepare_devserver(config_path: str | None, *, extra_reload_dirs: Sequence[str] = ()) -> DevserverState:
def prepare_devserver(
config_path: str | None,
*,
extra_reload_dirs: Sequence[str] = (),
reload_module_ids: Sequence[str] | None = None,
) -> DevserverState:
runtime_root = apply_runtime_defaults(config_path)
database_url = os.getenv("DATABASE_URL", "")
validate_sqlite_database_url(database_url)
@@ -291,7 +337,13 @@ def prepare_devserver(config_path: str | None, *, extra_reload_dirs: Sequence[st
)
enabled_modules = load_startup_enabled_modules(config.enabled_modules, available=available_modules)
registry = build_platform_registry(enabled_modules, manifest_factories=config.manifest_factories)
reload_dirs = build_reload_dirs(config, config_path=config_path, registry=registry, extra_dirs=extra_reload_dirs)
reload_dirs = build_reload_dirs(
config,
config_path=config_path,
registry=registry,
extra_dirs=extra_reload_dirs,
module_ids=reload_module_ids,
)
return DevserverState(
config_path=config_path,
runtime_root=runtime_root,
@@ -300,6 +352,11 @@ def prepare_devserver(config_path: str | None, *, extra_reload_dirs: Sequence[st
config=config,
registry=registry,
reload_dirs=reload_dirs,
reload_module_ids=(
None
if reload_module_ids is None
else tuple(sorted(set(reload_module_ids)))
),
)
@@ -320,6 +377,15 @@ def print_devserver_summary(state: DevserverState, *, app: str, no_reload: bool)
if no_reload:
print("Reload: disabled")
else:
if state.reload_module_ids is None:
print("Reload scope: core/config plus all enabled modules")
elif state.reload_module_ids:
print(
"Reload scope: core/config plus "
+ ", ".join(state.reload_module_ids)
)
else:
print("Reload scope: core/config only")
print("Reload dirs:")
for directory in state.reload_dirs:
print(f" - {directory}")
@@ -353,7 +419,14 @@ def main(argv: Sequence[str] | None = None) -> int:
os.environ["GOVOPLAN_SERVER_CONFIG"] = args.config
config_path = args.config or os.getenv("GOVOPLAN_SERVER_CONFIG")
state = prepare_devserver(config_path, extra_reload_dirs=args.reload_dir)
reload_module_ids: Sequence[str] | None = args.reload_module
if args.reload_core_only:
reload_module_ids = ()
state = prepare_devserver(
config_path,
extra_reload_dirs=args.reload_dir,
reload_module_ids=reload_module_ids,
)
print_devserver_summary(state, app=args.app, no_reload=args.no_reload)
if args.smoke: