diff --git a/src/js/bun.lockb b/src/js/bun.lockb index 21e1e7ca7..e7927347c 100644 Binary files a/src/js/bun.lockb and b/src/js/bun.lockb differ diff --git a/src/reactpy/__init__.py b/src/reactpy/__init__.py index 0f2fe0d63..422f16781 100644 --- a/src/reactpy/__init__.py +++ b/src/reactpy/__init__.py @@ -1,5 +1,5 @@ from reactpy import config, logging, reactjs, types, web, widgets -from reactpy._html import html +from reactpy._html import h, html from reactpy.core import hooks from reactpy.core.component import component from reactpy.core.events import event @@ -23,7 +23,7 @@ from reactpy.utils import Ref, reactpy_to_string, string_to_reactpy __author__ = "The Reactive Python Team" -__version__ = "2.0.0b9" +__version__ = "2.0.0b10" __all__ = [ "Ref", @@ -32,6 +32,7 @@ "config", "create_context", "event", + "h", "hooks", "html", "logging", diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index 6ebf90fdc..4966f9f4e 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -9,7 +9,7 @@ from lxml import etree from lxml.html import fromstring -from reactpy import html +from reactpy import h from reactpy.transforms import RequiredTransforms, attributes_to_reactjs from reactpy.types import Component, VdomDict @@ -105,9 +105,14 @@ def string_to_reactpy( event handler that prevents the browser from navigating to the link. This is useful if you would rather have `reactpy-router` handle your URL navigation. """ - if not isinstance(html, str): # nocov + if not isinstance(html, str): msg = f"Expected html to be a string, not {type(html).__name__}" raise TypeError(msg) + if not html.strip(): + return h.fragment() + if "<" not in html or ">" not in html: + msg = "Expected html string to contain HTML tags, but no tags were found." + raise ValueError(msg) # If the user provided a string, convert it to a list of lxml.etree nodes try: @@ -153,7 +158,7 @@ def _etree_to_vdom( attributes = attributes_to_reactjs(dict(node.items())) # Convert the lxml node to a VDOM dict - constructor = getattr(html, str(node.tag)) + constructor = getattr(h, str(node.tag)) el = constructor(attributes, children) # Perform necessary transformations on the VDOM attributes to meet VDOM spec @@ -236,13 +241,13 @@ def component_to_vdom(component: Component) -> VdomDict: """Convert the first render of a component into a VDOM dictionary""" result = component.render() + if result is None: + return h.fragment() if isinstance(result, dict): return result if hasattr(result, "render"): return component_to_vdom(cast(Component, result)) - elif isinstance(result, str): - return html.div(result) - return html() + return h.div(result) if isinstance(result, str) else h.div() def _react_attribute_to_html(key: str, value: Any) -> tuple[str, str]: diff --git a/tests/conftest.py b/tests/conftest.py index 96787a799..85aa2126d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,8 @@ ) from reactpy.testing.display import _playwright_visible +from . import pytestmark # noqa: F401 + REACTPY_ASYNC_RENDERING.set_current(True) REACTPY_DEBUG.set_current(True) diff --git a/tests/test_client.py b/tests/test_client.py index e05286f74..8098a3f98 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,6 +6,8 @@ from tests.tooling.common import DEFAULT_TYPE_DELAY from tests.tooling.hooks import use_counter +from . import pytestmark # noqa: F401 + JS_DIR = Path(__file__).parent / "js" diff --git a/tests/test_core/test_component.py b/tests/test_core/test_component.py index 4cbfebd54..31e63fadc 100644 --- a/tests/test_core/test_component.py +++ b/tests/test_core/test_component.py @@ -1,6 +1,8 @@ import reactpy from reactpy.testing import DisplayFixture +from .. import pytestmark # noqa: F401 + def test_component_repr(): @reactpy.component diff --git a/tests/test_core/test_events.py b/tests/test_core/test_events.py index dd99018b5..52b515308 100644 --- a/tests/test_core/test_events.py +++ b/tests/test_core/test_events.py @@ -13,6 +13,8 @@ from reactpy.types import Event from tests.tooling.common import DEFAULT_TYPE_DELAY +from .. import pytestmark # noqa: F401 + def test_event_handler_repr(): handler = EventHandler(lambda: None) diff --git a/tests/test_core/test_hooks.py b/tests/test_core/test_hooks.py index 8e5814152..c1816ea60 100644 --- a/tests/test_core/test_hooks.py +++ b/tests/test_core/test_hooks.py @@ -13,6 +13,8 @@ from reactpy.utils import Ref from tests.tooling.common import DEFAULT_TYPE_DELAY, update_message +from .. import pytestmark # noqa: F401 + async def test_must_be_rendering_in_layout_to_use_hooks(): @reactpy.component diff --git a/tests/test_html.py b/tests/test_html.py index 930b3e2fb..f97bf49b2 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -7,6 +7,8 @@ from tests.tooling.common import DEFAULT_TYPE_DELAY from tests.tooling.hooks import use_counter +from . import pytestmark # noqa: F401 + async def test_script_re_run_on_content_change(display: DisplayFixture): @component diff --git a/tests/test_pyscript/test_components.py b/tests/test_pyscript/test_components.py index 6d1080a57..cb8b729c2 100644 --- a/tests/test_pyscript/test_components.py +++ b/tests/test_pyscript/test_components.py @@ -8,6 +8,8 @@ from reactpy.testing import BackendFixture, DisplayFixture from reactpy.testing.backend import root_hotswap_component +from .. import pytestmark # noqa: F401 + @pytest.fixture(scope="module") async def display(browser): diff --git a/tests/test_reactjs/test_modules.py b/tests/test_reactjs/test_modules.py index 9094310a1..8152fdfd3 100644 --- a/tests/test_reactjs/test_modules.py +++ b/tests/test_reactjs/test_modules.py @@ -7,6 +7,8 @@ from reactpy.reactjs import component_from_string, import_reactjs from reactpy.testing import BackendFixture, DisplayFixture +from .. import pytestmark # noqa: F401 + JS_FIXTURES_DIR = Path(__file__).parent / "js_fixtures" diff --git a/tests/test_sample.py b/tests/test_sample.py index 1e6654c0e..3fb95f9a2 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -1,6 +1,8 @@ from reactpy.testing import DisplayFixture from tests.sample import SampleApp +from . import pytestmark # noqa: F401 + async def test_sample_app(display: DisplayFixture): await display.show(SampleApp) diff --git a/tests/test_testing.py b/tests/test_testing.py index 60435a8cf..ffefde330 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -10,6 +10,8 @@ from reactpy.testing.display import DisplayFixture from tests.sample import SampleApp +from . import pytestmark # noqa: F401 + def test_assert_reactpy_logged_does_not_suppress_errors(): with pytest.raises(RuntimeError, match=r"expected error"): diff --git a/tests/test_utils.py b/tests/test_utils.py index 52cea0f6a..f15277a9d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -92,6 +92,23 @@ def test_string_to_reactpy(case): assert utils.string_to_reactpy(case["source"]) == case["model"] +@pytest.mark.parametrize("source", ["", " ", "\n\t "]) +def test_string_to_reactpy_empty_source(source): + assert utils.string_to_reactpy(source) == html.fragment() + + +@pytest.mark.parametrize("source", [123, None, object()]) +def test_string_to_reactpy_non_string_source(source): + with pytest.raises(TypeError): + utils.string_to_reactpy(source) # type: ignore[arg-type] + + +@pytest.mark.parametrize("source", ["no tags", "plain text", "just words"]) +def test_string_to_reactpy_missing_tags(source): + with pytest.raises(ValueError): + utils.string_to_reactpy(source) + + @pytest.mark.parametrize( "case", [ diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index 9b5cb87d9..72f5518de 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -21,6 +21,8 @@ ) from reactpy.types import InlineJavaScript +from .. import pytestmark # noqa: F401 + JS_FIXTURES_DIR = Path(__file__).parent / "js_fixtures"