Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved default transforms for vdom_to_html #1278

Merged
Merged
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
4 changes: 4 additions & 0 deletions docs/source/about/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Unreleased
- :pull:`1113` - Renamed ``reactpy.config.REACTPY_DEBUG_MODE`` to ``reactpy.config.REACTPY_DEBUG``.
- :pull:`1113` - ``@reactpy/client`` now exports ``React`` and ``ReactDOM``.
- :pull:`1263` - ReactPy no longer auto-converts ``snake_case`` props to ``camelCase``. It is now the responsibility of the user to ensure that props are in the correct format.
- :pull:`1278` - ``reactpy.utils.reactpy_to_string`` will now retain the user's original casing for ``data-*`` and ``aria-*`` attributes.
- :pull:`1278` - ``reactpy.utils.string_to_reactpy`` has been upgraded to handle more complex scenarios without causing ReactJS rendering errors.

**Removed**

Expand All @@ -48,6 +50,8 @@ Unreleased
- :pull:`1113` - Removed ``reactpy.run``. See the documentation for the new method to run ReactPy applications.
- :pull:`1113` - Removed ``reactpy.backend.*``. See the documentation for the new method to run ReactPy applications.
- :pull:`1113` - Removed ``reactpy.core.types`` module. Use ``reactpy.types`` instead.
- :pull:`1278` - Removed ``reactpy.utils.html_to_vdom``. Use ``reactpy.utils.string_to_reactpy`` instead.
- :pull:`1278` - Removed ``reactpy.utils.vdom_to_html``. Use ``reactpy.utils.reactpy_to_string`` instead.
- :pull:`1113` - All backend related installation extras (such as ``pip install reactpy[starlette]``) have been removed.
- :pull:`1113` - Removed deprecated function ``module_from_template``.
- :pull:`1113` - Removed support for Python 3.9.
Expand Down
6 changes: 3 additions & 3 deletions src/reactpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from reactpy.core.layout import Layout
from reactpy.core.vdom import vdom
from reactpy.pyscript.components import pyscript_component
from reactpy.utils import Ref, html_to_vdom, vdom_to_html
from reactpy.utils import Ref, reactpy_to_string, string_to_reactpy

__author__ = "The Reactive Python Team"
__version__ = "2.0.0a1"
Expand All @@ -35,9 +35,10 @@
"event",
"hooks",
"html",
"html_to_vdom",
"logging",
"pyscript_component",
"reactpy_to_string",
"string_to_reactpy",
"types",
"use_async_effect",
"use_callback",
Expand All @@ -52,7 +53,6 @@
"use_scope",
"use_state",
"vdom",
"vdom_to_html",
"web",
"widgets",
]
2 changes: 1 addition & 1 deletion src/reactpy/core/_life_cycle_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async def __call__(self, stop: Event) -> None: ...
logger = logging.getLogger(__name__)


class _HookStack(Singleton): # pragma: no cover
class _HookStack(Singleton): # nocov
"""A singleton object which manages the current component tree's hooks.
Life cycle hooks can be stored in a thread local or context variable depending
on the platform."""
Expand Down
2 changes: 1 addition & 1 deletion src/reactpy/core/_thread_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
_StateType = TypeVar("_StateType")


class ThreadLocal(Generic[_StateType]): # pragma: no cover
class ThreadLocal(Generic[_StateType]): # nocov
"""Utility for managing per-thread state information. This is only used in
environments where ContextVars are not available, such as the `pyodide`
executor."""
Expand Down
2 changes: 1 addition & 1 deletion src/reactpy/core/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ def strictly_equal(x: Any, y: Any) -> bool:
return x == y # type: ignore

# Fallback to identity check
return x is y # pragma: no cover
return x is y # nocov


def run_effect_cleanup(cleanup_func: Ref[_EffectCleanFunc | None]) -> None:
Expand Down
6 changes: 3 additions & 3 deletions src/reactpy/executors/asgi/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ async def __call__(
msg: dict[str, str] = orjson.loads(event["text"])
if msg.get("type") == "layout-event":
await ws.rendering_queue.put(msg)
else: # pragma: no cover
else: # nocov
await asyncio.to_thread(
_logger.warning, f"Unknown message type: {msg.get('type')}"
)
Expand Down Expand Up @@ -205,7 +205,7 @@ async def run_dispatcher(self) -> None:
# Determine component to serve by analyzing the URL and/or class parameters.
if self.parent.multiple_root_components:
url_match = re.match(self.parent.dispatcher_pattern, self.scope["path"])
if not url_match: # pragma: no cover
if not url_match: # nocov
raise RuntimeError("Could not find component in URL path.")
dotted_path = url_match["dotted_path"]
if dotted_path not in self.parent.root_components:
Expand All @@ -215,7 +215,7 @@ async def run_dispatcher(self) -> None:
component = self.parent.root_components[dotted_path]
elif self.parent.root_component:
component = self.parent.root_component
else: # pragma: no cover
else: # nocov
raise RuntimeError("No root component provided.")

# Create a connection object by analyzing the websocket's query string.
Expand Down
4 changes: 1 addition & 3 deletions src/reactpy/executors/asgi/pyscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ def __init__(
self.html_head = html_head or html.head()
self.html_lang = html_lang

def match_dispatch_path(
self, scope: AsgiWebsocketScope
) -> bool: # pragma: no cover
def match_dispatch_path(self, scope: AsgiWebsocketScope) -> bool: # nocov
"""We do not use a WebSocket dispatcher for Client-Side Rendering (CSR)."""
return False

Expand Down
6 changes: 3 additions & 3 deletions src/reactpy/executors/asgi/standalone.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
RootComponentConstructor,
VdomDict,
)
from reactpy.utils import html_to_vdom, import_dotted_path
from reactpy.utils import import_dotted_path, string_to_reactpy

_logger = getLogger(__name__)

Expand Down Expand Up @@ -74,7 +74,7 @@ def __init__(
extra_py = pyscript_options.get("extra_py", [])
extra_js = pyscript_options.get("extra_js", {})
config = pyscript_options.get("config", {})
pyscript_head_vdom = html_to_vdom(
pyscript_head_vdom = string_to_reactpy(
pyscript_setup_html(extra_py, extra_js, config)
)
pyscript_head_vdom["tagName"] = ""
Expand Down Expand Up @@ -182,7 +182,7 @@ class ReactPyApp:
async def __call__(
self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend
) -> None:
if scope["type"] != "http": # pragma: no cover
if scope["type"] != "http": # nocov
if scope["type"] != "lifespan":
msg = (
"ReactPy app received unsupported request of type '%s' at path '%s'",
Expand Down
6 changes: 3 additions & 3 deletions src/reactpy/executors/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
REACTPY_RECONNECT_MAX_RETRIES,
)
from reactpy.types import ReactPyConfig, VdomDict
from reactpy.utils import import_dotted_path, vdom_to_html
from reactpy.utils import import_dotted_path, reactpy_to_string

logger = logging.getLogger(__name__)

Expand All @@ -25,7 +25,7 @@ def import_components(dotted_paths: Iterable[str]) -> dict[str, Any]:
}


def check_path(url_path: str) -> str: # pragma: no cover
def check_path(url_path: str) -> str: # nocov
"""Check that a path is valid URL path."""
if not url_path:
return "URL path must not be empty."
Expand All @@ -41,7 +41,7 @@ def check_path(url_path: str) -> str: # pragma: no cover

def vdom_head_to_html(head: VdomDict) -> str:
if isinstance(head, dict) and head.get("tagName") == "head":
return vdom_to_html(head)
return reactpy_to_string(head)

raise ValueError(
"Invalid head element! Element must be either `html.head` or a string."
Expand Down
6 changes: 3 additions & 3 deletions src/reactpy/pyscript/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from reactpy import component, hooks
from reactpy.pyscript.utils import pyscript_component_html
from reactpy.types import ComponentType, Key
from reactpy.utils import html_to_vdom
from reactpy.utils import string_to_reactpy

if TYPE_CHECKING:
from reactpy.types import VdomDict
Expand All @@ -22,15 +22,15 @@ def _pyscript_component(
raise ValueError("At least one file path must be provided.")

rendered, set_rendered = hooks.use_state(False)
initial = html_to_vdom(initial) if isinstance(initial, str) else initial
initial = string_to_reactpy(initial) if isinstance(initial, str) else initial

if not rendered:
# FIXME: This is needed to properly re-render PyScript during a WebSocket
# disconnection / reconnection. There may be a better way to do this in the future.
set_rendered(True)
return None

component_vdom = html_to_vdom(
component_vdom = string_to_reactpy(
pyscript_component_html(tuple(str(fp) for fp in file_paths), initial, root)
)
component_vdom["tagName"] = ""
Expand Down
6 changes: 3 additions & 3 deletions src/reactpy/pyscript/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import reactpy
from reactpy.config import REACTPY_DEBUG, REACTPY_PATH_PREFIX, REACTPY_WEB_MODULES_DIR
from reactpy.types import VdomDict
from reactpy.utils import vdom_to_html
from reactpy.utils import reactpy_to_string

if TYPE_CHECKING:
from collections.abc import Sequence
Expand Down Expand Up @@ -77,7 +77,7 @@ def pyscript_component_html(
file_paths: Sequence[str], initial: str | VdomDict, root: str
) -> str:
"""Renders a PyScript component with the user's code."""
_initial = initial if isinstance(initial, str) else vdom_to_html(initial)
_initial = initial if isinstance(initial, str) else reactpy_to_string(initial)
uuid = uuid4().hex
executor_code = pyscript_executor_html(file_paths=file_paths, uuid=uuid, root=root)

Expand Down Expand Up @@ -144,7 +144,7 @@ def extend_pyscript_config(
return orjson.dumps(pyscript_config).decode("utf-8")


def reactpy_version_string() -> str: # pragma: no cover
def reactpy_version_string() -> str: # nocov
from reactpy.testing.common import GITHUB_ACTIONS

local_version = reactpy.__version__
Expand Down
2 changes: 1 addition & 1 deletion src/reactpy/templatetags/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def render(self, *args: str, **kwargs: str) -> str:
return pyscript_setup(*args, **kwargs)

# This should never happen, but we validate it for safety.
raise ValueError(f"Unknown tag: {self.tag_name}") # pragma: no cover
raise ValueError(f"Unknown tag: {self.tag_name}") # nocov


def component(dotted_path: str, **kwargs: str) -> str:
Expand Down
10 changes: 2 additions & 8 deletions src/reactpy/testing/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT, REACTPY_WEB_MODULES_DIR
from reactpy.core._life_cycle_hook import HOOK_STACK, LifeCycleHook
from reactpy.core.events import EventHandler, to_event_handler_function
from reactpy.utils import str_to_bool


def clear_reactpy_web_modules_dir() -> None:
Expand All @@ -29,14 +30,7 @@ def clear_reactpy_web_modules_dir() -> None:


_DEFAULT_POLL_DELAY = 0.1
GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "False") in {
"y",
"yes",
"t",
"true",
"on",
"1",
}
GITHUB_ACTIONS = str_to_bool(os.getenv("GITHUB_ACTIONS", ""))


class poll(Generic[_R]): # noqa: N801
Expand Down
2 changes: 1 addition & 1 deletion src/reactpy/testing/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async def __aenter__(self) -> DisplayFixture:

self.page.set_default_timeout(REACTPY_TESTS_DEFAULT_TIMEOUT.current * 1000)

if not hasattr(self, "backend"): # pragma: no cover
if not hasattr(self, "backend"): # nocov
self.backend = BackendFixture()
await es.enter_async_context(self.backend)

Expand Down
2 changes: 1 addition & 1 deletion src/reactpy/testing/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

def find_available_port(
host: str, port_min: int = 8000, port_max: int = 9000
) -> int: # pragma: no cover
) -> int: # nocov
"""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:
Expand Down
Loading