|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import asyncio |
| 4 | +import warnings |
| 5 | +import webbrowser |
| 6 | +from importlib import import_module |
| 7 | +from typing import Any, Awaitable, TypeVar, runtime_checkable |
| 8 | + |
| 9 | +from typing_extensions import Protocol |
| 10 | + |
| 11 | +from idom.types import ComponentConstructor |
| 12 | + |
| 13 | +from .utils import find_available_port |
| 14 | + |
| 15 | + |
| 16 | +SUPPORTED_PACKAGES = ( |
| 17 | + "starlette", |
| 18 | + "fastapi", |
| 19 | + "sanic", |
| 20 | + "flask", |
| 21 | + "tornado", |
| 22 | +) |
| 23 | + |
| 24 | + |
| 25 | +def develop( |
| 26 | + component: ComponentConstructor, |
| 27 | + app: Any | None = None, |
| 28 | + host: str = "127.0.0.1", |
| 29 | + port: int | None = None, |
| 30 | + open_browser: bool = True, |
| 31 | +) -> None: |
| 32 | + """Run a component with a development server""" |
| 33 | + |
| 34 | + warnings.warn( |
| 35 | + "You are running a development server, be sure to change this before deploying in production!", |
| 36 | + UserWarning, |
| 37 | + stacklevel=2, |
| 38 | + ) |
| 39 | + |
| 40 | + implementation = _get_implementation(app) |
| 41 | + |
| 42 | + if app is None: |
| 43 | + app = implementation.create_development_app() |
| 44 | + |
| 45 | + implementation.configure_development_view(component) |
| 46 | + |
| 47 | + coros: list[Awaitable] = [] |
| 48 | + |
| 49 | + host = host |
| 50 | + port = port or find_available_port(host) |
| 51 | + server_did_start = asyncio.Event() |
| 52 | + |
| 53 | + coros.append( |
| 54 | + implementation.serve_development_app( |
| 55 | + app, |
| 56 | + host=host, |
| 57 | + port=port, |
| 58 | + did_start=server_did_start, |
| 59 | + ) |
| 60 | + ) |
| 61 | + |
| 62 | + if open_browser: |
| 63 | + coros.append(_open_browser_after_server(host, port, server_did_start)) |
| 64 | + |
| 65 | + asyncio.get_event_loop().run_forever(asyncio.gather(*coros)) |
| 66 | + |
| 67 | + |
| 68 | +async def _open_browser_after_server( |
| 69 | + host: str, |
| 70 | + port: int, |
| 71 | + server_did_start: asyncio.Event, |
| 72 | +) -> None: |
| 73 | + await server_did_start.wait() |
| 74 | + webbrowser.open(f"http://{host}:{port}") |
| 75 | + |
| 76 | + |
| 77 | +def _get_implementation(app: _App | None) -> _Implementation: |
| 78 | + implementations = _all_implementations() |
| 79 | + |
| 80 | + if app is None: |
| 81 | + return next(iter(implementations.values())) |
| 82 | + |
| 83 | + for cls in type(app).mro(): |
| 84 | + if cls in implementations: |
| 85 | + return implementations[cls] |
| 86 | + else: |
| 87 | + raise TypeError(f"No built-in and installed implementation supports {app}") |
| 88 | + |
| 89 | + |
| 90 | +def _all_implementations() -> dict[type[Any], _Implementation]: |
| 91 | + if not _INSTALLED_IMPLEMENTATIONS: |
| 92 | + for name in SUPPORTED_PACKAGES: |
| 93 | + try: |
| 94 | + module = import_module(f"idom.server.{name}") |
| 95 | + except ImportError: # pragma: no cover |
| 96 | + continue |
| 97 | + |
| 98 | + if not isinstance(module, _Implementation): |
| 99 | + raise TypeError(f"{module.__name__!r} is an invalid implementation") |
| 100 | + |
| 101 | + _INSTALLED_IMPLEMENTATIONS[module.SERVER_TYPE] = module |
| 102 | + |
| 103 | + if not _INSTALLED_IMPLEMENTATIONS: |
| 104 | + raise RuntimeError("No built-in implementations are installed") |
| 105 | + |
| 106 | + return _INSTALLED_IMPLEMENTATIONS |
| 107 | + |
| 108 | + |
| 109 | +_App = TypeVar("_App") |
| 110 | + |
| 111 | + |
| 112 | +@runtime_checkable |
| 113 | +class _Implementation(Protocol): |
| 114 | + |
| 115 | + APP_TYPE = type[Any] |
| 116 | + |
| 117 | + def create_development_app(self) -> Any: |
| 118 | + ... |
| 119 | + |
| 120 | + def configure_development_view(self, component: ComponentConstructor) -> None: |
| 121 | + ... |
| 122 | + |
| 123 | + async def serve_development_app( |
| 124 | + self, |
| 125 | + app: Any, |
| 126 | + host: str, |
| 127 | + port: int, |
| 128 | + did_start: asyncio.Event, |
| 129 | + ) -> None: |
| 130 | + ... |
| 131 | + |
| 132 | + |
| 133 | +_INSTALLED_IMPLEMENTATIONS: dict[type[Any], _Implementation[Any]] = {} |
0 commit comments