reactpy.run and configure(...) refactoring (#1051)

- Change `reactpy.backends.utils.find_all_implementations()` to first try to import `<backend_name>` before importing `reactpy.backend.<backend_name>`
   - Allows for missing sub-dependencies to not cause `reactpy.run` to silently fail
- Import `uvicorn` directly within `serve_with_uvicorn` in order to defer import.
   - Allows for `ModuleNotFound: Could not import uvicorn` exception to tell the user what went wrong
- Added `CommonOptions.serve_index_route: bool`
   - Allows us to not clutter the route patterns when it's not needed
   - There are real circumstances where a user might want the index route to 404
- Fix bug where in-use ports are being assigned on Windows.
   - Removes `allow_reuse_waiting_ports` parameter on `find_available_port()` 
- Rename `BackendImplementation` to `BackendProtocol`
- Change load order of `SUPPORTED_PACKAGES` so that `FastAPI` has a chance to run before `starlette`
- Rename `SUPPORTED_PACKAGES` to `SUPPORTED_BACKENDS`
- Refactor `reactpy.backend.*` code to be more human readable
- Use f-strings where possible
- Merge `if` statements where possible
- Use `contextlib.supress` where possible
- Remove defunct `requirements.txt` file
This commit is contained in:
Mark Bakhit 2023-07-18 02:15:08 -07:00 committed by GitHub
parent 778057d7ab
commit fb9c57f073
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 198 additions and 190 deletions

View file

@ -40,11 +40,15 @@ v1.0.1
**Changed**
- :pull:`1050` - Warn and attempt to fix missing mime types, which can result in ``reactpy.run`` not working as expected.
- :pull:`1051` - Rename ``reactpy.backend.BackendImplementation`` to ``reactpy.backend.BackendType``
- :pull:`1051` - Allow ``reactpy.run`` to fail in more predictable ways
**Fixed**
- :issue:`930` - better traceback for JSON serialization errors (via :pull:`1008`)
- :issue:`437` - explain that JS component attributes must be JSON (via :pull:`1008`)
- :pull:`1051` - Fix ``reactpy.run`` port assignment sometimes attaching to in-use ports on Windows
- :pull:`1051` - Fix ``reactpy.run`` not recognizing ``fastapi``
v1.0.0

View file

@ -1,9 +0,0 @@
-r requirements/build-docs.txt
-r requirements/build-pkg.txt
-r requirements/check-style.txt
-r requirements/check-types.txt
-r requirements/make-release.txt
-r requirements/pkg-deps.txt
-r requirements/pkg-extras.txt
-r requirements/test-env.txt
-r requirements/nox-deps.txt

View file

@ -14,53 +14,49 @@ from reactpy.core.types import VdomDict
from reactpy.utils import vdom_to_html
if TYPE_CHECKING:
import uvicorn
from asgiref.typing import ASGIApplication
PATH_PREFIX = PurePosixPath("/_reactpy")
MODULES_PATH = PATH_PREFIX / "modules"
ASSETS_PATH = PATH_PREFIX / "assets"
STREAM_PATH = PATH_PREFIX / "stream"
CLIENT_BUILD_DIR = Path(_reactpy_file_path).parent / "_static" / "app" / "dist"
try:
async def serve_with_uvicorn(
app: ASGIApplication | Any,
host: str,
port: int,
started: asyncio.Event | None,
) -> None:
"""Run a development server for an ASGI application"""
import uvicorn
except ImportError: # nocov
pass
else:
async def serve_development_asgi(
app: ASGIApplication | Any,
host: str,
port: int,
started: asyncio.Event | None,
) -> None:
"""Run a development server for an ASGI application"""
server = uvicorn.Server(
uvicorn.Config(
app,
host=host,
port=port,
loop="asyncio",
reload=True,
)
server = uvicorn.Server(
uvicorn.Config(
app,
host=host,
port=port,
loop="asyncio",
)
server.config.setup_event_loop()
coros: list[Awaitable[Any]] = [server.serve()]
)
server.config.setup_event_loop()
coros: list[Awaitable[Any]] = [server.serve()]
# If a started event is provided, then use it signal based on `server.started`
if started:
coros.append(_check_if_started(server, started))
# If a started event is provided, then use it signal based on `server.started`
if started:
coros.append(_check_if_started(server, started))
try:
await asyncio.gather(*coros)
finally:
# Since we aren't using the uvicorn's `run()` API, we can't guarantee uvicorn's
# order of operations. So we need to make sure `shutdown()` always has an initialized
# list of `self.servers` to use.
if not hasattr(server, "servers"): # nocov
server.servers = []
await asyncio.wait_for(server.shutdown(), timeout=3)
try:
await asyncio.gather(*coros)
finally:
# Since we aren't using the uvicorn's `run()` API, we can't guarantee uvicorn's
# order of operations. So we need to make sure `shutdown()` always has an initialized
# list of `self.servers` to use.
if not hasattr(server, "servers"): # nocov
server.servers = []
await asyncio.wait_for(server.shutdown(), timeout=3)
async def _check_if_started(server: uvicorn.Server, started: asyncio.Event) -> None:
@ -72,8 +68,7 @@ async def _check_if_started(server: uvicorn.Server, started: asyncio.Event) -> N
def safe_client_build_dir_path(path: str) -> Path:
"""Prevent path traversal out of :data:`CLIENT_BUILD_DIR`"""
return traversal_safe_path(
CLIENT_BUILD_DIR,
*("index.html" if path in ("", "/") else path).split("/"),
CLIENT_BUILD_DIR, *("index.html" if path in {"", "/"} else path).split("/")
)
@ -140,6 +135,9 @@ class CommonOptions:
url_prefix: str = ""
"""The URL prefix where ReactPy resources will be served from"""
serve_index_route: bool = True
"""Automatically generate and serve the index route (``/``)"""
def __post_init__(self) -> None:
if self.url_prefix and not self.url_prefix.startswith("/"):
msg = "Expected 'url_prefix' to start with '/'"

View file

@ -5,13 +5,26 @@ from logging import getLogger
from sys import exc_info
from typing import Any, NoReturn
from reactpy.backend.types import BackendImplementation
from reactpy.backend.utils import SUPPORTED_PACKAGES, all_implementations
from reactpy.backend.types import BackendType
from reactpy.backend.utils import SUPPORTED_BACKENDS, all_implementations
from reactpy.types import RootComponentConstructor
logger = getLogger(__name__)
_DEFAULT_IMPLEMENTATION: BackendType[Any] | None = None
# BackendType.Options
class Options: # nocov
"""Configuration options that can be provided to the backend.
This definition should not be used/instantiated. It exists only for
type hinting purposes."""
def __init__(self, *args: Any, **kwds: Any) -> NoReturn:
msg = "Default implementation has no options."
raise ValueError(msg)
# BackendType.configure
def configure(
app: Any, component: RootComponentConstructor, options: None = None
) -> None:
@ -22,17 +35,13 @@ def configure(
return _default_implementation().configure(app, component)
# BackendType.create_development_app
def create_development_app() -> Any:
"""Create an application instance for development purposes"""
return _default_implementation().create_development_app()
def Options(*args: Any, **kwargs: Any) -> NoReturn: # nocov
"""Create configuration options"""
msg = "Default implementation has no options."
raise ValueError(msg)
# BackendType.serve_development_app
async def serve_development_app(
app: Any,
host: str,
@ -45,10 +54,7 @@ async def serve_development_app(
)
_DEFAULT_IMPLEMENTATION: BackendImplementation[Any] | None = None
def _default_implementation() -> BackendImplementation[Any]:
def _default_implementation() -> BackendType[Any]:
"""Get the first available server implementation"""
global _DEFAULT_IMPLEMENTATION # noqa: PLW0603
@ -59,7 +65,7 @@ def _default_implementation() -> BackendImplementation[Any]:
implementation = next(all_implementations())
except StopIteration: # nocov
logger.debug("Backend implementation import failed", exc_info=exc_info())
supported_backends = ", ".join(SUPPORTED_PACKAGES)
supported_backends = ", ".join(SUPPORTED_BACKENDS)
msg = (
"It seems you haven't installed a backend. To resolve this issue, "
"you can install a backend by running:\n\n"

View file

@ -4,22 +4,22 @@ from fastapi import FastAPI
from reactpy.backend import starlette
serve_development_app = starlette.serve_development_app
"""Alias for :func:`reactpy.backend.starlette.serve_development_app`"""
use_connection = starlette.use_connection
"""Alias for :func:`reactpy.backend.starlette.use_location`"""
use_websocket = starlette.use_websocket
"""Alias for :func:`reactpy.backend.starlette.use_websocket`"""
# BackendType.Options
Options = starlette.Options
"""Alias for :class:`reactpy.backend.starlette.Options`"""
# BackendType.configure
configure = starlette.configure
"""Alias for :class:`reactpy.backend.starlette.configure`"""
# BackendType.create_development_app
def create_development_app() -> FastAPI:
"""Create a development ``FastAPI`` application instance."""
return FastAPI(debug=True)
# BackendType.serve_development_app
serve_development_app = starlette.serve_development_app
use_connection = starlette.use_connection
use_websocket = starlette.use_websocket

View file

@ -45,6 +45,19 @@ from reactpy.utils import Ref
logger = logging.getLogger(__name__)
# BackendType.Options
@dataclass
class Options(CommonOptions):
"""Render server config for :func:`reactpy.backend.flask.configure`"""
cors: bool | dict[str, Any] = False
"""Enable or configure Cross Origin Resource Sharing (CORS)
For more information see docs for ``flask_cors.CORS``
"""
# BackendType.configure
def configure(
app: Flask, component: RootComponentConstructor, options: Options | None = None
) -> None:
@ -69,20 +82,21 @@ def configure(
app.register_blueprint(spa_bp)
# BackendType.create_development_app
def create_development_app() -> Flask:
"""Create an application instance for development purposes"""
os.environ["FLASK_DEBUG"] = "true"
app = Flask(__name__)
return app
return Flask(__name__)
# BackendType.serve_development_app
async def serve_development_app(
app: Flask,
host: str,
port: int,
started: asyncio.Event | None = None,
) -> None:
"""Run an application using a development server"""
"""Run a development server for FastAPI"""
loop = asyncio.get_running_loop()
stopped = asyncio.Event()
@ -135,17 +149,6 @@ def use_connection() -> Connection[_FlaskCarrier]:
return conn
@dataclass
class Options(CommonOptions):
"""Render server config for :func:`reactpy.backend.flask.configure`"""
cors: bool | dict[str, Any] = False
"""Enable or configure Cross Origin Resource Sharing (CORS)
For more information see docs for ``flask_cors.CORS``
"""
def _setup_common_routes(
api_blueprint: Blueprint,
spa_blueprint: Blueprint,
@ -166,10 +169,12 @@ def _setup_common_routes(
index_html = read_client_index_html(options)
@spa_blueprint.route("/")
@spa_blueprint.route("/<path:_>")
def send_client_dir(_: str = "") -> Any:
return index_html
if options.serve_index_route:
@spa_blueprint.route("/")
@spa_blueprint.route("/<path:_>")
def send_client_dir(_: str = "") -> Any:
return index_html
def _setup_single_view_dispatcher_route(

View file

@ -22,7 +22,7 @@ from reactpy.backend._common import (
read_client_index_html,
safe_client_build_dir_path,
safe_web_modules_dir_path,
serve_development_asgi,
serve_with_uvicorn,
)
from reactpy.backend.hooks import ConnectionContext
from reactpy.backend.hooks import use_connection as _use_connection
@ -34,6 +34,19 @@ from reactpy.core.types import RootComponentConstructor
logger = logging.getLogger(__name__)
# BackendType.Options
@dataclass
class Options(CommonOptions):
"""Render server config for :func:`reactpy.backend.sanic.configure`"""
cors: bool | dict[str, Any] = False
"""Enable or configure Cross Origin Resource Sharing (CORS)
For more information see docs for ``sanic_cors.CORS``
"""
# BackendType.configure
def configure(
app: Sanic, component: RootComponentConstructor, options: Options | None = None
) -> None:
@ -49,14 +62,15 @@ def configure(
app.blueprint([spa_bp, api_bp])
# BackendType.create_development_app
def create_development_app() -> Sanic:
"""Return a :class:`Sanic` app instance in test mode"""
Sanic.test_mode = True
logger.warning("Sanic.test_mode is now active")
app = Sanic(f"reactpy_development_app_{uuid4().hex}", Config())
return app
return Sanic(f"reactpy_development_app_{uuid4().hex}", Config())
# BackendType.serve_development_app
async def serve_development_app(
app: Sanic,
host: str,
@ -64,7 +78,7 @@ async def serve_development_app(
started: asyncio.Event | None = None,
) -> None:
"""Run a development server for :mod:`sanic`"""
await serve_development_asgi(app, host, port, started)
await serve_with_uvicorn(app, host, port, started)
def use_request() -> request.Request:
@ -86,17 +100,6 @@ def use_connection() -> Connection[_SanicCarrier]:
return conn
@dataclass
class Options(CommonOptions):
"""Render server config for :func:`reactpy.backend.sanic.configure`"""
cors: bool | dict[str, Any] = False
"""Enable or configure Cross Origin Resource Sharing (CORS)
For more information see docs for ``sanic_cors.CORS``
"""
def _setup_common_routes(
api_blueprint: Blueprint,
spa_blueprint: Blueprint,
@ -115,16 +118,17 @@ def _setup_common_routes(
) -> response.HTTPResponse:
return response.html(index_html)
spa_blueprint.add_route(
single_page_app_files,
"/",
name="single_page_app_files_root",
)
spa_blueprint.add_route(
single_page_app_files,
"/<_:path>",
name="single_page_app_files_path",
)
if options.serve_index_route:
spa_blueprint.add_route(
single_page_app_files,
"/",
name="single_page_app_files_root",
)
spa_blueprint.add_route(
single_page_app_files,
"/<_:path>",
name="single_page_app_files_path",
)
async def asset_files(
request: request.Request,

View file

@ -21,7 +21,7 @@ from reactpy.backend._common import (
STREAM_PATH,
CommonOptions,
read_client_index_html,
serve_development_asgi,
serve_with_uvicorn,
)
from reactpy.backend.hooks import ConnectionContext
from reactpy.backend.hooks import use_connection as _use_connection
@ -34,6 +34,19 @@ from reactpy.core.types import RootComponentConstructor
logger = logging.getLogger(__name__)
# BackendType.Options
@dataclass
class Options(CommonOptions):
"""Render server config for :func:`reactpy.backend.starlette.configure`"""
cors: bool | dict[str, Any] = False
"""Enable or configure Cross Origin Resource Sharing (CORS)
For more information see docs for ``starlette.middleware.cors.CORSMiddleware``
"""
# BackendType.configure
def configure(
app: Starlette,
component: RootComponentConstructor,
@ -54,11 +67,13 @@ def configure(
_setup_common_routes(options, app)
# BackendType.create_development_app
def create_development_app() -> Starlette:
"""Return a :class:`Starlette` app instance in debug mode"""
return Starlette(debug=True)
# BackendType.serve_development_app
async def serve_development_app(
app: Starlette,
host: str,
@ -66,7 +81,7 @@ async def serve_development_app(
started: asyncio.Event | None = None,
) -> None:
"""Run a development server for starlette"""
await serve_development_asgi(app, host, port, started)
await serve_with_uvicorn(app, host, port, started)
def use_websocket() -> WebSocket:
@ -82,17 +97,6 @@ def use_connection() -> Connection[WebSocket]:
return conn
@dataclass
class Options(CommonOptions):
"""Render server config for :func:`reactpy.backend.starlette.configure`"""
cors: bool | dict[str, Any] = False
"""Enable or configure Cross Origin Resource Sharing (CORS)
For more information see docs for ``starlette.middleware.cors.CORSMiddleware``
"""
def _setup_common_routes(options: Options, app: Starlette) -> None:
cors_options = options.cors
if cors_options: # nocov
@ -115,8 +119,10 @@ def _setup_common_routes(options: Options, app: Starlette) -> None:
)
# register this last so it takes least priority
index_route = _make_index_route(options)
app.add_route(url_prefix + "/", index_route)
app.add_route(url_prefix + "/{path:path}", index_route)
if options.serve_index_route:
app.add_route(f"{url_prefix}/", index_route)
app.add_route(url_prefix + "/{path:path}", index_route)
def _make_index_route(options: Options) -> Callable[[Request], Awaitable[HTMLResponse]]:

View file

@ -32,10 +32,11 @@ from reactpy.core.layout import Layout
from reactpy.core.serve import serve_layout
from reactpy.core.types import ComponentConstructor
# BackendType.Options
Options = CommonOptions
"""Render server config for :func:`reactpy.backend.tornado.configure`"""
# BackendType.configure
def configure(
app: Application,
component: ComponentConstructor,
@ -60,10 +61,12 @@ def configure(
)
# BackendType.create_development_app
def create_development_app() -> Application:
return Application(debug=True)
# BackendType.serve_development_app
async def serve_development_app(
app: Application,
host: str,
@ -119,12 +122,17 @@ def _setup_common_routes(options: Options) -> _RouteHandlerSpecs:
StaticFileHandler,
{"path": str(CLIENT_BUILD_DIR / "assets")},
),
(
r"/(.*)",
IndexHandler,
{"index_html": read_client_index_html(options)},
),
]
] + (
[
(
r"/(.*)",
IndexHandler,
{"index_html": read_client_index_html(options)},
),
]
if options.serve_index_route
else []
)
def _add_handler(

View file

@ -11,11 +11,11 @@ _App = TypeVar("_App")
@runtime_checkable
class BackendImplementation(Protocol[_App]):
class BackendType(Protocol[_App]):
"""Common interface for built-in web server/framework integrations"""
Options: Callable[..., Any]
"""A constructor for options passed to :meth:`BackendImplementation.configure`"""
"""A constructor for options passed to :meth:`BackendType.configure`"""
def configure(
self,

View file

@ -3,22 +3,23 @@ from __future__ import annotations
import asyncio
import logging
import socket
import sys
from collections.abc import Iterator
from contextlib import closing
from importlib import import_module
from typing import Any
from reactpy.backend.types import BackendImplementation
from reactpy.backend.types import BackendType
from reactpy.types import RootComponentConstructor
logger = logging.getLogger(__name__)
SUPPORTED_PACKAGES = (
"starlette",
SUPPORTED_BACKENDS = (
"fastapi",
"sanic",
"tornado",
"flask",
"starlette",
)
@ -26,43 +27,37 @@ def run(
component: RootComponentConstructor,
host: str = "127.0.0.1",
port: int | None = None,
implementation: BackendImplementation[Any] | None = None,
implementation: BackendType[Any] | None = None,
) -> None:
"""Run a component with a development server"""
logger.warning(_DEVELOPMENT_RUN_FUNC_WARNING)
implementation = implementation or import_module("reactpy.backend.default")
app = implementation.create_development_app()
implementation.configure(app, component)
host = host
port = port or find_available_port(host)
app_cls = type(app)
logger.info(
f"Running with {app_cls.__module__}.{app_cls.__name__} at http://{host}:{port}"
)
logger.info(
"ReactPy is running with '%s.%s' at http://%s:%s",
app_cls.__module__,
app_cls.__name__,
host,
port,
)
asyncio.run(implementation.serve_development_app(app, host, port))
def find_available_port(
host: str,
port_min: int = 8000,
port_max: int = 9000,
allow_reuse_waiting_ports: bool = True,
) -> int:
def find_available_port(host: str, port_min: int = 8000, port_max: int = 9000) -> int:
"""Get a port that's available for the given host and port range"""
for port in range(port_min, port_max):
with closing(socket.socket()) as sock:
try:
if allow_reuse_waiting_ports:
# As per this answer: https://stackoverflow.com/a/19247688/3159288
# setting can be somewhat unreliable because we allow the use of
# ports that are stuck in TIME_WAIT. However, not setting the option
# means we're overly cautious and almost always use a different addr
# even if it could have actually been used.
if sys.platform == "linux":
# Fixes bug where every time you restart the server you'll
# get a different port on Linux. This cannot be set on Windows
# otherwise address will always be reused.
# Ref: https://stackoverflow.com/a/19247688/3159288
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
except OSError:
@ -73,26 +68,20 @@ def find_available_port(
raise RuntimeError(msg)
def all_implementations() -> Iterator[BackendImplementation[Any]]:
def all_implementations() -> Iterator[BackendType[Any]]:
"""Yield all available server implementations"""
for name in SUPPORTED_PACKAGES:
for name in SUPPORTED_BACKENDS:
try:
relative_import_name = f"{__name__.rsplit('.', 1)[0]}.{name}"
module = import_module(relative_import_name)
import_module(name)
except ImportError: # nocov
logger.debug(f"Failed to import {name!r}", exc_info=True)
logger.debug("Failed to import %s", name, exc_info=True)
continue
if not isinstance(module, BackendImplementation): # nocov
msg = f"{module.__name__!r} is an invalid implementation"
raise TypeError(msg)
yield module
reactpy_backend_name = f"{__name__.rsplit('.', 1)[0]}.{name}"
yield import_module(reactpy_backend_name)
_DEVELOPMENT_RUN_FUNC_WARNING = f"""\
The `run()` function is only intended for testing during development! To run in \
production, consider selecting a supported backend and importing its associated \
`configure()` function from `reactpy.backend.<package>` where `<package>` is one of \
{list(SUPPORTED_PACKAGES)}. For details refer to the docs on how to run each package.\
_DEVELOPMENT_RUN_FUNC_WARNING = """\
The `run()` function is only intended for testing during development! To run \
in production, refer to the docs on how to use reactpy.backend.*.configure.\
"""

View file

@ -2,13 +2,13 @@ from __future__ import annotations
import asyncio
import logging
from contextlib import AsyncExitStack
from contextlib import AsyncExitStack, suppress
from types import TracebackType
from typing import Any, Callable
from urllib.parse import urlencode, urlunparse
from reactpy.backend import default as default_server
from reactpy.backend.types import BackendImplementation
from reactpy.backend.types import BackendType
from reactpy.backend.utils import find_available_port
from reactpy.config import REACTPY_TESTING_DEFAULT_TIMEOUT
from reactpy.core.component import component
@ -43,21 +43,20 @@ class BackendFixture:
host: str = "127.0.0.1",
port: int | None = None,
app: Any | None = None,
implementation: BackendImplementation[Any] | None = None,
implementation: BackendType[Any] | None = None,
options: Any | None = None,
timeout: float | None = None,
) -> None:
self.host = host
self.port = port or find_available_port(host, allow_reuse_waiting_ports=False)
self.port = port or find_available_port(host)
self.mount, self._root_component = _hotswap()
self.timeout = (
REACTPY_TESTING_DEFAULT_TIMEOUT.current if timeout is None else timeout
)
if app is not None:
if implementation is None:
msg = "If an application instance its corresponding server implementation must be provided too."
raise ValueError(msg)
if app is not None and implementation is None:
msg = "If an application instance its corresponding server implementation must be provided too."
raise ValueError(msg)
self._app = app
self.implementation = implementation or default_server
@ -124,10 +123,8 @@ class BackendFixture:
async def stop_server() -> None:
server_future.cancel()
try:
with suppress(asyncio.CancelledError):
await asyncio.wait_for(server_future, timeout=self.timeout)
except asyncio.CancelledError:
pass
self._exit_stack.push_async_callback(stop_server)

View file

@ -4,7 +4,7 @@
- :mod:`reactpy.backend.types`
"""
from reactpy.backend.types import BackendImplementation, Connection, Location
from reactpy.backend.types import BackendType, Connection, Location
from reactpy.core.component import Component
from reactpy.core.hooks import Context
from reactpy.core.types import (
@ -27,7 +27,7 @@ from reactpy.core.types import (
)
__all__ = [
"BackendImplementation",
"BackendType",
"Component",
"ComponentConstructor",
"ComponentType",

View file

@ -6,7 +6,7 @@ import reactpy
from reactpy import html
from reactpy.backend import default as default_implementation
from reactpy.backend._common import PATH_PREFIX
from reactpy.backend.types import BackendImplementation, Connection, Location
from reactpy.backend.types import BackendType, Connection, Location
from reactpy.backend.utils import all_implementations
from reactpy.testing import BackendFixture, DisplayFixture, poll
@ -17,7 +17,7 @@ from reactpy.testing import BackendFixture, DisplayFixture, poll
scope="module",
)
async def display(page, request):
imp: BackendImplementation = request.param
imp: BackendType = request.param
# we do this to check that route priorities for each backend are correct
if imp is default_implementation:
@ -158,7 +158,7 @@ async def test_use_request(display: DisplayFixture, hook_name):
@pytest.mark.parametrize("imp", all_implementations())
async def test_customized_head(imp: BackendImplementation, page):
async def test_customized_head(imp: BackendType, page):
custom_title = f"Custom Title for {imp.__name__}"
@reactpy.component