diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index 9f833d28f..4e9d753d2 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -32,6 +32,7 @@ Unreleased - :pull:`1113` - Renamed the ``use_location`` hook's ``search`` attribute to ``query_string``. - :pull:`1113` - Renamed the ``use_location`` hook's ``pathname`` attribute to ``path``. - :pull:`1113` - Renamed ``reactpy.config.REACTPY_DEBUG_MODE`` to ``reactpy.config.REACTPY_DEBUG``. +- :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. **Removed** diff --git a/pyproject.toml b/pyproject.toml index 92430e71b..4ca1a411a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,11 +18,12 @@ authors = [ ] requires-python = ">=3.9" classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] @@ -60,6 +61,9 @@ license-files = { paths = ["LICENSE"] } [tool.hatch.envs.default] installer = "uv" +[project.scripts] +reactpy = "reactpy._console.cli:entry_point" + [[tool.hatch.build.hooks.build-scripts.scripts]] # Note: `hatch` can't be called within `build-scripts` when installing packages in editable mode, so we have to write the commands long-form commands = [ @@ -162,8 +166,6 @@ extra-dependencies = [ "mypy==1.8", "types-toml", "types-click", - "types-tornado", - "types-flask", "types-requests", ] @@ -194,6 +196,7 @@ test = [ ] build = [ 'hatch run "src/build_scripts/clean_js_dir.py"', + 'bun install --cwd "src/js"', 'hatch run javascript:build_event_to_object', 'hatch run javascript:build_client', 'hatch run javascript:build_app', diff --git a/src/js/packages/@reactpy/client/package.json b/src/js/packages/@reactpy/client/package.json index b6b12830f..95e545eb4 100644 --- a/src/js/packages/@reactpy/client/package.json +++ b/src/js/packages/@reactpy/client/package.json @@ -1,6 +1,6 @@ { "name": "@reactpy/client", - "version": "0.3.2", + "version": "1.0.0", "description": "A client for ReactPy implemented in React", "author": "Ryan Morshead", "license": "MIT", diff --git a/src/js/packages/@reactpy/client/src/vdom.tsx b/src/js/packages/@reactpy/client/src/vdom.tsx index d86d9232a..25eb9f3e7 100644 --- a/src/js/packages/@reactpy/client/src/vdom.tsx +++ b/src/js/packages/@reactpy/client/src/vdom.tsx @@ -152,8 +152,7 @@ export function createAttributes( createEventHandler(client, name, handler), ), ), - // Convert snake_case to camelCase names - }).map(normalizeAttribute), + }), ); } @@ -182,33 +181,3 @@ function createEventHandler( }, ]; } - -function normalizeAttribute([key, value]: [string, any]): [string, any] { - let normKey = key; - let normValue = value; - - if (key === "style" && typeof value === "object") { - normValue = Object.fromEntries( - Object.entries(value).map(([k, v]) => [snakeToCamel(k), v]), - ); - } else if ( - key.startsWith("data_") || - key.startsWith("aria_") || - DASHED_HTML_ATTRS.includes(key) - ) { - normKey = key.split("_").join("-"); - } else { - normKey = snakeToCamel(key); - } - return [normKey, normValue]; -} - -function snakeToCamel(str: string): string { - return str.replace(/([_][a-z])/g, (group) => - group.toUpperCase().replace("_", ""), - ); -} - -// see list of HTML attributes with dashes in them: -// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#attribute_list -const DASHED_HTML_ATTRS = ["accept_charset", "http_equiv"]; diff --git a/src/reactpy/__main__.py b/src/reactpy/__main__.py deleted file mode 100644 index d70ddf684..000000000 --- a/src/reactpy/__main__.py +++ /dev/null @@ -1,19 +0,0 @@ -import click - -import reactpy -from reactpy._console.rewrite_camel_case_props import rewrite_camel_case_props -from reactpy._console.rewrite_keys import rewrite_keys - - -@click.group() -@click.version_option(reactpy.__version__, prog_name=reactpy.__name__) -def app() -> None: - pass - - -app.add_command(rewrite_keys) -app.add_command(rewrite_camel_case_props) - - -if __name__ == "__main__": - app() diff --git a/src/reactpy/_console/cli.py b/src/reactpy/_console/cli.py new file mode 100644 index 000000000..720583002 --- /dev/null +++ b/src/reactpy/_console/cli.py @@ -0,0 +1,19 @@ +"""Entry point for the ReactPy CLI.""" + +import click + +import reactpy +from reactpy._console.rewrite_props import rewrite_props + + +@click.group() +@click.version_option(version=reactpy.__version__, prog_name=reactpy.__name__) +def entry_point() -> None: + pass + + +entry_point.add_command(rewrite_props) + + +if __name__ == "__main__": + entry_point() diff --git a/src/reactpy/_console/rewrite_camel_case_props.py b/src/reactpy/_console/rewrite_props.py similarity index 58% rename from src/reactpy/_console/rewrite_camel_case_props.py rename to src/reactpy/_console/rewrite_props.py index 12c96c4f3..f7ae7c656 100644 --- a/src/reactpy/_console/rewrite_camel_case_props.py +++ b/src/reactpy/_console/rewrite_props.py @@ -1,7 +1,6 @@ from __future__ import annotations import ast -import re from copy import copy from keyword import kwlist from pathlib import Path @@ -15,15 +14,13 @@ rewrite_changed_nodes, ) -CAMEL_CASE_SUB_PATTERN = re.compile(r"(? None: - """Rewrite camelCase props to snake_case""" - +def rewrite_props(paths: list[str]) -> None: + """Rewrite snake_case props to camelCase within .""" for p in map(Path, paths): + # Process each file or recursively process each Python file in directories for f in [p] if p.is_file() else p.rglob("*.py"): result = generate_rewrite(file=f, source=f.read_text(encoding="utf-8")) if result is not None: @@ -31,43 +28,66 @@ def rewrite_camel_case_props(paths: list[str]) -> None: def generate_rewrite(file: Path, source: str) -> str | None: - tree = ast.parse(source) + """Generate the rewritten source code if changes are detected""" + tree = ast.parse(source) # Parse the source code into an AST - changed = find_nodes_to_change(tree) + changed = find_nodes_to_change(tree) # Find nodes that need to be changed if not changed: - return None + return None # Return None if no changes are needed - new = rewrite_changed_nodes(file, source, tree, changed) + new = rewrite_changed_nodes( + file, source, tree, changed + ) # Rewrite the changed nodes return new def find_nodes_to_change(tree: ast.AST) -> list[ChangedNode]: + """Find nodes in the AST that need to be changed""" changed: list[ChangedNode] = [] for el_info in find_element_constructor_usages(tree): + # Check if the props need to be rewritten if _rewrite_props(el_info.props, _construct_prop_item): + # Add the changed node to the list changed.append(ChangedNode(el_info.call, el_info.parents)) return changed def conv_attr_name(name: str) -> str: - new_name = CAMEL_CASE_SUB_PATTERN.sub("_", name).lower() - return f"{new_name}_" if new_name in kwlist else new_name + """Convert snake_case attribute name to camelCase""" + # Return early if the value is a Python keyword + if name in kwlist: + return name + + # Return early if the value is not snake_case + if "_" not in name: + return name + + # Split the string by underscores + components = name.split("_") + + # Capitalize the first letter of each component except the first one + # and join them together + return components[0] + "".join(x.title() for x in components[1:]) def _construct_prop_item(key: str, value: ast.expr) -> tuple[str, ast.expr]: + """Construct a new prop item with the converted key and possibly modified value""" if key == "style" and isinstance(value, (ast.Dict, ast.Call)): + # Create a copy of the value to avoid modifying the original new_value = copy(value) if _rewrite_props( new_value, lambda k, v: ( (k, v) - # avoid infinite recursion + # Avoid infinite recursion if k == "style" else _construct_prop_item(k, v) ), ): + # Update the value if changes were made value = new_value else: + # Convert the key to camelCase key = conv_attr_name(key) return key, value @@ -76,12 +96,15 @@ def _rewrite_props( props_node: ast.Dict | ast.Call, constructor: Callable[[str, ast.expr], tuple[str, ast.expr]], ) -> bool: + """Rewrite the props in the given AST node using the provided constructor""" + did_change = False if isinstance(props_node, ast.Dict): - did_change = False keys: list[ast.expr | None] = [] values: list[ast.expr] = [] + # Iterate over the keys and values in the dictionary for k, v in zip(props_node.keys, props_node.values): if isinstance(k, ast.Constant) and isinstance(k.value, str): + # Construct the new key and value k_value, new_v = constructor(k.value, v) if k_value != k.value or new_v is not v: did_change = True @@ -90,20 +113,22 @@ def _rewrite_props( keys.append(k) values.append(v) if not did_change: - return False + return False # Return False if no changes were made props_node.keys = keys props_node.values = values else: did_change = False keywords: list[ast.keyword] = [] + # Iterate over the keywords in the call for kw in props_node.keywords: if kw.arg is not None: + # Construct the new keyword argument and value kw_arg, kw_value = constructor(kw.arg, kw.value) if kw_arg != kw.arg or kw_value is not kw.value: did_change = True kw = ast.keyword(arg=kw_arg, value=kw_value) keywords.append(kw) if not did_change: - return False + return False # Return False if no changes were made props_node.keywords = keywords return True diff --git a/src/reactpy/testing/backend.py b/src/reactpy/testing/backend.py index 9ebd15f3a..1f6521e92 100644 --- a/src/reactpy/testing/backend.py +++ b/src/reactpy/testing/backend.py @@ -16,6 +16,7 @@ from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT from reactpy.core.component import component from reactpy.core.hooks import use_callback, use_effect, use_state +from reactpy.testing.common import GITHUB_ACTIONS from reactpy.testing.logs import ( LogAssertionError, capture_reactpy_logs, @@ -138,7 +139,9 @@ async def __aexit__( msg = "Unexpected logged exception" raise LogAssertionError(msg) from logged_errors[0] - await asyncio.wait_for(self.webserver.shutdown(), timeout=60) + await asyncio.wait_for( + self.webserver.shutdown(), timeout=60 if GITHUB_ACTIONS else 5 + ) async def restart(self) -> None: """Restart the server""" diff --git a/src/reactpy/testing/common.py b/src/reactpy/testing/common.py index de5afaba7..6921bb8da 100644 --- a/src/reactpy/testing/common.py +++ b/src/reactpy/testing/common.py @@ -2,6 +2,7 @@ import asyncio import inspect +import os import shutil import time from collections.abc import Awaitable @@ -28,6 +29,14 @@ 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", +} class poll(Generic[_R]): # noqa: N801 diff --git a/src/reactpy/widgets.py b/src/reactpy/widgets.py index bc559c15d..01532b277 100644 --- a/src/reactpy/widgets.py +++ b/src/reactpy/widgets.py @@ -73,7 +73,7 @@ def sync_inputs(event: dict[str, Any]) -> None: inputs: list[VdomDict] = [] for attrs in attributes: - inputs.append(html.input({**attrs, "on_change": sync_inputs, "value": value})) + inputs.append(html.input({**attrs, "onChange": sync_inputs, "value": value})) return inputs diff --git a/tests/conftest.py b/tests/conftest.py index 119e7571d..2bcd5d3ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,17 +19,10 @@ capture_reactpy_logs, clear_reactpy_web_modules_dir, ) +from reactpy.testing.common import GITHUB_ACTIONS REACTPY_ASYNC_RENDERING.set_current(True) REACTPY_DEBUG.set_current(True) -GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "False") in { - "y", - "yes", - "t", - "true", - "on", - "1", -} def pytest_addoption(parser: Parser) -> None: diff --git a/tests/test_asgi/test_standalone.py b/tests/test_asgi/test_standalone.py index 8c477b21d..c94ee96c1 100644 --- a/tests/test_asgi/test_standalone.py +++ b/tests/test_asgi/test_standalone.py @@ -40,7 +40,7 @@ def Counter(): return reactpy.html.button( { "id": "counter", - "on_click": lambda event: set_count(lambda old_count: old_count + 1), + "onClick": lambda event: set_count(lambda old_count: old_count + 1), }, f"Count: {count}", ) diff --git a/tests/test_client.py b/tests/test_client.py index 7d1da4007..7815dcce8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -18,9 +18,9 @@ async def test_automatic_reconnect( def SomeComponent(): count, incr_count = use_counter(0) return reactpy.html.fragment( - reactpy.html.p({"data_count": count, "id": "count"}, "count", count), + reactpy.html.p({"data-count": count, "id": "count"}, "count", count), reactpy.html.button( - {"on_click": lambda e: incr_count(), "id": "incr"}, "incr" + {"onClick": lambda e: incr_count(), "id": "incr"}, "incr" ), ) @@ -74,8 +74,8 @@ def ButtonWithChangingColor(): return reactpy.html.button( { "id": "my-button", - "on_click": lambda event: set_color_toggle(not color_toggle), - "style": {"background_color": color, "color": "white"}, + "onClick": lambda event: set_color_toggle(not color_toggle), + "style": {"backgroundColor": color, "color": "white"}, }, f"color: {color}", ) @@ -117,7 +117,7 @@ async def handle_change(event): await asyncio.sleep(delay) set_value(event["target"]["value"]) - return reactpy.html.input({"on_change": handle_change, "id": "test-input"}) + return reactpy.html.input({"onChange": handle_change, "id": "test-input"}) await display.show(SomeComponent) @@ -125,27 +125,3 @@ async def handle_change(event): await inp.type("hello", delay=DEFAULT_TYPE_DELAY) assert (await inp.evaluate("node => node.value")) == "hello" - - -async def test_snake_case_attributes(display: DisplayFixture): - @reactpy.component - def SomeComponent(): - return reactpy.html.h1( - { - "id": "my-title", - "style": {"background_color": "blue"}, - "class_name": "hello", - "data_some_thing": "some-data", - "aria_some_thing": "some-aria", - }, - "title with some attributes", - ) - - await display.show(SomeComponent) - - title = await display.page.wait_for_selector("#my-title") - - assert await title.get_attribute("class") == "hello" - assert await title.get_attribute("style") == "background-color: blue;" - assert await title.get_attribute("data-some-thing") == "some-data" - assert await title.get_attribute("aria-some-thing") == "some-aria" diff --git a/tests/test_console/test_rewrite_camel_case_props.py b/tests/test_console/test_rewrite_props.py similarity index 87% rename from tests/test_console/test_rewrite_camel_case_props.py rename to tests/test_console/test_rewrite_props.py index af3a5dd4b..26b88f072 100644 --- a/tests/test_console/test_rewrite_camel_case_props.py +++ b/tests/test_console/test_rewrite_props.py @@ -4,9 +4,9 @@ import pytest from click.testing import CliRunner -from reactpy._console.rewrite_camel_case_props import ( +from reactpy._console.rewrite_props import ( generate_rewrite, - rewrite_camel_case_props, + rewrite_props, ) @@ -14,22 +14,22 @@ def test_rewrite_camel_case_props_declarations(tmp_path): runner = CliRunner() tempfile: Path = tmp_path / "temp.py" - tempfile.write_text("html.div(dict(camelCase='test'))") + tempfile.write_text("html.div(dict(example_attribute='test'))") result = runner.invoke( - rewrite_camel_case_props, + rewrite_props, args=[str(tmp_path)], catch_exceptions=False, ) assert result.exit_code == 0 - assert tempfile.read_text() == "html.div(dict(camel_case='test'))" + assert tempfile.read_text() == "html.div(dict(exampleAttribute='test'))" def test_rewrite_camel_case_props_declarations_no_files(): runner = CliRunner() result = runner.invoke( - rewrite_camel_case_props, + rewrite_props, args=["directory-does-no-exist"], catch_exceptions=False, ) @@ -41,40 +41,40 @@ def test_rewrite_camel_case_props_declarations_no_files(): "source, expected", [ ( - "html.div(dict(camelCase='test'))", "html.div(dict(camel_case='test'))", + "html.div(dict(camelCase='test'))", ), ( - "reactpy.html.button({'onClick': block_forever})", "reactpy.html.button({'on_click': block_forever})", + "reactpy.html.button({'onClick': block_forever})", ), ( - "html.div(dict(style={'testThing': test}))", "html.div(dict(style={'test_thing': test}))", + "html.div(dict(style={'testThing': test}))", ), ( - "html.div(dict(style=dict(testThing=test)))", "html.div(dict(style=dict(test_thing=test)))", + "html.div(dict(style=dict(testThing=test)))", ), ( - "vdom('tag', dict(camelCase='test'))", "vdom('tag', dict(camel_case='test'))", + "vdom('tag', dict(camelCase='test'))", ), ( - "vdom('tag', dict(camelCase='test', **props))", "vdom('tag', dict(camel_case='test', **props))", + "vdom('tag', dict(camelCase='test', **props))", ), ( - "html.div({'camelCase': test, 'data-thing': test})", "html.div({'camel_case': test, 'data-thing': test})", + "html.div({'camelCase': test, 'data-thing': test})", ), ( - "html.div({'camelCase': test, ignore: this})", "html.div({'camel_case': test, ignore: this})", + "html.div({'camelCase': test, ignore: this})", ), # no rewrite ( - "html.div({'snake_case': test})", + "html.div({'camelCase': test})", None, ), ( @@ -82,7 +82,7 @@ def test_rewrite_camel_case_props_declarations_no_files(): None, ), ( - "html.div(dict(snake_case='test'))", + "html.div(dict(camelCase='test'))", None, ), ( diff --git a/tests/test_core/test_events.py b/tests/test_core/test_events.py index b6fea346a..310ddc880 100644 --- a/tests/test_core/test_events.py +++ b/tests/test_core/test_events.py @@ -151,7 +151,7 @@ def Input(): async def on_key_down(value): pass - return reactpy.html.input({"on_key_down": on_key_down, "id": "input"}) + return reactpy.html.input({"onKeyDown": on_key_down, "id": "input"}) await display.show(Input) @@ -171,7 +171,7 @@ async def on_click(event): if not clicked: return reactpy.html.button( - {"on_click": on_click, "id": "click"}, ["Click Me!"] + {"onClick": on_click, "id": "click"}, ["Click Me!"] ) else: return reactpy.html.p({"id": "complete"}, ["Complete"]) @@ -197,8 +197,8 @@ def outer_click_is_not_triggered(event): outer = reactpy.html.div( { - "style": {"height": "35px", "width": "35px", "background_color": "red"}, - "on_click": outer_click_is_not_triggered, + "style": {"height": "35px", "width": "35px", "backgroundColor": "red"}, + "onClick": outer_click_is_not_triggered, "id": "outer", }, reactpy.html.div( @@ -206,9 +206,9 @@ def outer_click_is_not_triggered(event): "style": { "height": "30px", "width": "30px", - "background_color": "blue", + "backgroundColor": "blue", }, - "on_click": inner_click_no_op, + "onClick": inner_click_no_op, "id": "inner", } ), diff --git a/tests/test_core/test_hooks.py b/tests/test_core/test_hooks.py index 8fe5fdab1..30ad878bb 100644 --- a/tests/test_core/test_hooks.py +++ b/tests/test_core/test_hooks.py @@ -186,14 +186,14 @@ def TestComponent(): reactpy.html.button( { "id": "r_1", - "on_click": event_count_tracker(lambda event: set_state(r_1)), + "onClick": event_count_tracker(lambda event: set_state(r_1)), }, "r_1", ), reactpy.html.button( { "id": "r_2", - "on_click": event_count_tracker(lambda event: set_state(r_2)), + "onClick": event_count_tracker(lambda event: set_state(r_2)), }, "r_2", ), @@ -240,7 +240,7 @@ async def on_change(event): set_message(event["target"]["value"]) if message is None: - return reactpy.html.input({"id": "input", "on_change": on_change}) + return reactpy.html.input({"id": "input", "onChange": on_change}) else: return reactpy.html.p({"id": "complete"}, ["Complete"]) @@ -271,7 +271,7 @@ def double_set_state(event): {"id": "second", "data-value": state_2}, f"value is: {state_2}" ), reactpy.html.button( - {"id": "button", "on_click": double_set_state}, "click me" + {"id": "button", "onClick": double_set_state}, "click me" ), ) diff --git a/tests/test_core/test_layout.py b/tests/test_core/test_layout.py index 01472edd2..234b00e9c 100644 --- a/tests/test_core/test_layout.py +++ b/tests/test_core/test_layout.py @@ -510,10 +510,10 @@ def bad_trigger(): children = [ reactpy.html.button( - {"on_click": good_trigger, "id": "good", "key": "good"}, "good" + {"onClick": good_trigger, "id": "good", "key": "good"}, "good" ), reactpy.html.button( - {"on_click": bad_trigger, "id": "bad", "key": "bad"}, "bad" + {"onClick": bad_trigger, "id": "bad", "key": "bad"}, "bad" ), ] @@ -572,7 +572,7 @@ def callback(): msg = "Called bad trigger" raise ValueError(msg) - return reactpy.html.button({"on_click": callback, "id": "good"}, "good") + return reactpy.html.button({"onClick": callback, "id": "good"}, "good") async with reactpy.Layout(RootComponent()) as layout: await layout.render() @@ -654,8 +654,8 @@ def HasEventHandlerAtRoot(): value, set_value = reactpy.hooks.use_state(False) set_value(not value) # trigger renders forever event_handler.current = weakref(set_value) - button = reactpy.html.button({"on_click": set_value}, "state is: ", value) - event_handler.current = weakref(button["eventHandlers"]["on_click"].function) + button = reactpy.html.button({"onClick": set_value}, "state is: ", value) + event_handler.current = weakref(button["eventHandlers"]["onClick"].function) return button async with reactpy.Layout(HasEventHandlerAtRoot()) as layout: @@ -676,8 +676,8 @@ def HasNestedEventHandler(): value, set_value = reactpy.hooks.use_state(False) set_value(not value) # trigger renders forever event_handler.current = weakref(set_value) - button = reactpy.html.button({"on_click": set_value}, "state is: ", value) - event_handler.current = weakref(button["eventHandlers"]["on_click"].function) + button = reactpy.html.button({"onClick": set_value}, "state is: ", value) + event_handler.current = weakref(button["eventHandlers"]["onClick"].function) return reactpy.html.div(reactpy.html.div(button)) async with reactpy.Layout(HasNestedEventHandler()) as layout: @@ -759,7 +759,7 @@ def raise_error(): msg = "bad event handler" raise Exception(msg) - return reactpy.html.button({"on_click": raise_error}) + return reactpy.html.button({"onClick": raise_error}) with assert_reactpy_did_log(match_error="bad event handler"): async with reactpy.Layout(ComponentWithBadEventHandler()) as layout: @@ -857,7 +857,7 @@ def SomeComponent(): [ reactpy.html.div( {"key": i}, - reactpy.html.input({"on_change": lambda event: None}), + reactpy.html.input({"onChange": lambda event: None}), ) for i in items ] @@ -915,14 +915,14 @@ def Root(): toggle, toggle_type.current = use_toggle(True) handler = element_static_handler.use(lambda: None) if toggle: - return html.div(html.button({"on_event": handler})) + return html.div(html.button({"onEvent": handler})) else: return html.div(SomeComponent()) @reactpy.component def SomeComponent(): handler = component_static_handler.use(lambda: None) - return html.button({"on_another_event": handler}) + return html.button({"onAnotherEvent": handler}) async with reactpy.Layout(Root()) as layout: await layout.render() @@ -1005,7 +1005,7 @@ def Parent(): state, set_state = use_state(0) return html.div( html.button( - {"on_click": set_child_key_num.use(lambda: set_state(state + 1))}, + {"onClick": set_child_key_num.use(lambda: set_state(state + 1))}, "click me", ), Child("some-key"), @@ -1217,8 +1217,8 @@ def colorize(event): return html.div( {"id": item, "color": color.value}, - html.button({"on_click": colorize}, f"Color {item}"), - html.button({"on_click": deleteme}, f"Delete {item}"), + html.button({"onClick": colorize}, f"Color {item}"), + html.button({"onClick": deleteme}, f"Delete {item}"), ) @component @@ -1233,7 +1233,7 @@ def App(): b, b_info = find_element(tree, select.id_equals("B")) assert b_info.path == (0, 1, 0) b_delete, _ = find_element(b, select.text_equals("Delete B")) - await runner.trigger(b_delete, "on_click", {}) + await runner.trigger(b_delete, "onClick", {}) tree = await runner.render() @@ -1242,7 +1242,7 @@ def App(): c, c_info = find_element(tree, select.id_equals("C")) assert c_info.path == (0, 1, 0) c_color, _ = find_element(c, select.text_equals("Color C")) - await runner.trigger(c_color, "on_click", {}) + await runner.trigger(c_color, "onClick", {}) tree = await runner.render() diff --git a/tests/test_core/test_serve.py b/tests/test_core/test_serve.py index 8dee3e19e..df92b8091 100644 --- a/tests/test_core/test_serve.py +++ b/tests/test_core/test_serve.py @@ -15,7 +15,7 @@ from tests.tooling.aio import Event from tests.tooling.common import event_message -EVENT_NAME = "on_event" +EVENT_NAME = "onEvent" STATIC_EVENT_HANDLER = StaticEventHandler() @@ -126,8 +126,8 @@ def set_did_render(): did_render.set() return reactpy.html.div( - reactpy.html.button({"on_click": block_forever}), - reactpy.html.button({"on_click": handle_event}), + reactpy.html.button({"onClick": block_forever}), + reactpy.html.button({"onClick": handle_event}), ) send_queue = asyncio.Queue() diff --git a/tests/test_core/test_vdom.py b/tests/test_core/test_vdom.py index 0f3cdafc4..8e349fcc4 100644 --- a/tests/test_core/test_vdom.py +++ b/tests/test_core/test_vdom.py @@ -10,7 +10,7 @@ from reactpy.types import VdomDict FAKE_EVENT_HANDLER = EventHandler(lambda data: None) -FAKE_EVENT_HANDLER_DICT = {"on_event": FAKE_EVENT_HANDLER} +FAKE_EVENT_HANDLER_DICT = {"onEvent": FAKE_EVENT_HANDLER} @pytest.mark.parametrize( @@ -47,7 +47,7 @@ def test_is_vdom(result, value): }, ), ( - reactpy.vdom("div", {"on_event": FAKE_EVENT_HANDLER}), + reactpy.vdom("div", {"onEvent": FAKE_EVENT_HANDLER}), {"tagName": "div", "eventHandlers": FAKE_EVENT_HANDLER_DICT}, ), ( @@ -82,14 +82,14 @@ async def test_callable_attributes_are_cast_to_event_handlers(): params_from_calls = [] node = reactpy.vdom( - "div", {"on_event": lambda *args: params_from_calls.append(args)} + "div", {"onEvent": lambda *args: params_from_calls.append(args)} ) event_handlers = node.pop("eventHandlers") assert node == {"tagName": "div"} - handler = event_handlers["on_event"] - assert event_handlers == {"on_event": EventHandler(handler.function)} + handler = event_handlers["onEvent"] + assert event_handlers == {"onEvent": EventHandler(handler.function)} await handler.function([1, 2]) await handler.function([3, 4, 5]) @@ -217,39 +217,39 @@ def test_valid_vdom(value): r"data\.eventHandlers must be object", ), ( - {"tagName": "tag", "eventHandlers": {"on_event": None}}, - r"data\.eventHandlers\.on_event must be object", + {"tagName": "tag", "eventHandlers": {"onEvent": None}}, + r"data\.eventHandlers\.onEvent must be object", ), ( { "tagName": "tag", - "eventHandlers": {"on_event": {}}, + "eventHandlers": {"onEvent": {}}, }, - r"data\.eventHandlers\.on_event\ must contain \['target'\] properties", + r"data\.eventHandlers\.onEvent\ must contain \['target'\] properties", ), ( { "tagName": "tag", "eventHandlers": { - "on_event": { + "onEvent": { "target": "something", "preventDefault": None, } }, }, - r"data\.eventHandlers\.on_event\.preventDefault must be boolean", + r"data\.eventHandlers\.onEvent\.preventDefault must be boolean", ), ( { "tagName": "tag", "eventHandlers": { - "on_event": { + "onEvent": { "target": "something", "stopPropagation": None, } }, }, - r"data\.eventHandlers\.on_event\.stopPropagation must be boolean", + r"data\.eventHandlers\.onEvent\.stopPropagation must be boolean", ), ( {"tagName": "tag", "importSource": None}, @@ -312,4 +312,4 @@ 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"): - reactpy.html.div({"non_json_serializable_object": object()}) + reactpy.html.div({"nonJsonSerializableObject": object()}) diff --git a/tests/test_html.py b/tests/test_html.py index 68e353681..fe046c49e 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -17,7 +17,7 @@ def on_click(event): set_count(count + 1) return html.div( - html.div({"id": "mount-count", "data_value": 0}), + html.div({"id": "mount-count", "dataValue": 0}), html.script( f'document.getElementById("mount-count").setAttribute("data-value", {count});' ), @@ -57,7 +57,7 @@ def HasScript(): return html.div() else: return html.div( - html.div({"id": "run-count", "data_value": 0}), + html.div({"id": "run-count", "dataValue": 0}), html.script( { "src": f"/reactpy/modules/{file_name_template.format(src_id=src_id)}" @@ -98,7 +98,7 @@ def test_child_of_script_must_be_string(): def test_script_has_no_event_handlers(): with pytest.raises(ValueError, match="do not support event handlers"): - html.script({"on_event": lambda: None}) + html.script({"onEvent": lambda: None}) def test_simple_fragment(): @@ -114,7 +114,7 @@ def test_simple_fragment(): def test_fragment_can_have_no_attributes(): with pytest.raises(TypeError, match="Fragments cannot have attributes"): - html.fragment({"some_attribute": 1}) + html.fragment({"someAttribute": 1}) async def test_svg(display: DisplayFixture): diff --git a/tests/test_testing.py b/tests/test_testing.py index e2c227d61..ad7a9af48 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -193,7 +193,7 @@ async def on_click(event): mount(hotswap_3) return html.div( - html.button({"on_click": on_click, "id": "incr-button"}, "incr"), + html.button({"onClick": on_click, "id": "incr-button"}, "incr"), hostswap(), ) diff --git a/tests/test_utils.py b/tests/test_utils.py index ef67766e5..c79a9d1f3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -226,7 +226,7 @@ def example_child(): '
helloexampleworld
', ), ( - html.button({"on_click": lambda event: None}), + html.button({"onClick": lambda event: None}), "", ), ( @@ -264,7 +264,7 @@ def example_child(): ), ( html.div( - {"data_something": 1, "data_something_else": 2, "dataisnotdashed": 3} + {"data_Something": 1, "dataSomethingElse": 2, "dataisnotdashed": 3} ), '
', ), diff --git a/tests/tooling/common.py b/tests/tooling/common.py index c850d714b..75495db0c 100644 --- a/tests/tooling/common.py +++ b/tests/tooling/common.py @@ -1,12 +1,9 @@ -import os from typing import Any +from reactpy.testing.common import GITHUB_ACTIONS from reactpy.types import LayoutEventMessage, LayoutUpdateMessage -GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "False") -DEFAULT_TYPE_DELAY = ( - 250 if GITHUB_ACTIONS.lower() in {"y", "yes", "t", "true", "on", "1"} else 50 -) +DEFAULT_TYPE_DELAY = 250 if GITHUB_ACTIONS else 50 def event_message(target: str, *data: Any) -> LayoutEventMessage: