Kludex / uvicorn-extensions

Uvicorn Extensions ⚡
https://kludex.github.io/uvicorn-extensions/
MIT License
7 stars 0 forks source link

Study `uvicorn-browser` #15

Open Kludex opened 1 year ago

Kludex commented 1 year ago

I've implemented uvicorn-browser some months ago, but the implementation is too naive. Should I improve it, and release it here as well?

florimondmanca commented 1 year ago

This prompted me to think about arel vs uvicorn-browser.

I never thought we could implement browser reload at the ASGI server level. That's pretty interesting. As I understand, uvicorn-reload wraps the uvicorn CLI and plugs onto the reload_dirs etc options.

arel uses a different approach. It does reload at the application level, rather than server level. My motivation was to make it work for any ASGI app -- and server. The counterpart is: users have to do a bit of setup themselves. 1/ Register the WebSocket endpoint on their app (eg add a WebSocketRoute(hot_reload) on Starlette), 2/ register the reload JS script in their HTML pages (eg Jinja base template).

Arel has dedicated path reloading options, which allows reloading the page when something other than Python files changes (eg a Jinja template, a JS script, etc). But this also brings a problem. If I'm also running uvicorn <app> --reload during development, and I change a Python file, then Uvicorn reloads itself first, which breaks any open WebSocket reload connection. And so the web page doesn't get reloaded. One has to refresh the page so the WebSocket connection is established again.

uvicorn-browser doesn't use WebSocket, instead it relies on selenium to actually control the browser. Which is again pretty interesting. Is that something other reloaders do, e.g. in JavaScript frameworks? I always thought they mainly maintained a WebSocket connection.

So, arel has a problem (what to do when the ASGI server reloads, which closes any open connections? [right?*]), uvicorn-browser has another (it's an ASGI server-specific implementation, not agnostic)... Is there a way to solve both of these problems to have some sort of "ultimate ASGI browser reload" solution?

Edit: one solution might be to simply augment arel's JS script to retry upon WebSocket disconnects, after all...

Kludex commented 1 year ago

Hold on Florimond, let me type, stop editing! hahaha

Kludex commented 1 year ago

First, thanks for coming here. I'm always happy to read your messages. :pray:

Yesterday, I was playing with arel, and yes, indeed the --reload problem happened, and indeed, I solved it relying on a different JS script.

This is where I stopped:

FastAPI Docs Reload ```python from __future__ import annotations from pathlib import Path import arel from fastapi import FastAPI from fastapi.openapi.docs import ( get_redoc_html, get_swagger_ui_html, get_swagger_ui_oauth2_redirect_html, ) from fastapi.openapi.utils import get_openapi from starlette.requests import Request from starlette.responses import HTMLResponse from starlette.routing import WebSocketRoute def script(url: str) -> str: return f""" """ def reload_docs_ui(app: FastAPI, paths: list[Path]) -> None: # if app.docs_url or app.redoc_url: # raise RuntimeError( # "You cannot use `reload_docs_ui` when you have `docs_url` or `redoc_url`.\n" # "On your `FastAPI` instance, set `FastAPI(docs_url=None, redoc_url=None)`." # ) hot_reload = arel.HotReload(paths=[arel.Path(str(path)) for path in paths]) hot_reload_route = WebSocketRoute("/hot-reload", hot_reload, name="hot-reload") app.router.routes.append(hot_reload_route) app.add_event_handler("startup", hot_reload.startup) app.add_event_handler("shutdown", hot_reload.shutdown) def custom_openapi(): return get_openapi( title=app.title, version=app.version, openapi_version=app.openapi_version, description=app.description, terms_of_service=app.terms_of_service, contact=app.contact, license_info=app.license_info, routes=app.routes, tags=app.openapi_tags, servers=app.servers, ) app.openapi = custom_openapi if app.openapi_url and app.docs_url: async def swagger_ui_html(req: Request) -> HTMLResponse: root_path = req.scope.get("root_path", "").rstrip("/") openapi_url = root_path + app.openapi_url oauth2_redirect_url = app.swagger_ui_oauth2_redirect_url if oauth2_redirect_url: oauth2_redirect_url = root_path + oauth2_redirect_url html_response = get_swagger_ui_html( openapi_url=openapi_url, title=app.title + " - Swagger UI", oauth2_redirect_url=oauth2_redirect_url, init_oauth=app.swagger_ui_init_oauth, swagger_ui_parameters=app.swagger_ui_parameters, ) return HTMLResponse( html_response.body.decode(html_response.charset) + script(req.url_for("hot-reload")) ) app.add_route("/potato", swagger_ui_html, include_in_schema=False) if app.swagger_ui_oauth2_redirect_url: async def swagger_ui_redirect(req: Request) -> HTMLResponse: return get_swagger_ui_oauth2_redirect_html() app.add_route( app.swagger_ui_oauth2_redirect_url, swagger_ui_redirect, include_in_schema=False, ) if app.openapi_url and app.redoc_url: async def redoc_html(req: Request) -> HTMLResponse: root_path = req.scope.get("root_path", "").rstrip("/") openapi_url = root_path + app.openapi_url return get_redoc_html(openapi_url=openapi_url, title=app.title + " - ReDoc") app.add_route(app.redoc_url, redoc_html, include_in_schema=False) ```

Application:

from pathlib import Path

from fastapi import FastAPI
from fastapi_docs_reload import reload_docs_ui

app = FastAPI()

# @app.get("/")
# def home():
#     ...

reload_docs_ui(app, [Path.cwd()])

Using uvicorn main:app --reload is possible with the above.

Kludex commented 1 year ago

It's kind of a draft, so if you try it, you see that I've hard coded the "/potato" endpoint.

Kludex commented 1 year ago

Can we create a middleware to inject the script when more_body is False or something like that? :thinking:

Like, to use with another ASGI frameworks.

florimondmanca commented 1 year ago

@Kludex Ah, well yes there's another limitation with arel: what about the pages where we can't inject the {{ script }} ourselves? FastAPI docs are a good example.

A middleware that injects the script into the HTML might be a possibility, yes.