From 03f83e7d23ac35eefe1749ed54069d87b27092b9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:26:33 -0800 Subject: [PATCH 01/21] Fix hook stack errors --- src/reactpy/core/_life_cycle_hook.py | 5 +- src/reactpy/core/layout.py | 74 +++++++++++++++------------- src/reactpy/core/serve.py | 26 +++++----- tests/conftest.py | 17 ------- 4 files changed, 54 insertions(+), 68 deletions(-) diff --git a/src/reactpy/core/_life_cycle_hook.py b/src/reactpy/core/_life_cycle_hook.py index 14b1bc084..e0f88a169 100644 --- a/src/reactpy/core/_life_cycle_hook.py +++ b/src/reactpy/core/_life_cycle_hook.py @@ -33,7 +33,10 @@ class _HookStack(Singleton): # nocov ) def get(self) -> list[LifeCycleHook]: - return self._state.get() + try: + return self._state.get() + except LookupError: + return [] def initialize(self) -> Token[list[LifeCycleHook]] | None: return None if isinstance(self._state, ThreadLocal) else self._state.set([]) diff --git a/src/reactpy/core/layout.py b/src/reactpy/core/layout.py index 5ed2a204e..4e8ebda3d 100644 --- a/src/reactpy/core/layout.py +++ b/src/reactpy/core/layout.py @@ -34,7 +34,7 @@ REACTPY_CHECK_VDOM_SPEC, REACTPY_DEBUG, ) -from reactpy.core._life_cycle_hook import LifeCycleHook +from reactpy.core._life_cycle_hook import HOOK_STACK, LifeCycleHook from reactpy.core.vdom import validate_vdom_json from reactpy.types import ( BaseLayout, @@ -162,43 +162,47 @@ async def _parallel_render(self) -> LayoutUpdateMessage: async def _create_layout_update( self, old_state: _ModelState ) -> LayoutUpdateMessage: - component = old_state.life_cycle_state.component + token = HOOK_STACK.initialize() try: - parent: _ModelState | None = old_state.parent - except AttributeError: - parent = None - - async with AsyncExitStack() as exit_stack: - new_state = await self._render_component( - exit_stack, - old_state, - parent, - old_state.index, - old_state.key, - component, - ) + component = old_state.life_cycle_state.component + try: + parent: _ModelState | None = old_state.parent + except AttributeError: + parent = None + + async with AsyncExitStack() as exit_stack: + new_state = await self._render_component( + exit_stack, + old_state, + parent, + old_state.index, + old_state.key, + component, + ) - if parent is not None: - parent.children_by_key[new_state.key] = new_state - old_parent_model = parent.model.current - old_parent_children = old_parent_model.setdefault("children", []) - parent.model.current = { - **old_parent_model, - "children": [ - *old_parent_children[: new_state.index], - new_state.model.current, - *old_parent_children[new_state.index + 1 :], - ], + if parent is not None: + parent.children_by_key[new_state.key] = new_state + old_parent_model = parent.model.current + old_parent_children = old_parent_model.setdefault("children", []) + parent.model.current = { + **old_parent_model, + "children": [ + *old_parent_children[: new_state.index], + new_state.model.current, + *old_parent_children[new_state.index + 1 :], + ], + } + + if REACTPY_CHECK_VDOM_SPEC.current: + validate_vdom_json(new_state.model.current) + + return { + "type": "layout-update", + "path": new_state.patch_path, + "model": new_state.model.current, } - - if REACTPY_CHECK_VDOM_SPEC.current: - validate_vdom_json(new_state.model.current) - - return { - "type": "layout-update", - "path": new_state.patch_path, - "model": new_state.model.current, - } + finally: + HOOK_STACK.reset(token) async def _render_component( self, diff --git a/src/reactpy/core/serve.py b/src/reactpy/core/serve.py index 435cd442c..c8fcec4fb 100644 --- a/src/reactpy/core/serve.py +++ b/src/reactpy/core/serve.py @@ -45,22 +45,18 @@ async def _single_outgoing_loop( send: SendCoroutine, ) -> None: while True: - token = HOOK_STACK.initialize() + update = await layout.render() try: - update = await layout.render() - try: - await send(update) - except Exception: # nocov - if not REACTPY_DEBUG.current: - msg = ( - "Failed to send update. More info may be available " - "if you enabling debug mode by setting " - "`reactpy.config.REACTPY_DEBUG.current = True`." - ) - logger.error(msg) - raise - finally: - HOOK_STACK.reset(token) + await send(update) + except Exception: # nocov + if not REACTPY_DEBUG.current: + msg = ( + "Failed to send update. More info may be available " + "if you enabling debug mode by setting " + "`reactpy.config.REACTPY_DEBUG.current = True`." + ) + logger.error(msg) + raise async def _single_incoming_loop( diff --git a/tests/conftest.py b/tests/conftest.py index 167a85b26..8531f9874 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,23 +54,6 @@ def rebuild(): subprocess.run(["hatch", "build", "-t", "wheel"], check=True, env=env) # noqa: S607 -@pytest.fixture(autouse=True, scope="function") -def create_hook_state(): - """This fixture is a bug fix related to `pytest_asyncio`. - - Usually the hook stack is created automatically within the display fixture, but context - variables aren't retained within `pytest_asyncio` async fixtures. As a workaround, - this fixture ensures that the hook stack is created before each test is run. - - Ref: https://github.com/pytest-dev/pytest-asyncio/issues/127 - """ - from reactpy.core._life_cycle_hook import HOOK_STACK - - token = HOOK_STACK.initialize() - yield token - HOOK_STACK.reset(token) - - @pytest.fixture async def display(server, page): async with DisplayFixture(server, page) as display: From 5e10713e8b24b6a77a9305087c9aafcb48ea46bd Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:44:54 -0800 Subject: [PATCH 02/21] fix lint --- src/reactpy/core/serve.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/reactpy/core/serve.py b/src/reactpy/core/serve.py index c8fcec4fb..622d8b1ac 100644 --- a/src/reactpy/core/serve.py +++ b/src/reactpy/core/serve.py @@ -8,7 +8,6 @@ from anyio.abc import TaskGroup from reactpy.config import REACTPY_DEBUG -from reactpy.core._life_cycle_hook import HOOK_STACK from reactpy.types import BaseLayout, LayoutEventMessage, LayoutUpdateMessage logger = getLogger(__name__) From 8a9737aa4a52acce6f1ee3e761dfdfc8826dd1b7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:46:00 -0800 Subject: [PATCH 03/21] hatch fmt tests --- tests/test_asgi/test_init.py | 2 +- tests/test_asgi/test_middleware.py | 4 ++-- tests/test_asgi/test_utils.py | 2 +- tests/test_client.py | 2 +- tests/test_config.py | 2 +- tests/test_core/test_events.py | 6 +++--- tests/test_core/test_hooks.py | 2 +- tests/test_core/test_layout.py | 6 +++--- tests/test_core/test_vdom.py | 12 ++++++------ tests/test_html.py | 10 ++++++---- tests/test_option.py | 18 +++++++++--------- tests/test_testing.py | 2 +- tests/test_utils.py | 8 ++++---- tests/test_web/test_module.py | 4 ++-- tests/tooling/select.py | 3 +-- 15 files changed, 42 insertions(+), 41 deletions(-) diff --git a/tests/test_asgi/test_init.py b/tests/test_asgi/test_init.py index fd6124394..f7ef2d78d 100644 --- a/tests/test_asgi/test_init.py +++ b/tests/test_asgi/test_init.py @@ -15,7 +15,7 @@ def test_asgi_import_error(): ModuleNotFoundError, match=r"ASGI executors require the 'reactpy\[asgi\]' extra to be installed", ): - import reactpy.executors.asgi + import reactpy.executors.asgi # noqa: F401 # Clean up if "reactpy.executors.asgi" in sys.modules: diff --git a/tests/test_asgi/test_middleware.py b/tests/test_asgi/test_middleware.py index fc76571eb..476187072 100644 --- a/tests/test_asgi/test_middleware.py +++ b/tests/test_asgi/test_middleware.py @@ -37,7 +37,7 @@ async def homepage(request): def test_invalid_path_prefix(): - with pytest.raises(ValueError, match="Invalid `path_prefix`*"): + with pytest.raises(ValueError, match=r"Invalid `path_prefix`*"): async def app(scope, receive, send): pass @@ -47,7 +47,7 @@ async def app(scope, receive, send): def test_invalid_web_modules_dir(): with pytest.raises( - ValueError, match='Web modules directory "invalid" does not exist.' + ValueError, match=r'Web modules directory "invalid" does not exist.' ): async def app(scope, receive, send): diff --git a/tests/test_asgi/test_utils.py b/tests/test_asgi/test_utils.py index 7b65b2d80..369283dce 100644 --- a/tests/test_asgi/test_utils.py +++ b/tests/test_asgi/test_utils.py @@ -17,5 +17,5 @@ def test_process_settings(): def test_invalid_setting(): - with pytest.raises(ValueError, match='Unknown ReactPy setting "foobar".'): + with pytest.raises(ValueError, match=r'Unknown ReactPy setting "foobar".'): utils.process_settings({"foobar": True}) diff --git a/tests/test_client.py b/tests/test_client.py index 7815dcce8..39e59ad41 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -111,7 +111,7 @@ async def test_slow_server_response_on_input_change(display: DisplayFixture): @reactpy.component def SomeComponent(): - value, set_value = reactpy.hooks.use_state("") + _value, set_value = reactpy.hooks.use_state("") async def handle_change(event): await asyncio.sleep(delay) diff --git a/tests/test_config.py b/tests/test_config.py index e5c6457c5..37bc9174e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -15,7 +15,7 @@ def reset_options(): yield - for opt, val in zip(options, original_values): + for opt, val in zip(options, original_values, strict=False): if val is should_unset: if opt.is_set(): opt.unset() diff --git a/tests/test_core/test_events.py b/tests/test_core/test_events.py index 6e22abf5c..dd99018b5 100644 --- a/tests/test_core/test_events.py +++ b/tests/test_core/test_events.py @@ -86,7 +86,7 @@ def func(*args): async def test_merge_event_handler_empty_list(): - with pytest.raises(ValueError, match="No event handlers to merge"): + with pytest.raises(ValueError, match=r"No event handlers to merge"): merge_event_handlers([]) @@ -102,7 +102,7 @@ async def test_merge_event_handlers_raises_on_mismatch(kwargs_1, kwargs_2): def func(data): return None - with pytest.raises(ValueError, match="Cannot merge handlers"): + with pytest.raises(ValueError, match=r"Cannot merge handlers"): merge_event_handlers( [ EventHandler(func, **kwargs_1), @@ -127,7 +127,7 @@ async def test_merge_event_handlers(): def test_merge_event_handler_funcs_empty_list(): - with pytest.raises(ValueError, match="No event handler functions to merge"): + with pytest.raises(ValueError, match=r"No event handler functions to merge"): merge_event_handler_funcs([]) diff --git a/tests/test_core/test_hooks.py b/tests/test_core/test_hooks.py index 41a568326..ae9d05c34 100644 --- a/tests/test_core/test_hooks.py +++ b/tests/test_core/test_hooks.py @@ -20,7 +20,7 @@ def SimpleComponentWithHook(): reactpy.hooks.use_state(None) return reactpy.html.div() - with pytest.raises(RuntimeError, match="No life cycle hook is active"): + with pytest.raises(RuntimeError, match=r"No life cycle hook is active"): await SimpleComponentWithHook().render() async with Layout(SimpleComponentWithHook()) as layout: diff --git a/tests/test_core/test_layout.py b/tests/test_core/test_layout.py index c3330d882..211487db6 100644 --- a/tests/test_core/test_layout.py +++ b/tests/test_core/test_layout.py @@ -57,9 +57,9 @@ def MyComponent(): ... def test_layout_expects_abstract_component(): - with pytest.raises(TypeError, match="Expected a ReactPy component"): + with pytest.raises(TypeError, match=r"Expected a ReactPy component"): Layout(None) - with pytest.raises(TypeError, match="Expected a ReactPy component"): + with pytest.raises(TypeError, match=r"Expected a ReactPy component"): Layout(reactpy.html.div()) @@ -449,7 +449,7 @@ def AnyComponent(): layout.render(), timeout=0.1, # this should have been plenty of time ) - except asyncio.TimeoutError: + except TimeoutError: pass # the render should still be rendering since we only update once assert run_count.current == 2 diff --git a/tests/test_core/test_vdom.py b/tests/test_core/test_vdom.py index 68d27e6fa..c1436f1d7 100644 --- a/tests/test_core/test_vdom.py +++ b/tests/test_core/test_vdom.py @@ -117,7 +117,7 @@ def test_make_vdom_constructor(): no_children = Vdom("no-children", allow_children=False) - with pytest.raises(TypeError, match="cannot have children"): + with pytest.raises(TypeError, match=r"cannot have children"): no_children([1, 2, 3]) assert no_children() == {"tagName": "no-children"} @@ -127,7 +127,7 @@ def test_nested_html_access_raises_error(): elmt = Vdom("div") with pytest.raises( - AttributeError, match="can only be accessed on web module components" + AttributeError, match=r"can only be accessed on web module components" ): elmt.fails() @@ -330,16 +330,16 @@ def MyComponent(): @pytest.mark.skipif(not REACTPY_DEBUG.current, reason="only checked in debug mode") def test_raise_for_non_json_attrs(): - with pytest.raises(TypeError, match="JSON serializable"): + with pytest.raises(TypeError, match=r"JSON serializable"): reactpy.html.div({"nonJsonSerializableObject": object()}) def test_invalid_vdom_keys(): - with pytest.raises(ValueError, match="Invalid keys:*"): + with pytest.raises(ValueError, match=r"Invalid keys:*"): reactpy.types.VdomDict(tagName="test", foo="bar") - with pytest.raises(KeyError, match="Invalid key:*"): + with pytest.raises(KeyError, match=r"Invalid key:*"): reactpy.types.VdomDict(tagName="test")["foo"] = "bar" - with pytest.raises(ValueError, match="VdomDict requires a 'tagName' key."): + with pytest.raises(ValueError, match=r"VdomDict requires a 'tagName' key."): reactpy.types.VdomDict(foo="bar") diff --git a/tests/test_html.py b/tests/test_html.py index 151857a57..5251eb189 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -87,17 +87,19 @@ def HasScript(): def test_script_may_only_have_one_child(): - with pytest.raises(ValueError, match="'script' nodes may have, at most, one child"): + with pytest.raises( + ValueError, match=r"'script' nodes may have, at most, one child" + ): html.script("one child", "two child") def test_child_of_script_must_be_string(): - with pytest.raises(ValueError, match="The child of a 'script' must be a string"): + with pytest.raises(ValueError, match=r"The child of a 'script' must be a string"): html.script(1) def test_script_has_no_event_handlers(): - with pytest.raises(ValueError, match="do not support event handlers"): + with pytest.raises(ValueError, match=r"do not support event handlers"): html.script({"onEvent": lambda: None}) @@ -113,7 +115,7 @@ def test_simple_fragment(): def test_fragment_can_have_no_attributes(): - with pytest.raises(TypeError, match="Fragments cannot have attributes"): + with pytest.raises(TypeError, match=r"Fragments cannot have attributes"): html.fragment({"someAttribute": 1}) diff --git a/tests/test_option.py b/tests/test_option.py index 929e17488..8c5492150 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -33,16 +33,16 @@ def test_option_validator(): opt.current = "0" assert opt.current is False - with pytest.raises(ValueError, match="Invalid value"): + with pytest.raises(ValueError, match=r"Invalid value"): opt.current = "not-an-int" def test_immutable_option(): opt = Option("A_FAKE_OPTION", "default-value", mutable=False) assert not opt.mutable - with pytest.raises(TypeError, match="cannot be modified after initial load"): + with pytest.raises(TypeError, match=r"cannot be modified after initial load"): opt.current = "a-new-value" - with pytest.raises(TypeError, match="cannot be modified after initial load"): + with pytest.raises(TypeError, match=r"cannot be modified after initial load"): opt.unset() @@ -78,7 +78,7 @@ def test_option_set_default(): def test_cannot_subscribe_immutable_option(): opt = Option("A_FAKE_OPTION", "default", mutable=False) - with pytest.raises(TypeError, match="Immutable options cannot be subscribed to"): + with pytest.raises(TypeError, match=r"Immutable options cannot be subscribed to"): opt.subscribe(lambda value: None) @@ -104,10 +104,10 @@ def test_option_subscribe(): def test_deprecated_option(): opt = DeprecatedOption("A_FAKE_OPTION", None, message="is deprecated!") - with pytest.warns(DeprecationWarning, match="is deprecated!"): + with pytest.warns(DeprecationWarning, match=r"is deprecated!"): assert opt.current is None - with pytest.warns(DeprecationWarning, match="is deprecated!"): + with pytest.warns(DeprecationWarning, match=r"is deprecated!"): opt.current = "something" @@ -124,14 +124,14 @@ def test_option_parent(): def test_option_parent_child_must_be_mutable(): mut_parent_opt = Option("A_FAKE_OPTION", "default-value", mutable=True) immu_parent_opt = Option("A_FAKE_OPTION", "default-value", mutable=False) - with pytest.raises(TypeError, match="must be mutable"): + with pytest.raises(TypeError, match=r"must be mutable"): Option("A_FAKE_OPTION", parent=mut_parent_opt, mutable=False) - with pytest.raises(TypeError, match="must be mutable"): + with pytest.raises(TypeError, match=r"must be mutable"): Option("A_FAKE_OPTION", parent=immu_parent_opt, mutable=None) def test_no_default_or_parent(): with pytest.raises( - TypeError, match="Must specify either a default or a parent option" + TypeError, match=r"Must specify either a default or a parent option" ): Option("A_FAKE_OPTION") diff --git a/tests/test_testing.py b/tests/test_testing.py index ad7a9af48..3318bb2c4 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -11,7 +11,7 @@ def test_assert_reactpy_logged_does_not_suppress_errors(): - with pytest.raises(RuntimeError, match="expected error"): + with pytest.raises(RuntimeError, match=r"expected error"): with testing.assert_reactpy_did_log(): msg = "expected error" raise RuntimeError(msg) diff --git a/tests/test_utils.py b/tests/test_utils.py index 2e4c41c83..01475219a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -391,22 +391,22 @@ def test_reactpy_to_string(vdom_in, html_out): def test_reactpy_to_string_error(): - with pytest.raises(TypeError, match="Expected a VDOM dict"): + with pytest.raises(TypeError, match=r"Expected a VDOM dict"): utils.reactpy_to_string({"notVdom": True}) def test_invalid_dotted_path(): - with pytest.raises(ValueError, match='"abc" is not a valid dotted path.'): + with pytest.raises(ValueError, match=r'"abc" is not a valid dotted path.'): utils.import_dotted_path("abc") def test_invalid_component(): with pytest.raises( - AttributeError, match='ReactPy failed to import "foobar" from "reactpy"' + AttributeError, match=r'ReactPy failed to import "foobar" from "reactpy"' ): utils.import_dotted_path("reactpy.foobar") def test_invalid_module(): - with pytest.raises(ImportError, match='ReactPy failed to import "foo"'): + with pytest.raises(ImportError, match=r'ReactPy failed to import "foo"'): utils.import_dotted_path("foo.bar") diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index 85116007c..9357d1c36 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -100,7 +100,7 @@ def ShowSimpleButton(): def test_module_from_file_source_conflict(tmp_path): first_file = tmp_path / "first.js" - with pytest.raises(FileNotFoundError, match="does not exist"): + with pytest.raises(FileNotFoundError, match=r"does not exist"): reactpy.web.module._module_from_file("temp", first_file) first_file.touch() @@ -165,7 +165,7 @@ def test_web_module_from_file_replace_existing(tmp_path): def test_module_missing_exports(): module = WebModule("test", NAME_SOURCE, None, {"a", "b", "c"}, None, False) - with pytest.raises(ValueError, match="does not export 'x'"): + with pytest.raises(ValueError, match=r"does not export 'x'"): reactpy.web.module._vdom_from_web_module(module, "x") with pytest.raises(ValueError, match=r"does not export \['x', 'y'\]"): diff --git a/tests/tooling/select.py b/tests/tooling/select.py index 2a0f170b8..7e93f7c4b 100644 --- a/tests/tooling/select.py +++ b/tests/tooling/select.py @@ -1,8 +1,7 @@ from __future__ import annotations -from collections.abc import Iterator, Sequence +from collections.abc import Callable, Iterator, Sequence from dataclasses import dataclass -from typing import Callable from reactpy.types import VdomJson From a1746f3d913b1aa23b047b7f333206aa5c36fc7a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:12:50 -0800 Subject: [PATCH 04/21] Remove `key` from VdomDict --- docs/source/about/changelog.rst | 1 + .../packages/@reactpy/client/src/components.tsx | 6 +++--- src/js/packages/@reactpy/client/src/types.ts | 1 - src/reactpy/_html.py | 16 ++++++++-------- src/reactpy/core/layout.py | 7 ++++--- src/reactpy/core/vdom.py | 8 +++----- src/reactpy/transforms.py | 4 ++-- src/reactpy/types.py | 10 +--------- tests/test_html.py | 7 +++++-- tests/test_utils.py | 9 +++------ 10 files changed, 30 insertions(+), 39 deletions(-) diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index 8f7e87c78..2deb0c49b 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -39,6 +39,7 @@ Unreleased **Changed** +- :pull:`1284` - The ``key`` attribute is now stored within ``attributes`` in the VDOM spec. - :pull:`1251` - Substitute client-side usage of ``react`` with ``preact``. - :pull:`1239` - Script elements no longer support behaving like effects. They now strictly behave like plain HTML scripts. - :pull:`1255` - The ``reactpy.html`` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a ```` element by calling ``html.data_table()``. diff --git a/src/js/packages/@reactpy/client/src/components.tsx b/src/js/packages/@reactpy/client/src/components.tsx index a4fa97ce3..d7c5825bd 100644 --- a/src/js/packages/@reactpy/client/src/components.tsx +++ b/src/js/packages/@reactpy/client/src/components.tsx @@ -67,7 +67,7 @@ function StandardElement({ model }: { model: ReactPyVdom }) { model.tagName === "" ? Fragment : model.tagName, createAttributes(model, client), ...createChildren(model, (child) => { - return ; + return ; }), ); } @@ -100,7 +100,7 @@ function UserInputElement({ model }: { model: ReactPyVdom }): JSX.Element { // overwrite { ...props, value }, ...createChildren(model, (child) => ( - + )), ); } @@ -135,7 +135,7 @@ function ScriptElement({ model }: { model: ReactPyVdom }) { return () => { ref.current?.removeChild(scriptElement); }; - }, [model.key]); + }, [model.attributes?.key]); return
; } diff --git a/src/js/packages/@reactpy/client/src/types.ts b/src/js/packages/@reactpy/client/src/types.ts index 49232e532..12bc8f3fa 100644 --- a/src/js/packages/@reactpy/client/src/types.ts +++ b/src/js/packages/@reactpy/client/src/types.ts @@ -48,7 +48,6 @@ export type ReactPyComponent = ComponentType<{ model: ReactPyVdom }>; export type ReactPyVdom = { tagName: string; - key?: string; attributes?: { [key: string]: string }; children?: (ReactPyVdom | string)[]; error?: string; diff --git a/src/reactpy/_html.py b/src/reactpy/_html.py index ffeee7072..82dbba963 100644 --- a/src/reactpy/_html.py +++ b/src/reactpy/_html.py @@ -6,7 +6,6 @@ from reactpy.core.vdom import Vdom from reactpy.types import ( EventHandlerDict, - Key, VdomAttributes, VdomChild, VdomChildren, @@ -100,12 +99,10 @@ def _fragment( attributes: VdomAttributes, children: Sequence[VdomChild], - key: Key | None, event_handlers: EventHandlerDict, ) -> VdomDict: """An HTML fragment - this element will not appear in the DOM""" - attributes.pop("key", None) - if attributes or event_handlers: + if any(k != "key" for k in attributes) or event_handlers: msg = "Fragments cannot have attributes besides 'key'" raise TypeError(msg) model = VdomDict(tagName="") @@ -113,8 +110,8 @@ def _fragment( if children: model["children"] = children - if key is not None: - model["key"] = key + if attributes: + model["attributes"] = attributes return model @@ -122,7 +119,6 @@ def _fragment( def _script( attributes: VdomAttributes, children: Sequence[VdomChild], - key: Key | None, event_handlers: EventHandlerDict, ) -> VdomDict: """Create a new `