Skip to content

src/genro_asgi/reloading.py

Source from this local checkout, regenerated when the reader rebuilds. Line links use #L<number>; a GitHub line range opens its first line.

1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 #     https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """The reload launch, reachable by any launcher (#39).16 17 ``serve_reloading`` boots a server under uvicorn's reload supervisor: the18 caller names WHAT to rebuild (a ``config`` path or an ``application`` target —19 the very keys ``factory()`` reads back) and WHAT IS WATCHED (``reload_dirs``,20 plural, with ``reload_excludes`` when a watched tree is written into at21 runtime). Nothing is derived here: whoever ships a recipe inside a package22 knows that watching the recipe's own directory is useless — what its23 developers edit is the site, and only the caller knows where that lives.24 25 The supervisor accepts only an import string, so the description crosses the26 process boundary as one JSON object in ``LAUNCHER_ENV`` and27 ``genro_asgi.__main__:factory`` rebuilds the very same server on every28 restart — with ``shutdown_mode = QUITTING``, so every reload exit takes the29 soft quit and the population survives the restart.30 31 This package's own CLI is one caller among others: ``genro-asgi serve32 --reload`` keeps deriving its default watch root from the source file it was33 given, and hands the result here.34 """35 36 from __future__ import annotations37 38 import json39 import os40 41 import uvicorn42 43 LAUNCHER_ENV = "GENRO_ASGI_LAUNCHER"44 """The variable carrying the launcher's state across the reload process boundary."""45 46 FACTORY_TARGET = "genro_asgi.__main__:factory"47 """The import string the supervisor rebuilds from, in every restarted process."""48 49 __all__ = ["FACTORY_TARGET", "LAUNCHER_ENV", "serve_reloading"]50 51 52 def serve_reloading(53     *,54     host: str,55     port: int,56     reload_dirs: list[str],57     reload_excludes: list[str] | None = None,58     config: str | None = None,59     application: str | None = None,60     save_session: str | None = None,61     debug: bool | str = False,62 ) -> None:63     """Boot under the reload supervisor, watching the roots the caller names.64 65     Args:66         host: the bind host.67         port: the bind port.68         reload_dirs: the directories whose ``*.py`` changes restart the child.69         reload_excludes: patterns the watcher must ignore — a site that writes70             inside its own tree at runtime says so here.71         config: the config.py path ``factory()`` rebuilds from.72         application: the quickstart target, when there is no config; exactly73             one of the two must be given.74         save_session: the session snapshot file, when a named serve armed one.75         debug: the declared usage mode, carried to the rebuilt server as is.76 77     Raises:78         ValueError: neither or both of ``config`` and ``application``.79 80     Blocks until the supervisor ends. Every restarted child is rebuilt by81     ``factory()`` from what this wrote in the environment.82     """83     if (config is None) == (application is None):84         raise ValueError("exactly one of 'config' and 'application' must be given")85     payload: dict[str, object] = {"host": host, "port": port}86     if config is not None:87         payload["config"] = config88     else:89         payload["application"] = application90     if save_session is not None:91         payload["save_session"] = save_session92     if debug is not False:93         payload["debug"] = debug94     os.environ[LAUNCHER_ENV] = json.dumps(payload)95     uvicorn.run(96         FACTORY_TARGET,97         factory=True,98         reload=True,99         reload_dirs=reload_dirs,100         reload_excludes=reload_excludes,101         host=host,102         port=port,103     )