Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/build_scripts/install_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions src/reactpy/executors/asgi/pyscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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 ``<body>`` 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
Expand All @@ -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
Expand All @@ -97,6 +106,11 @@ 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)
body_content = (
""
if self.parent.prepend_body is None
else reactpy_to_string(self.parent.prepend_body)
)
pyscript_setup = pyscript_setup_html(
extra_py=self.parent.extra_py,
extra_js=self.parent.extra_js,
Expand All @@ -114,6 +128,7 @@ def render_index_html(self) -> None:
f'<html lang="{self.parent.html_lang}">'
f"{head_content}"
"<body>"
f"{body_content}"
f"{pyscript_component}"
"</body>"
"</html>"
Expand Down
21 changes: 19 additions & 2 deletions src/reactpy/executors/asgi/standalone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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,
Expand All @@ -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 ``<body>`` 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.
Expand All @@ -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:
Expand Down Expand Up @@ -229,11 +240,17 @@ async def __call__(

def render_index_html(self) -> None:
"""Process the index.html and store the results in this class."""
body_content = (
""
if self.parent.prepend_body is None
else reactpy_to_string(self.parent.prepend_body)
)
self._index_html = (
"<!doctype html>"
f'<html lang="{self.parent.html_lang}">'
f"{vdom_head_to_html(self.parent.html_head)}"
"<body>"
f"{body_content}"
f"{server_side_component_html(element_id='app', class_='', component_path='')}"
"</body>"
"</html>"
Expand Down
65 changes: 65 additions & 0 deletions tests/test_asgi/test_pyscript.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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 (
'<noscript><p id="noscript-message">Please enable JavaScript.</p></noscript>'
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 (
'<noscript><p id="noscript-message">Please enable JavaScript.</p></noscript>'
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 (
"<noscript>Enable JavaScript to view this site.</noscript>" 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 "<noscript>" not in response.text


async def test_jinja_template_tag(jinja_display: DisplayFixture):
await jinja_display.goto("/")

Expand Down
58 changes: 58 additions & 0 deletions tests/test_asgi/test_standalone.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,64 @@ def sample():
assert (await new_display.page.title()) == custom_title


async def test_prepend_body_vdom():
@reactpy.component
def sample():
return html.h1("Hello World")

app = ReactPy(
sample,
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 (
'<noscript><p id="noscript-message">Please enable JavaScript.</p></noscript>'
in response.text
)


async def test_prepend_body_default_is_noscript():
@reactpy.component
def sample():
return html.h1("Hello World")

app = ReactPy(sample)

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 (
"<noscript>Enable JavaScript to view this site.</noscript>" in response.text
)


async def test_prepend_body_disabled():
@reactpy.component
def sample():
return html.h1("Hello World")

app = ReactPy(sample, 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 "<noscript>" not in response.text


async def test_head_request():
@reactpy.component
def sample():
Expand Down
22 changes: 21 additions & 1 deletion tests/test_asgi/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from reactpy import config
from reactpy import config, html
from reactpy.executors import utils


Expand All @@ -9,6 +9,26 @@ def test_invalid_vdom_head():
utils.vdom_head_to_html({"tagName": "invalid"})


def test_prepend_body_content_as_vdom_dict():
from reactpy.utils import reactpy_to_string

assert (
reactpy_to_string(
html.div(html.p({"id": "noscript-message"}, "Please enable JavaScript."))
)
== '<div><p id="noscript-message">Please enable JavaScript.</p></div>'
)


def test_prepend_body_content_as_noscript():
from reactpy.utils import reactpy_to_string

assert (
reactpy_to_string(html.noscript(html.p("Enable JavaScript to view this site.")))
== "<noscript><p>Enable JavaScript to view this site.</p></noscript>"
)


def test_process_settings():
utils.process_settings({"async_rendering": False})
assert config.REACTPY_ASYNC_RENDERING.current is False
Expand Down
Loading