diff --git a/src/build_scripts/install_deps.py b/src/build_scripts/install_deps.py index 8045d0325..15ee43c56 100644 --- a/src/build_scripts/install_deps.py +++ b/src/build_scripts/install_deps.py @@ -30,7 +30,7 @@ def find_deps(data): def install_deps(): - pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml" with open(pyproject_path, "rb") as f: pyproject_data = toml.load(f) find_deps(pyproject_data) diff --git a/src/reactpy/executors/asgi/pyscript.py b/src/reactpy/executors/asgi/pyscript.py index 20a094cb7..9c7bfcf5c 100644 --- a/src/reactpy/executors/asgi/pyscript.py +++ b/src/reactpy/executors/asgi/pyscript.py @@ -19,6 +19,7 @@ ) from reactpy.executors.utils import vdom_head_to_html from reactpy.types import ReactPyConfig, VdomDict +from reactpy.utils import reactpy_to_string class ReactPyCsr(ReactPy): @@ -32,6 +33,7 @@ def __init__( initial: str | VdomDict = "", http_headers: dict[str, str] | None = None, html_head: VdomDict | None = None, + prepend_body: VdomDict | None = ..., # type: ignore[assignment] html_lang: str = "en", **settings: Unpack[ReactPyConfig], ) -> None: @@ -59,6 +61,9 @@ def __init__( commonly used to render a loading animation. http_headers: Additional headers to include in the HTTP response for the base HTML document. html_head: Additional head elements to include in the HTML response. + prepend_body: Content rendered at the start of the ```` element. + A ``VdomDict`` constructed via ``html.*`` functions, or ``None`` to omit. + Defaults to ``html.noscript("Enable JavaScript to view this site.")``. html_lang: The language of the HTML document. settings: Global ReactPy configuration settings that affect behavior and performance. Most settings @@ -78,6 +83,10 @@ def __init__( self.extra_headers = http_headers or {} self.dispatcher_pattern = re.compile(f"^{self.dispatcher_path}?") self.html_head = html_head or html.head() + if prepend_body is not ...: + self.prepend_body = prepend_body + else: + self.prepend_body = html.noscript("Enable JavaScript to view this site.") self.html_lang = html_lang def match_dispatch_path(self, scope: AsgiWebsocketScope) -> bool: # nocov @@ -97,6 +106,10 @@ class ReactPyPyscriptApp(ReactPyApp): def render_index_html(self) -> None: """Process the index.html and store the results in this class.""" head_content = vdom_head_to_html(self.parent.html_head) + if not self.parent.prepend_body or self.parent.prepend_body == ...: + prepend_body = "" + else: + prepend_body = reactpy_to_string(self.parent.prepend_body) pyscript_setup = pyscript_setup_html( extra_py=self.parent.extra_py, extra_js=self.parent.extra_js, @@ -114,6 +127,7 @@ def render_index_html(self) -> None: f'' f"{head_content}" "" + f"{prepend_body}" f"{pyscript_component}" "" "" diff --git a/src/reactpy/executors/asgi/standalone.py b/src/reactpy/executors/asgi/standalone.py index b16246995..e962707dc 100644 --- a/src/reactpy/executors/asgi/standalone.py +++ b/src/reactpy/executors/asgi/standalone.py @@ -24,14 +24,17 @@ AsgiWebsocketScope, ) from reactpy.executors.pyscript.utils import pyscript_setup_html -from reactpy.executors.utils import server_side_component_html, vdom_head_to_html +from reactpy.executors.utils import ( + server_side_component_html, + vdom_head_to_html, +) from reactpy.types import ( PyScriptOptions, ReactPyConfig, RootComponentConstructor, VdomDict, ) -from reactpy.utils import import_dotted_path, string_to_reactpy +from reactpy.utils import import_dotted_path, reactpy_to_string, string_to_reactpy _logger = getLogger(__name__) @@ -45,6 +48,7 @@ def __init__( *, http_headers: dict[str, str] | None = None, html_head: VdomDict | None = None, + prepend_body: VdomDict | None = ..., # type: ignore[assignment] html_lang: str = "en", pyscript_setup: bool = False, pyscript_options: PyScriptOptions | None = None, @@ -56,6 +60,9 @@ def __init__( root_component: The root component to render. This app is typically a single page application. http_headers: Additional headers to include in the HTTP response for the base HTML document. html_head: Additional head elements to include in the HTML response. + prepend_body: Content rendered at the start of the ```` element. + A ``VdomDict`` constructed via ``html.*`` functions, or ``None`` to omit. + Defaults to ``html.noscript("Enable JavaScript to view this site.")``. html_lang: The language of the HTML document. pyscript_setup: Whether to automatically load PyScript within your HTML head. pyscript_options: Options to configure PyScript behavior. @@ -66,6 +73,10 @@ def __init__( self.extra_headers = http_headers or {} self.dispatcher_pattern = re.compile(f"^{self.dispatcher_path}?") self.html_head = html_head or html.head() + if prepend_body is not ...: + self.prepend_body = prepend_body + else: + self.prepend_body = html.noscript("Enable JavaScript to view this site.") self.html_lang = html_lang if pyscript_setup: @@ -229,11 +240,16 @@ async def __call__( def render_index_html(self) -> None: """Process the index.html and store the results in this class.""" + if not self.parent.prepend_body or self.parent.prepend_body == ...: + prepend_body = "" + else: + prepend_body = reactpy_to_string(self.parent.prepend_body) self._index_html = ( "" f'' f"{vdom_head_to_html(self.parent.html_head)}" "" + f"{prepend_body}" f"{server_side_component_html(element_id='app', class_='', component_path='')}" "" "" diff --git a/tests/test_asgi/test_pyscript.py b/tests/test_asgi/test_pyscript.py index 24608d301..940bfa8e2 100644 --- a/tests/test_asgi/test_pyscript.py +++ b/tests/test_asgi/test_pyscript.py @@ -1,17 +1,22 @@ # ruff: noqa: S701 +import asyncio from pathlib import Path import pytest from jinja2 import Environment as JinjaEnvironment from jinja2 import FileSystemLoader as JinjaFileSystemLoader +from requests import request from starlette.applications import Starlette from starlette.routing import Route from starlette.templating import Jinja2Templates +from reactpy import config as _config from reactpy import html from reactpy.executors.asgi.pyscript import ReactPyCsr from reactpy.testing import BackendFixture, DisplayFixture +REACTPY_TESTS_DEFAULT_TIMEOUT = _config.REACTPY_TESTS_DEFAULT_TIMEOUT + @pytest.fixture(scope="module") async def display(browser): @@ -102,6 +107,66 @@ def test_bad_file_path(): ReactPyCsr() +async def test_customized_noscript_vdom(): + app = ReactPyCsr( + Path(__file__).parent / "pyscript_components" / "root.py", + prepend_body=html.noscript( + html.p({"id": "noscript-message"}, "Please enable JavaScript.") + ), + ) + + async with BackendFixture(app) as server: + url = f"http://{server.host}:{server.port}" + response = await asyncio.to_thread( + request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current + ) + assert response.status_code == 200 + assert ( + '' + in response.text + ) + + async with BackendFixture(app) as server: + url = f"http://{server.host}:{server.port}" + response = await asyncio.to_thread( + request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current + ) + assert response.status_code == 200 + assert ( + '' + in response.text + ) + + +async def test_prepend_body_default_is_noscript(): + app = ReactPyCsr(Path(__file__).parent / "pyscript_components" / "root.py") + + async with BackendFixture(app) as server: + url = f"http://{server.host}:{server.port}" + response = await asyncio.to_thread( + request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current + ) + assert response.status_code == 200 + assert ( + "" in response.text + ) + + +async def test_prepend_body_disabled(): + app = ReactPyCsr( + Path(__file__).parent / "pyscript_components" / "root.py", + prepend_body=None, + ) + + async with BackendFixture(app) as server: + url = f"http://{server.host}:{server.port}" + response = await asyncio.to_thread( + request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current + ) + assert response.status_code == 200 + assert "