diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b679d4f6f..673bf9b32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: session-name: test_python_suite session-arguments: --maxfail=3 --no-cov runs-on-array: '["ubuntu-latest", "macos-latest", "windows-latest"]' - python-version-array: '["3.7", "3.8", "3.9", "3.10"]' + python-version-array: '["3.7", "3.8", "3.9", "3.10", "3.11"]' docs: uses: ./.github/workflows/.nox-session.yml with: diff --git a/docs/examples.py b/docs/examples.py index 7b9a5160a..5b273503e 100644 --- a/docs/examples.py +++ b/docs/examples.py @@ -122,7 +122,7 @@ def Wrapper(): def PrintView(): text, set_text = idom.hooks.use_state(print_buffer.getvalue()) print_buffer.set_callback(set_text) - return idom.html.pre({"class": "printout"}, text) if text else idom.html.div() + return idom.html.pre(text, class_name="printout") if text else idom.html.div() return Wrapper() diff --git a/docs/source/_custom_js/package-lock.json b/docs/source/_custom_js/package-lock.json index ed1027145..ee2c660bf 100644 --- a/docs/source/_custom_js/package-lock.json +++ b/docs/source/_custom_js/package-lock.json @@ -19,12 +19,12 @@ } }, "../../../src/client/packages/idom-client-react": { - "version": "0.42.0", + "version": "0.43.0", "integrity": "sha512-pIK5eNwFSHKXg7ClpASWFVKyZDYxz59MSFpVaX/OqJFkrJaAxBuhKGXNTMXmuyWOL5Iyvb/ErwwDRxQRzMNkfQ==", "license": "MIT", "dependencies": { - "fast-json-patch": "^3.0.0-1", - "htm": "^3.0.3" + "htm": "^3.0.3", + "json-pointer": "^0.6.2" }, "devDependencies": { "jsdom": "16.5.0", @@ -37,11 +37,6 @@ "react-dom": ">=16" } }, - "../../../src/client/packages/idom-client-react/node_modules/fast-json-patch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.0.tgz", - "integrity": "sha512-IhpytlsVTRndz0hU5t0/MGzS/etxLlfrpG5V5M9mVbuj9TrJLWaMfsox9REM5rkuGX0T+5qjpe8XA1o0gZ42nA==" - }, "../../../src/client/packages/idom-client-react/node_modules/htm": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", @@ -602,19 +597,14 @@ "idom-client-react": { "version": "file:../../../src/client/packages/idom-client-react", "requires": { - "fast-json-patch": "^3.0.0-1", "htm": "^3.0.3", "jsdom": "16.5.0", + "json-pointer": "^0.6.2", "lodash": "^4.17.21", "prettier": "^2.5.1", "uvu": "^0.5.1" }, "dependencies": { - "fast-json-patch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.0.tgz", - "integrity": "sha512-IhpytlsVTRndz0hU5t0/MGzS/etxLlfrpG5V5M9mVbuj9TrJLWaMfsox9REM5rkuGX0T+5qjpe8XA1o0gZ42nA==" - }, "htm": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", diff --git a/docs/source/_exts/custom_autosectionlabel.py b/docs/source/_exts/custom_autosectionlabel.py index 573bc35dd..7bb659230 100644 --- a/docs/source/_exts/custom_autosectionlabel.py +++ b/docs/source/_exts/custom_autosectionlabel.py @@ -5,7 +5,7 @@ """ from fnmatch import fnmatch -from typing import Any, Dict, cast +from typing import Any, cast from docutils import nodes from docutils.nodes import Node @@ -30,7 +30,6 @@ def get_node_depth(node: Node) -> int: def register_sections_as_label(app: Sphinx, document: Node) -> None: docname = app.env.docname - print(docname) for pattern in app.config.autosectionlabel_skip_docs: if fnmatch(docname, pattern): @@ -67,7 +66,7 @@ def register_sections_as_label(app: Sphinx, document: Node) -> None: domain.labels[name] = docname, labelid, sectname -def setup(app: Sphinx) -> Dict[str, Any]: +def setup(app: Sphinx) -> dict[str, Any]: app.add_config_value("autosectionlabel_prefix_document", False, "env") app.add_config_value("autosectionlabel_maxdepth", None, "env") app.add_config_value("autosectionlabel_skip_docs", [], "env") diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/main.py b/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/main.py index 724831f89..9c1c301e8 100644 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/main.py +++ b/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/main.py @@ -25,10 +25,10 @@ def handle_click(event): url = sculpture["url"] return html.div( - html.button({"onClick": handle_click}, "Next"), + html.button("Next", on_click=handle_click), html.h2(name, " by ", artist), html.p(f"({bounded_index + 1} of {len(sculpture_data)})"), - html.img({"src": url, "alt": alt, "style": {"height": "200px"}}), + html.img(src=url, alt=alt, style={"height": "200px"}), html.p(description), ) diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/main.py b/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/main.py index 08a53d1c6..e235856b9 100644 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/main.py +++ b/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/main.py @@ -29,14 +29,14 @@ def handle_more_click(event): url = sculpture["url"] return html.div( - html.button({"onClick": handle_next_click}, "Next"), + html.button("Next", on_click=handle_next_click), html.h2(name, " by ", artist), html.p(f"({bounded_index + 1} or {len(sculpture_data)})"), - html.img({"src": url, "alt": alt, "style": {"height": "200px"}}), + html.img(src=url, alt=alt, style={"height": "200px"}), html.div( html.button( - {"onClick": handle_more_click}, - f"{'Show' if show_more else 'Hide'} details", + f"{('Show' if show_more else 'Hide')} details", + on_click=handle_more_click, ), (html.p(description) if show_more else ""), ), @@ -46,8 +46,8 @@ def handle_more_click(event): @component def App(): return html.div( - html.section({"style": {"width": "50%", "float": "left"}}, Gallery()), - html.section({"style": {"width": "50%", "float": "left"}}, Gallery()), + html.section(Gallery(), style={"width": "50%", "float": "left"}), + html.section(Gallery(), style={"width": "50%", "float": "left"}), ) diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/main.py b/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/main.py index 3e7f7bde4..e9a9b5648 100644 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/main.py +++ b/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/main.py @@ -29,14 +29,14 @@ def handle_more_click(event): url = sculpture["url"] return html.div( - html.button({"onClick": handle_next_click}, "Next"), + html.button("Next", on_click=handle_next_click), html.h2(name, " by ", artist), html.p(f"({bounded_index + 1} or {len(sculpture_data)})"), - html.img({"src": url, "alt": alt, "style": {"height": "200px"}}), + html.img(src=url, alt=alt, style={"height": "200px"}), html.div( html.button( - {"onClick": handle_more_click}, - f"{'Show' if show_more else 'Hide'} details", + f"{('Show' if show_more else 'Hide')} details", + on_click=handle_more_click, ), (html.p(description) if show_more else ""), ), diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/main.py b/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/main.py index f8679cbfc..5647418b2 100644 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/main.py +++ b/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/main.py @@ -31,10 +31,10 @@ def handle_click(event): url = sculpture["url"] return html.div( - html.button({"onClick": handle_click}, "Next"), + html.button("Next", on_click=handle_click), html.h2(name, " by ", artist), html.p(f"({bounded_index + 1} or {len(sculpture_data)})"), - html.img({"src": url, "alt": alt, "style": {"height": "200px"}}), + html.img(src=url, alt=alt, style={"height": "200px"}), html.p(description), ) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_remove.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_remove.py index cf1955301..03444e250 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_remove.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_remove.py @@ -26,26 +26,21 @@ def handle_click(event): return handle_click return html.div( - html.button({"onClick": handle_add_click}, "add term"), + html.button("add term", on_click=handle_add_click), html.label( "Term: ", - html.input({"value": term_to_add, "onChange": handle_term_to_add_change}), + html.input(value=term_to_add, on_change=handle_term_to_add_change), ), html.label( "Definition: ", html.input( - { - "value": definition_to_add, - "onChange": handle_definition_to_add_change, - } + value=definition_to_add, on_change=handle_definition_to_add_change ), ), html.hr(), [ html.div( - html.button( - {"onClick": make_delete_click_handler(term)}, "delete term" - ), + html.button("delete term", on_click=make_delete_click_handler(term)), html.dt(term), html.dd(definition), key=term, diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_update.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_update.py index 92085c0b6..d9dd35dff 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_update.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_update.py @@ -23,21 +23,15 @@ def handle_email_change(event): return html.div( html.label( "First name: ", - html.input( - {"value": person["first_name"], "onChange": handle_first_name_change}, - ), + html.input(value=person["first_name"], on_change=handle_first_name_change), ), html.label( "Last name: ", - html.input( - {"value": person["last_name"], "onChange": handle_last_name_change}, - ), + html.input(value=person["last_name"], on_change=handle_last_name_change), ), html.label( "Email: ", - html.input( - {"value": person["email"], "onChange": handle_email_change}, - ), + html.input(value=person["email"], on_change=handle_email_change), ), html.p(f"{person['first_name']} {person['last_name']} {person['email']}"), ) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_insert.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_insert.py index 374198f45..35cf2ce5d 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_insert.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_insert.py @@ -16,8 +16,8 @@ def handle_click(event): return html.div( html.h1("Inspiring sculptors:"), - html.input({"value": artist_to_add, "onChange": handle_change}), - html.button({"onClick": handle_click}, "add"), + html.input(value=artist_to_add, on_change=handle_change), + html.button("add", on_click=handle_click), html.ul([html.li(name, key=name) for name in artists]), ) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_re_order.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_re_order.py index 32a0e47c0..e8ab5f7e1 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_re_order.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_re_order.py @@ -15,8 +15,8 @@ def handle_reverse_click(event): return html.div( html.h1("Inspiring sculptors:"), - html.button({"onClick": handle_sort_click}, "sort"), - html.button({"onClick": handle_reverse_click}, "reverse"), + html.button("sort", on_click=handle_sort_click), + html.button("reverse", on_click=handle_reverse_click), html.ul([html.li(name, key=name) for name in artists]), ) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_remove.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_remove.py index 170deb2f4..f37c2ff9f 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_remove.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_remove.py @@ -24,13 +24,13 @@ def handle_click(event): return html.div( html.h1("Inspiring sculptors:"), - html.input({"value": artist_to_add, "onChange": handle_change}), - html.button({"onClick": handle_add_click}, "add"), + html.input(value=artist_to_add, on_change=handle_change), + html.button("add", on_click=handle_add_click), html.ul( [ html.li( name, - html.button({"onClick": make_handle_delete_click(index)}, "delete"), + html.button("delete", on_click=make_handle_delete_click(index)), key=name, ) for index, name in enumerate(artists) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_replace.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_replace.py index 2fafcfde8..7cfcd0bf3 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_replace.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_replace.py @@ -16,7 +16,7 @@ def handle_click(event): [ html.li( count, - html.button({"onClick": make_increment_click_handler(index)}, "+1"), + html.button("+1", on_click=make_increment_click_handler(index)), key=index, ) for index, count in enumerate(counters) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot.py index d3edb8590..58e0b386f 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot.py @@ -16,29 +16,25 @@ async def handle_pointer_move(event): ) return html.div( - { - "onPointerMove": handle_pointer_move, - "style": { - "position": "relative", - "height": "200px", - "width": "100%", - "backgroundColor": "white", - }, - }, html.div( - { - "style": { - "position": "absolute", - "backgroundColor": "red", - "borderRadius": "50%", - "width": "20px", - "height": "20px", - "left": "-10px", - "top": "-10px", - "transform": f"translate({position['x']}px, {position['y']}px)", - }, + style={ + "position": "absolute", + "background_color": "red", + "border_radius": "50%", + "width": "20px", + "height": "20px", + "left": "-10px", + "top": "-10px", + "transform": f"translate({position['x']}px, {position['y']}px)", } ), + on_pointer_move=handle_pointer_move, + style={ + "position": "relative", + "height": "200px", + "width": "100%", + "background_color": "white", + }, ) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot_broken.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot_broken.py index 90885e7fe..3f8f738db 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot_broken.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot_broken.py @@ -14,29 +14,25 @@ def handle_pointer_move(event): position["y"] = event["clientY"] - outer_div_bounds["y"] return html.div( - { - "onPointerMove": handle_pointer_move, - "style": { - "position": "relative", - "height": "200px", - "width": "100%", - "backgroundColor": "white", - }, - }, html.div( - { - "style": { - "position": "absolute", - "backgroundColor": "red", - "borderRadius": "50%", - "width": "20px", - "height": "20px", - "left": "-10px", - "top": "-10px", - "transform": f"translate({position['x']}px, {position['y']}px)", - }, + style={ + "position": "absolute", + "background_color": "red", + "border_radius": "50%", + "width": "20px", + "height": "20px", + "left": "-10px", + "top": "-10px", + "transform": f"translate({position['x']}px, {position['y']}px)", } ), + on_pointer_move=handle_pointer_move, + style={ + "position": "relative", + "height": "200px", + "width": "100%", + "background_color": "white", + }, ) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_remove.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_remove.py index 4c4905350..b2d830909 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_remove.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_remove.py @@ -16,25 +16,23 @@ def handle_click(event): return handle_click return html.div( - {"style": {"display": "flex", "flex-direction": "row"}}, [ html.div( - { - "onClick": make_handle_click(index), - "style": { - "height": "30px", - "width": "30px", - "backgroundColor": ( - "black" if index in selected_indices else "white" - ), - "outline": "1px solid grey", - "cursor": "pointer", - }, - }, key=index, + on_click=make_handle_click(index), + style={ + "height": "30px", + "width": "30px", + "background_color": "black" + if index in selected_indices + else "white", + "outline": "1px solid grey", + "cursor": "pointer", + }, ) for index in range(line_size) ], + style={"display": "flex", "flex-direction": "row"}, ) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_update.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_update.py index 9e1077cb0..37bd7a591 100644 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_update.py +++ b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_update.py @@ -13,25 +13,23 @@ def handle_click(event): return handle_click return html.div( - {"style": {"display": "flex", "flex-direction": "row"}}, [ html.div( - { - "onClick": make_handle_click(index), - "style": { - "height": "30px", - "width": "30px", - "backgroundColor": ( - "black" if index in selected_indices else "white" - ), - "outline": "1px solid grey", - "cursor": "pointer", - }, - }, key=index, + on_click=make_handle_click(index), + style={ + "height": "30px", + "width": "30px", + "background_color": "black" + if index in selected_indices + else "white", + "outline": "1px solid grey", + "cursor": "pointer", + }, ) for index in range(line_size) ], + style={"display": "flex", "flex-direction": "row"}, ) diff --git a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_count_updater.py b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_count_updater.py index 0c000477e..6b24f0110 100644 --- a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_count_updater.py +++ b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_count_updater.py @@ -13,7 +13,7 @@ async def handle_click(event): return html.div( html.h1(number), - html.button({"onClick": handle_click}, "Increment"), + html.button("Increment", on_click=handle_click), ) diff --git a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_set_count.py b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_set_count.py index 024df12e7..062e57e19 100644 --- a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_set_count.py +++ b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_set_count.py @@ -13,7 +13,7 @@ async def handle_click(event): return html.div( html.h1(number), - html.button({"onClick": handle_click}, "Increment"), + html.button("Increment", on_click=handle_click), ) diff --git a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_color_3_times.py b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_color_3_times.py index e755c35b9..313035a6e 100644 --- a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_color_3_times.py +++ b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_color_3_times.py @@ -15,11 +15,9 @@ def handle_reset(event): return html.div( html.button( - {"onClick": handle_click, "style": {"backgroundColor": color}}, "Set Color" - ), - html.button( - {"onClick": handle_reset, "style": {"backgroundColor": color}}, "Reset" + "Set Color", on_click=handle_click, style={"background_color": color} ), + html.button("Reset", on_click=handle_reset, style={"background_color": color}), ) diff --git a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_state_function.py b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_state_function.py index ec3193de9..36cb5b395 100644 --- a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_state_function.py +++ b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_state_function.py @@ -17,7 +17,7 @@ def handle_click(event): return html.div( html.h1(number), - html.button({"onClick": handle_click}, "Increment"), + html.button("Increment", on_click=handle_click), ) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/audio_player.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/audio_player.py index 582588a8c..e6e3ab543 100644 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/audio_player.py +++ b/docs/source/guides/adding-interactivity/responding-to-events/_examples/audio_player.py @@ -8,11 +8,9 @@ def PlayDinosaurSound(): event, set_event = idom.hooks.use_state(None) return idom.html.div( idom.html.audio( - { - "controls": True, - "onTimeUpdate": lambda e: set_event(e), - "src": "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", - } + controls=True, + on_time_update=lambda e: set_event(e), + src="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", ), idom.html.pre(json.dumps(event, indent=2)), ) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_async_handlers.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_async_handlers.py index a355f6142..a11b3a40c 100644 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_async_handlers.py +++ b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_async_handlers.py @@ -9,7 +9,7 @@ async def handle_event(event): await asyncio.sleep(delay) print(message) - return html.button({"onClick": handle_event}, message) + return html.button(message, on_click=handle_event) @component diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_handler_as_arg.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_handler_as_arg.py index 4de22a024..4a576123b 100644 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_handler_as_arg.py +++ b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_handler_as_arg.py @@ -3,7 +3,7 @@ @component def Button(display_text, on_click): - return html.button({"onClick": on_click}, display_text) + return html.button(display_text, on_click=on_click) @component diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_event.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_event.py index eac05a588..bb4ac6b73 100644 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_event.py +++ b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_event.py @@ -6,7 +6,7 @@ def Button(): def handle_event(event): print(event) - return html.button({"onClick": handle_event}, "Click me!") + return html.button("Click me!", on_click=handle_event) run(Button) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_message.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_message.py index f5ee69f80..4be2fc1d4 100644 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_message.py +++ b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_message.py @@ -6,7 +6,7 @@ def PrintButton(display_text, message_text): def handle_event(event): print(message_text) - return html.button({"onClick": handle_event}, display_text) + return html.button(display_text, on_click=handle_event) @component diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/prevent_default_event_actions.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/prevent_default_event_actions.py index 7e8ef9938..cd2870a46 100644 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/prevent_default_event_actions.py +++ b/docs/source/guides/adding-interactivity/responding-to-events/_examples/prevent_default_event_actions.py @@ -6,11 +6,9 @@ def DoNotChangePages(): return html.div( html.p("Normally clicking this link would take you to a new page"), html.a( - { - "onClick": event(lambda event: None, prevent_default=True), - "href": "https://google.com", - }, "https://google.com", + on_click=event(lambda event: None, prevent_default=True), + href="https://google.com", ), ) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/stop_event_propagation.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/stop_event_propagation.py index e87bae026..4a2079d43 100644 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/stop_event_propagation.py +++ b/docs/source/guides/adding-interactivity/responding-to-events/_examples/stop_event_propagation.py @@ -8,25 +8,21 @@ def DivInDiv(): outer_count, set_outer_count = hooks.use_state(0) div_in_div = html.div( - { - "onClick": lambda event: set_outer_count(outer_count + 1), - "style": {"height": "100px", "width": "100px", "backgroundColor": "red"}, - }, html.div( - { - "onClick": event( - lambda event: set_inner_count(inner_count + 1), - stop_propagation=stop_propagatation, - ), - "style": {"height": "50px", "width": "50px", "backgroundColor": "blue"}, - }, + on_click=event( + lambda event: set_inner_count(inner_count + 1), + stop_propagation=stop_propagatation, + ), + style={"height": "50px", "width": "50px", "background_color": "blue"}, ), + on_click=lambda event: set_outer_count(outer_count + 1), + style={"height": "100px", "width": "100px", "background_color": "red"}, ) return html.div( html.button( - {"onClick": lambda event: set_stop_propagatation(not stop_propagatation)}, "Toggle Propogation", + on_click=lambda event: set_stop_propagatation(not stop_propagatation), ), html.pre(f"Will propagate: {not stop_propagatation}"), html.pre(f"Inner click count: {inner_count}"), diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/delayed_print_after_set.py b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/delayed_print_after_set.py index 5471616d4..99c3aab80 100644 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/delayed_print_after_set.py +++ b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/delayed_print_after_set.py @@ -15,7 +15,7 @@ async def handle_click(event): return html.div( html.h1(number), - html.button({"onClick": handle_click}, "Increment"), + html.button("Increment", on_click=handle_click), ) diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_chat_message.py b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_chat_message.py index 35fbc23fb..2e0df7abf 100644 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_chat_message.py +++ b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_chat_message.py @@ -16,27 +16,24 @@ async def handle_submit(event): print(f"Sent '{message}' to {recipient}") return html.form( - {"onSubmit": handle_submit, "style": {"display": "inline-grid"}}, html.label( "To: ", html.select( - { - "value": recipient, - "onChange": lambda event: set_recipient(event["target"]["value"]), - }, - html.option({"value": "Alice"}, "Alice"), - html.option({"value": "Bob"}, "Bob"), + html.option("Alice", value="Alice"), + html.option("Bob", value="Bob"), + value=recipient, + on_change=lambda event: set_recipient(event["target"]["value"]), ), ), html.input( - { - "type": "text", - "placeholder": "Your message...", - "value": message, - "onChange": lambda event: set_message(event["target"]["value"]), - } + type="text", + placeholder="Your message...", + value=message, + on_change=lambda event: set_message(event["target"]["value"]), ), - html.button({"type": "submit"}, "Send"), + html.button("Send", type="submit"), + on_submit=handle_submit, + style={"display": "inline-grid"}, ) diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_count_after_set.py b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_count_after_set.py index 039a261d9..d3f09253e 100644 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_count_after_set.py +++ b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_count_after_set.py @@ -11,7 +11,7 @@ def handle_click(event): return html.div( html.h1(number), - html.button({"onClick": handle_click}, "Increment"), + html.button("Increment", on_click=handle_click), ) diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/send_message.py b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/send_message.py index 0ceaf8850..fa1bfa7d8 100644 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/send_message.py +++ b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/send_message.py @@ -9,9 +9,7 @@ def App(): if is_sent: return html.div( html.h1("Message sent!"), - html.button( - {"onClick": lambda event: set_is_sent(False)}, "Send new message?" - ), + html.button("Send new message?", on_click=lambda event: set_is_sent(False)), ) @event(prevent_default=True) @@ -20,15 +18,14 @@ def handle_submit(event): set_is_sent(True) return html.form( - {"onSubmit": handle_submit, "style": {"display": "inline-grid"}}, html.textarea( - { - "placeholder": "Your message here...", - "value": message, - "onChange": lambda event: set_message(event["target"]["value"]), - } + placeholder="Your message here...", + value=message, + on_change=lambda event: set_message(event["target"]["value"]), ), - html.button({"type": "submit"}, "Send"), + html.button("Send", type="submit"), + on_submit=handle_submit, + style={"display": "inline-grid"}, ) diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/set_counter_3_times.py b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/set_counter_3_times.py index 24801d47b..0a8318231 100644 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/set_counter_3_times.py +++ b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/set_counter_3_times.py @@ -12,7 +12,7 @@ def handle_click(event): return html.div( html.h1(number), - html.button({"onClick": handle_click}, "Increment"), + html.button("Increment", on_click=handle_click), ) diff --git a/docs/source/guides/creating-interfaces/html-with-idom/index.rst b/docs/source/guides/creating-interfaces/html-with-idom/index.rst index 52ca1e617..0afb4116d 100644 --- a/docs/source/guides/creating-interfaces/html-with-idom/index.rst +++ b/docs/source/guides/creating-interfaces/html-with-idom/index.rst @@ -90,11 +90,9 @@ Additionally, instead of specifying ``style`` using a string, we use a dictionar .. testcode:: html.img( - { - "src": "https://picsum.photos/id/237/500/300", - "style": {"width": "50%", "marginLeft": "25%"}, - "alt": "Billie Holiday", - } + src="https://picsum.photos/id/237/500/300", + style={"width": "50%", "marginLeft": "25%"}, + alt="Billie Holiday", ) .. raw:: html diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/nested_photos.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/nested_photos.py index 4c512b7e6..59796e49a 100644 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/nested_photos.py +++ b/docs/source/guides/creating-interfaces/your-first-components/_examples/nested_photos.py @@ -4,11 +4,9 @@ @component def Photo(): return html.img( - { - "src": "https://picsum.photos/id/274/500/300", - "style": {"width": "30%"}, - "alt": "Ray Charles", - } + src="https://picsum.photos/id/274/500/300", + style={"width": "30%"}, + alt="Ray Charles", ) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/parametrized_photos.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/parametrized_photos.py index 7eacb8f36..2d5768d58 100644 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/parametrized_photos.py +++ b/docs/source/guides/creating-interfaces/your-first-components/_examples/parametrized_photos.py @@ -4,11 +4,9 @@ @component def Photo(alt_text, image_id): return html.img( - { - "src": f"https://picsum.photos/id/{image_id}/500/200", - "style": {"width": "50%"}, - "alt": alt_text, - } + src=f"https://picsum.photos/id/{image_id}/500/200", + style={"width": "50%"}, + alt=alt_text, ) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/simple_photo.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/simple_photo.py index c6b92c652..f5ccc5027 100644 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/simple_photo.py +++ b/docs/source/guides/creating-interfaces/your-first-components/_examples/simple_photo.py @@ -4,11 +4,7 @@ @component def Photo(): return html.img( - { - "src": "https://picsum.photos/id/237/500/300", - "style": {"width": "50%"}, - "alt": "Puppy", - } + src="https://picsum.photos/id/237/500/300", style={"width": "50%"}, alt="Puppy" ) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_div.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_div.py index 2ddcd1060..f0ba25e84 100644 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_div.py +++ b/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_div.py @@ -5,7 +5,7 @@ def MyTodoList(): return html.div( html.h1("My Todo List"), - html.img({"src": "https://picsum.photos/id/0/500/300"}), + html.img(src="https://picsum.photos/id/0/500/300"), html.ul(html.li("The first thing I need to do is...")), ) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_fragment.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_fragment.py index 027e253bf..85b9b3ceb 100644 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_fragment.py +++ b/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_fragment.py @@ -5,7 +5,7 @@ def MyTodoList(): return html._( html.h1("My Todo List"), - html.img({"src": "https://picsum.photos/id/0/500/200"}), + html.img(src="https://picsum.photos/id/0/500/200"), html.ul(html.li("The first thing I need to do is...")), ) diff --git a/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py b/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py index 9b0658371..619a35cff 100644 --- a/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py +++ b/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py @@ -21,7 +21,7 @@ def handle_change(event): set_value(event["target"]["value"]) return html.label( - "Search by Food Name: ", html.input({"value": value, "onChange": handle_change}) + "Search by Food Name: ", html.input(value=value, on_change=handle_change) ) diff --git a/docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py b/docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py index dcc3e1246..64bcb1aa7 100644 --- a/docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py +++ b/docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py @@ -15,9 +15,7 @@ def Input(label, value, set_value): def handle_change(event): set_value(event["target"]["value"]) - return html.label( - label + " ", html.input({"value": value, "onChange": handle_change}) - ) + return html.label(label + " ", html.input(value=value, on_change=handle_change)) run(SyncedInputs) diff --git a/docs/source/reference/_examples/character_movement/main.py b/docs/source/reference/_examples/character_movement/main.py index fbf257a32..24b47580f 100644 --- a/docs/source/reference/_examples/character_movement/main.py +++ b/docs/source/reference/_examples/character_movement/main.py @@ -36,15 +36,7 @@ def Scene(): position, set_position = use_state(Position(100, 100, 0)) return html.div( - {"style": {"width": "225px"}}, html.div( - { - "style": { - "width": "200px", - "height": "200px", - "backgroundColor": "slategray", - } - }, image( "png", CHARACTER_IMAGE, @@ -57,13 +49,15 @@ def Scene(): } }, ), + style={"width": "200px", "height": "200px", "backgroundColor": "slategray"}, ), - html.button({"onClick": lambda e: set_position(translate(x=-10))}, "Move Left"), - html.button({"onClick": lambda e: set_position(translate(x=10))}, "Move Right"), - html.button({"onClick": lambda e: set_position(translate(y=-10))}, "Move Up"), - html.button({"onClick": lambda e: set_position(translate(y=10))}, "Move Down"), - html.button({"onClick": lambda e: set_position(rotate(-30))}, "Rotate Left"), - html.button({"onClick": lambda e: set_position(rotate(30))}, "Rotate Right"), + html.button("Move Left", on_click=lambda e: set_position(translate(x=-10))), + html.button("Move Right", on_click=lambda e: set_position(translate(x=10))), + html.button("Move Up", on_click=lambda e: set_position(translate(y=-10))), + html.button("Move Down", on_click=lambda e: set_position(translate(y=10))), + html.button("Rotate Left", on_click=lambda e: set_position(rotate(-30))), + html.button("Rotate Right", on_click=lambda e: set_position(rotate(30))), + style={"width": "225px"}, ) diff --git a/docs/source/reference/_examples/click_count.py b/docs/source/reference/_examples/click_count.py index 6f30ce517..491fca839 100644 --- a/docs/source/reference/_examples/click_count.py +++ b/docs/source/reference/_examples/click_count.py @@ -6,8 +6,7 @@ def ClickCount(): count, set_count = idom.hooks.use_state(0) return idom.html.button( - {"onClick": lambda event: set_count(count + 1)}, - [f"Click count: {count}"], + [f"Click count: {count}"], on_click=lambda event: set_count(count + 1) ) diff --git a/docs/source/reference/_examples/matplotlib_plot.py b/docs/source/reference/_examples/matplotlib_plot.py index 6dffb79db..e219e4e96 100644 --- a/docs/source/reference/_examples/matplotlib_plot.py +++ b/docs/source/reference/_examples/matplotlib_plot.py @@ -39,8 +39,8 @@ def del_input(): return idom.html.div( idom.html.div( "add/remove term:", - idom.html.button({"onClick": lambda event: add_input()}, "+"), - idom.html.button({"onClick": lambda event: del_input()}, "-"), + idom.html.button("+", on_click=lambda event: add_input()), + idom.html.button("-", on_click=lambda event: del_input()), ), inputs, ) @@ -58,20 +58,10 @@ def plot(title, x, y): def poly_coef_input(index, callback): return idom.html.div( - {"style": {"margin-top": "5px"}}, - idom.html.label( - "C", - idom.html.sub(index), - " × X", - idom.html.sup(index), - ), - idom.html.input( - { - "type": "number", - "onChange": callback, - }, - ), + idom.html.label("C", idom.html.sub(index), " × X", idom.html.sup(index)), + idom.html.input(type="number", on_change=callback), key=index, + style={"margin_top": "5px"}, ) diff --git a/docs/source/reference/_examples/simple_dashboard.py b/docs/source/reference/_examples/simple_dashboard.py index 540082f58..4f7ce2a42 100644 --- a/docs/source/reference/_examples/simple_dashboard.py +++ b/docs/source/reference/_examples/simple_dashboard.py @@ -83,10 +83,10 @@ def update_value(value): set_value_callback(value) return idom.html.fieldset( - {"class": "number-input-container"}, - idom.html.legend({"style": {"font-size": "medium"}}, label), + idom.html.legend(label, style={"font_size": "medium"}), Input(update_value, "number", value, attributes=attrs, cast=float), Input(update_value, "range", value, attributes=attrs, cast=float), + class_name="number-input-container", ) diff --git a/docs/source/reference/_examples/slideshow.py b/docs/source/reference/_examples/slideshow.py index 0d3116ac4..49f732aed 100644 --- a/docs/source/reference/_examples/slideshow.py +++ b/docs/source/reference/_examples/slideshow.py @@ -9,11 +9,9 @@ def next_image(event): set_index(index + 1) return idom.html.img( - { - "src": f"https://picsum.photos/id/{index}/800/300", - "style": {"cursor": "pointer"}, - "onClick": next_image, - } + src=f"https://picsum.photos/id/{index}/800/300", + style={"cursor": "pointer"}, + on_click=next_image, ) diff --git a/docs/source/reference/_examples/snake_game.py b/docs/source/reference/_examples/snake_game.py index 92fe054f0..cf0328ef4 100644 --- a/docs/source/reference/_examples/snake_game.py +++ b/docs/source/reference/_examples/snake_game.py @@ -21,8 +21,7 @@ def GameView(): return GameLoop(grid_size=6, block_scale=50, set_game_state=set_game_state) start_button = idom.html.button( - {"onClick": lambda event: set_game_state(GameState.play)}, - "Start", + "Start", on_click=lambda event: set_game_state(GameState.play) ) if game_state == GameState.won: @@ -40,7 +39,7 @@ def GameView(): """ ) - return idom.html.div({"className": "snake-game-menu"}, menu_style, menu) + return idom.html.div(menu_style, menu, class_name="snake-game-menu") class Direction(enum.Enum): @@ -72,7 +71,7 @@ def on_direction_change(event): if direction_vector_sum != (0, 0): direction.current = maybe_new_direction - grid_wrapper = idom.html.div({"onKeyDown": on_direction_change}, grid) + grid_wrapper = idom.html.div(grid, on_key_down=on_direction_change) assign_grid_block_color(grid, food, "blue") @@ -139,50 +138,46 @@ async def interval() -> None: def create_grid(grid_size, block_scale): return idom.html.div( - { - "style": { - "height": f"{block_scale * grid_size}px", - "width": f"{block_scale * grid_size}px", - "cursor": "pointer", - "display": "grid", - "grid-gap": 0, - "grid-template-columns": f"repeat({grid_size}, {block_scale}px)", - "grid-template-rows": f"repeat({grid_size}, {block_scale}px)", - }, - "tabIndex": -1, - }, [ idom.html.div( - {"style": {"height": f"{block_scale}px"}}, [ create_grid_block("black", block_scale, key=i) for i in range(grid_size) ], key=i, + style={"height": f"{block_scale}px"}, ) for i in range(grid_size) ], + style={ + "height": f"{block_scale * grid_size}px", + "width": f"{block_scale * grid_size}px", + "cursor": "pointer", + "display": "grid", + "grid_gap": 0, + "grid_template_columns": f"repeat({grid_size}, {block_scale}px)", + "grid_template_rows": f"repeat({grid_size}, {block_scale}px)", + }, + tab_index=-1, ) def create_grid_block(color, block_scale, key): return idom.html.div( - { - "style": { - "height": f"{block_scale}px", - "width": f"{block_scale}px", - "backgroundColor": color, - "outline": "1px solid grey", - } - }, key=key, + style={ + "height": f"{block_scale}px", + "width": f"{block_scale}px", + "background_color": color, + "outline": "1px solid grey", + }, ) def assign_grid_block_color(grid, point, color): x, y = point block = grid["children"][x]["children"][y] - block["attributes"]["style"]["backgroundColor"] = color + block["attributes"]["style"]["background_color"] = color idom.run(GameView) diff --git a/docs/source/reference/_examples/todo.py b/docs/source/reference/_examples/todo.py index 7b1f6f675..36880e39a 100644 --- a/docs/source/reference/_examples/todo.py +++ b/docs/source/reference/_examples/todo.py @@ -17,10 +17,10 @@ async def remove_task(event, index=index): set_items(items[:index] + items[index + 1 :]) task_text = idom.html.td(idom.html.p(text)) - delete_button = idom.html.td({"onClick": remove_task}, idom.html.button(["x"])) + delete_button = idom.html.td(idom.html.button(["x"]), on_click=remove_task) tasks.append(idom.html.tr(task_text, delete_button)) - task_input = idom.html.input({"onKeyDown": add_new_task}) + task_input = idom.html.input(on_key_down=add_new_task) task_table = idom.html.table(tasks) return idom.html.div( diff --git a/docs/source/reference/_examples/use_reducer_counter.py b/docs/source/reference/_examples/use_reducer_counter.py index ea1b780a0..5f22490eb 100644 --- a/docs/source/reference/_examples/use_reducer_counter.py +++ b/docs/source/reference/_examples/use_reducer_counter.py @@ -17,9 +17,9 @@ def Counter(): count, dispatch = idom.hooks.use_reducer(reducer, 0) return idom.html.div( f"Count: {count}", - idom.html.button({"onClick": lambda event: dispatch("reset")}, "Reset"), - idom.html.button({"onClick": lambda event: dispatch("increment")}, "+"), - idom.html.button({"onClick": lambda event: dispatch("decrement")}, "-"), + idom.html.button("Reset", on_click=lambda event: dispatch("reset")), + idom.html.button("+", on_click=lambda event: dispatch("increment")), + idom.html.button("-", on_click=lambda event: dispatch("decrement")), ) diff --git a/docs/source/reference/_examples/use_state_counter.py b/docs/source/reference/_examples/use_state_counter.py index 8626a60b9..27948edec 100644 --- a/docs/source/reference/_examples/use_state_counter.py +++ b/docs/source/reference/_examples/use_state_counter.py @@ -15,9 +15,9 @@ def Counter(): count, set_count = idom.hooks.use_state(initial_count) return idom.html.div( f"Count: {count}", - idom.html.button({"onClick": lambda event: set_count(initial_count)}, "Reset"), - idom.html.button({"onClick": lambda event: set_count(increment)}, "+"), - idom.html.button({"onClick": lambda event: set_count(decrement)}, "-"), + idom.html.button("Reset", on_click=lambda event: set_count(initial_count)), + idom.html.button("+", on_click=lambda event: set_count(increment)), + idom.html.button("-", on_click=lambda event: set_count(decrement)), ) diff --git a/pyproject.toml b/pyproject.toml index cc0199bcd..633c3156a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,9 @@ exclude_lines = [ '\.\.\.', "raise NotImplementedError", ] +omit = [ + "src/idom/__main__.py", +] [tool.pydocstyle] inherit = false @@ -48,7 +51,7 @@ per-file-ignores = [ "docs/*/_examples/*.py:E402", ] max-line-length = 88 -max-complexity = 18 +max-complexity = 20 select = ["B", "C", "E", "F", "W", "T4", "B9", "N", "ROH"] exclude = ["**/node_modules/*", ".eggs/*", ".tox/*"] # -- flake8-tidy-imports -- diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index 5e4835f12..061839473 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -6,4 +6,5 @@ fastjsonschema >=2.14.5 requests >=2 colorlog >=6 asgiref >=3 -lxml >= 4 +lxml >=4 +click >=8, <9 diff --git a/scripts/fix_vdom_constructor_usage.py b/scripts/fix_vdom_constructor_usage.py new file mode 100644 index 000000000..97ee13d3a --- /dev/null +++ b/scripts/fix_vdom_constructor_usage.py @@ -0,0 +1,370 @@ +from __future__ import annotations + +import ast +import re +import sys +from collections.abc import Sequence +from keyword import kwlist +from pathlib import Path +from textwrap import dedent, indent +from tokenize import COMMENT as COMMENT_TOKEN +from tokenize import generate_tokens +from typing import Iterator + +from idom import html + + +CAMEL_CASE_SUB_PATTERN = re.compile(r"(? None: + tree = ast.parse(source) + + changed: list[Sequence[ast.AST]] = [] + for parents, node in walk_with_parent(tree): + if isinstance(node, ast.Call): + func = node.func + match func: + case ast.Attribute(): + name = func.attr + case ast.Name(ctx=ast.Load()): + name = func.id + case _: + name = "" + if hasattr(html, name): + match node.args: + case [ast.Dict(keys, values), *_]: + new_kwargs = list(node.keywords) + for k, v in zip(keys, values): + if isinstance(k, ast.Constant) and isinstance(k.value, str): + new_kwargs.append( + ast.keyword(arg=conv_attr_name(k.value), value=v) + ) + else: + new_kwargs = [ast.keyword(arg=None, value=node.args[0])] + break + node.args = node.args[1:] + node.keywords = new_kwargs + changed.append((node, *parents)) + case [ + ast.Call( + func=ast.Name(id="dict", ctx=ast.Load()), + args=args, + keywords=kwargs, + ), + *_, + ]: + new_kwargs = [ + *[ast.keyword(arg=None, value=a) for a in args], + *node.keywords, + ] + for kw in kwargs: + if kw.arg is not None: + new_kwargs.append( + ast.keyword( + arg=conv_attr_name(kw.arg), value=kw.value + ) + ) + else: + new_kwargs.append(kw) + node.args = node.args[1:] + node.keywords = new_kwargs + changed.append((node, *parents)) + + case _: + pass + + if not changed: + return + + ast.fix_missing_locations(tree) + + lines = source.split("\n") + + # find closest parent nodes that should be re-written + nodes_to_unparse: list[ast.AST] = [] + for node_lineage in changed: + origin_node = node_lineage[0] + for i in range(len(node_lineage) - 1): + current_node, next_node = node_lineage[i : i + 2] + if ( + not hasattr(next_node, "lineno") + or next_node.lineno < origin_node.lineno + or isinstance(next_node, (ast.ClassDef, ast.FunctionDef)) + ): + nodes_to_unparse.append(current_node) + break + else: + raise RuntimeError("Failed to change code") + + # check if an nodes to rewrite contain eachother, pick outermost nodes + current_outermost_node, *sorted_nodes_to_unparse = list( + sorted(nodes_to_unparse, key=lambda n: n.lineno) + ) + outermost_nodes_to_unparse = [current_outermost_node] + for node in sorted_nodes_to_unparse: + if node.lineno > current_outermost_node.end_lineno: + current_outermost_node = node + outermost_nodes_to_unparse.append(node) + + moved_comment_lines_from_end: list[int] = [] + # now actually rewrite these nodes (in reverse to avoid changes earlier in file) + for node in reversed(outermost_nodes_to_unparse): + # make a best effort to preserve any comments that we're going to overwrite + comments = find_comments(lines[node.lineno - 1 : node.end_lineno]) + + # there may be some content just before and after the content we're re-writing + before_replacement = lines[node.lineno - 1][: node.col_offset].lstrip() + + if node.end_lineno is not None and node.end_col_offset is not None: + after_replacement = lines[node.end_lineno - 1][ + node.end_col_offset : + ].strip() + else: + after_replacement = "" + + replacement = indent( + before_replacement + + "\n".join([*comments, ast.unparse(node)]) + + after_replacement, + " " * (node.col_offset - len(before_replacement)), + ) + + if node.end_lineno: + lines[node.lineno - 1 : node.end_lineno] = [replacement] + else: + lines[node.lineno - 1] = replacement + + if comments: + moved_comment_lines_from_end.append(len(lines) - node.lineno) + + for lineno_from_end in sorted(list(set(moved_comment_lines_from_end))): + print(f"Moved comments to {filename}:{len(lines) - lineno_from_end}") + + return "\n".join(lines) + + +def find_comments(lines: list[str]) -> list[str]: + iter_lines = iter(lines) + return [ + token + for token_type, token, _, _, _ in generate_tokens(lambda: next(iter_lines)) + if token_type == COMMENT_TOKEN + ] + + +def walk_with_parent( + node: ast.AST, parents: tuple[ast.AST, ...] = () +) -> Iterator[tuple[tuple[ast.AST, ...], ast.AST]]: + parents = (node,) + parents + for child in ast.iter_child_nodes(node): + yield parents, child + yield from walk_with_parent(child, parents) + + +def conv_attr_name(name: str) -> str: + new_name = CAMEL_CASE_SUB_PATTERN.sub("_", name).replace("-", "_").lower() + return f"{new_name}_" if new_name in kwlist else new_name + + +def run_tests(): + cases = [ + # simple conversions + ( + 'html.div({"className": "test"})', + "html.div(class_name='test')", + ), + ( + 'html.div({class_name: "test", **other})', + "html.div(**{class_name: 'test', **other})", + ), + ( + 'html.div(dict(other, className="test"))', + "html.div(**other, class_name='test')", + ), + ( + 'html.div({"className": "outer"}, html.div({"className": "inner"}))', + "html.div(html.div(class_name='inner'), class_name='outer')", + ), + ( + 'html.div({"className": "outer"}, html.div({"className": "inner"}))', + "html.div(html.div(class_name='inner'), class_name='outer')", + ), + ( + '["before", html.div({"className": "test"}), "after"]', + "['before', html.div(class_name='test'), 'after']", + ), + ( + """ + html.div( + {"className": "outer"}, + html.div({"className": "inner"}), + html.div({"className": "inner"}), + ) + """, + "html.div(html.div(class_name='inner'), html.div(class_name='inner'), class_name='outer')", + ), + ( + 'html.div(dict(className="test"))', + "html.div(class_name='test')", + ), + # when to not attempt conversion + ( + 'html.div(ignore, {"className": "test"})', + None, + ), + # avoid unnecessary changes + ( + """ + def my_function(): + x = 1 # some comment + return html.div({"className": "test"}) + """, + """ + def my_function(): + x = 1 # some comment + return html.div(class_name='test') + """, + ), + ( + """ + if condition: + # some comment + dom = html.div({"className": "test"}) + """, + """ + if condition: + # some comment + dom = html.div(class_name='test') + """, + ), + ( + """ + [ + html.div({"className": "test"}), + html.div({"className": "test"}), + ] + """, + """ + [ + html.div(class_name='test'), + html.div(class_name='test'), + ] + """, + ), + ( + """ + @deco( + html.div({"className": "test"}), + html.div({"className": "test"}), + ) + def func(): + # comment + x = [ + 1 + ] + """, + """ + @deco( + html.div(class_name='test'), + html.div(class_name='test'), + ) + def func(): + # comment + x = [ + 1 + ] + """, + ), + ( + """ + @deco(html.div({"className": "test"}), html.div({"className": "test"})) + def func(): + # comment + x = [ + 1 + ] + """, + """ + @deco(html.div(class_name='test'), html.div(class_name='test')) + def func(): + # comment + x = [ + 1 + ] + """, + ), + ( + """ + ( + result + if condition + else html.div({"className": "test"}) + ) + """, + """ + ( + result + if condition + else html.div(class_name='test') + ) + """, + ), + # best effort to preserve comments + ( + """ + x = 1 + html.div( + # comment 1 + {"className": "outer"}, + # comment 2 + html.div({"className": "inner"}), + ) + """, + """ + x = 1 + # comment 1 + # comment 2 + html.div(html.div(class_name='inner'), class_name='outer') + """, + ), + ] + + for source, expected in cases: + actual = update_vdom_constructor_usages(dedent(source).strip(), "test.py") + if isinstance(expected, str): + expected = dedent(expected).strip() + if actual != expected: + print(TEST_OUTPUT_TEMPLATE.format(actual=actual, expected=expected)) + return False + + return True + + +if __name__ == "__main__": + argv = sys.argv[1:] + + if not argv: + print("Running tests...") + result = run_tests() + print("Success" if result else "Failed") + sys.exit(0 if result else 0) + + for pattern in argv: + for file in Path.cwd().glob(pattern): + result = update_vdom_constructor_usages( + source=file.read_text(), + filename=str(file), + ) + if result is not None: + file.write_text(result) diff --git a/src/client/packages/idom-client-react/src/element-utils.js b/src/client/packages/idom-client-react/src/element-utils.js index 2300d6d8b..334ca9019 100644 --- a/src/client/packages/idom-client-react/src/element-utils.js +++ b/src/client/packages/idom-client-react/src/element-utils.js @@ -22,18 +22,14 @@ export function createElementAttributes(model, sendEvent) { if (model.eventHandlers) { for (const [eventName, eventSpec] of Object.entries(model.eventHandlers)) { - attributes[eventName] = createEventHandler( - eventName, - sendEvent, - eventSpec - ); + attributes[eventName] = createEventHandler(sendEvent, eventSpec); } } - return attributes; + return Object.fromEntries(Object.entries(attributes).map(normalizeAttribute)); } -function createEventHandler(eventName, sendEvent, eventSpec) { +function createEventHandler(sendEvent, eventSpec) { return function () { const data = Array.from(arguments).map((value) => { if (typeof value === "object" && value.nativeEvent) { @@ -51,7 +47,36 @@ function createEventHandler(eventName, sendEvent, eventSpec) { sendEvent({ data: data, target: eventSpec["target"], - type: "layout-event", }); }; } + +function normalizeAttribute([key, value]) { + 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.replace("_", "-"); + } else { + normKey = snakeToCamel(key); + } + return [normKey, normValue]; +} + +function snakeToCamel(str) { + 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/client/public/assets/idom-logo-square-small.svg b/src/client/public/assets/idom-logo-square-small.svg new file mode 100644 index 000000000..eb36c7b11 --- /dev/null +++ b/src/client/public/assets/idom-logo-square-small.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/idom/__main__.py b/src/idom/__main__.py new file mode 100644 index 000000000..d8d07aa66 --- /dev/null +++ b/src/idom/__main__.py @@ -0,0 +1,17 @@ +import click + +import idom +from idom._console.update_html_usages import update_html_usages + + +@click.group() +@click.version_option(idom.__version__, prog_name=idom.__name__) +def app() -> None: + pass + + +app.add_command(update_html_usages) + + +if __name__ == "__main__": + app() diff --git a/src/idom/_console/__init__.py b/src/idom/_console/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/idom/_console/update_html_usages.py b/src/idom/_console/update_html_usages.py new file mode 100644 index 000000000..f824df56f --- /dev/null +++ b/src/idom/_console/update_html_usages.py @@ -0,0 +1,234 @@ +from __future__ import annotations + +import ast +import re +import sys +from collections.abc import Sequence +from dataclasses import dataclass +from keyword import kwlist +from pathlib import Path +from textwrap import indent +from tokenize import COMMENT as COMMENT_TOKEN +from tokenize import generate_tokens +from typing import Iterator + +import click + +from idom import html + + +CAMEL_CASE_SUB_PATTERN = re.compile(r"(? None: + """Rewrite files under the given paths using the new html element API. + + The old API required users to pass a dictionary of attributes to html element + constructor functions. For example: + + >>> html.div({"className": "x"}, "y") + {"tagName": "div", "attributes": {"className": "x"}, "children": ["y"]} + + The latest API though allows for attributes to be passed as snake_cased keyword + arguments instead. The above example would be rewritten as: + + >>> html.div("y", class_name="x") + {"tagName": "div", "attributes": {"class_name": "x"}, "children": ["y"]} + + All snake_case attributes are converted to camelCase by the client where necessary. + + ----- Notes ----- + + While this command does it's best to preserve as much of the original code as + possible, there are inevitably some limitations in doing this. As a result, we + recommend running your code formatter like Black against your code after executing + this command. + + Additionally, We are unable to perserve the location of comments that lie within any + rewritten code. This command will place the comments in the code it plans to rewrite + just above its changes. As such it requires manual intervention to put those + comments back in their original location. + """ + if sys.version_info < (3, 9): # pragma: no cover + raise RuntimeError("This command requires Python>=3.9") + + for p in map(Path, paths): + for f in [p] if p.is_file() else p.rglob("*.py"): + result = generate_rewrite(file=f, source=f.read_text()) + if result is not None: + f.write_text(result) + + +def generate_rewrite(file: Path, source: str) -> str | None: + tree = ast.parse(source) + + changed: list[Sequence[ast.AST]] = [] + for parents, node in walk_with_parent(tree): + if not isinstance(node, ast.Call): + continue + + func = node.func + if isinstance(func, ast.Attribute): + name = func.attr + elif isinstance(func, ast.Name): + name = func.id + else: + continue + + if name == "vdom": + if len(node.args) < 2: + continue + maybe_attr_dict_node = node.args[1] + # remove attr dict from new args + new_args = node.args[:1] + node.args[2:] + elif hasattr(html, name): + if len(node.args) == 0: + continue + maybe_attr_dict_node = node.args[0] + # remove attr dict from new args + new_args = node.args[1:] + else: + continue + + new_keyword_info = extract_keywords(maybe_attr_dict_node) + if new_keyword_info is not None: + if new_keyword_info.replace: + node.keywords = new_keyword_info.keywords + else: + node.keywords.extend(new_keyword_info.keywords) + + node.args = new_args + changed.append((node, *parents)) + + if not changed: + return None + + ast.fix_missing_locations(tree) + + lines = source.split("\n") + + # find closest parent nodes that should be re-written + nodes_to_unparse: list[ast.AST] = [] + for node_lineage in changed: + origin_node = node_lineage[0] + for i in range(len(node_lineage) - 1): + current_node, next_node = node_lineage[i : i + 2] + if ( + not hasattr(next_node, "lineno") + or next_node.lineno < origin_node.lineno + or isinstance(next_node, (ast.ClassDef, ast.FunctionDef)) + ): + nodes_to_unparse.append(current_node) + break + else: # pragma: no cover + raise RuntimeError("Failed to change code") + + # check if an nodes to rewrite contain eachother, pick outermost nodes + current_outermost_node, *sorted_nodes_to_unparse = list( + sorted(nodes_to_unparse, key=lambda n: n.lineno) + ) + outermost_nodes_to_unparse = [current_outermost_node] + for node in sorted_nodes_to_unparse: + if ( + not current_outermost_node.end_lineno + or node.lineno > current_outermost_node.end_lineno + ): + current_outermost_node = node + outermost_nodes_to_unparse.append(node) + + moved_comment_lines_from_end: list[int] = [] + # now actually rewrite these nodes (in reverse to avoid changes earlier in file) + for node in reversed(outermost_nodes_to_unparse): + # make a best effort to preserve any comments that we're going to overwrite + comments = find_comments(lines[node.lineno - 1 : node.end_lineno]) + + # there may be some content just before and after the content we're re-writing + before_replacement = lines[node.lineno - 1][: node.col_offset].lstrip() + + after_replacement = ( + lines[node.end_lineno - 1][node.end_col_offset :].strip() + if node.end_lineno is not None and node.end_col_offset is not None + else "" + ) + + replacement = indent( + before_replacement + + "\n".join([*comments, ast.unparse(node)]) + + after_replacement, + " " * (node.col_offset - len(before_replacement)), + ) + + lines[node.lineno - 1 : node.end_lineno or node.lineno] = [replacement] + + if comments: + moved_comment_lines_from_end.append(len(lines) - node.lineno) + + for lineno_from_end in sorted(list(set(moved_comment_lines_from_end))): + click.echo(f"Moved comments to {file}:{len(lines) - lineno_from_end}") + + return "\n".join(lines) + + +def extract_keywords(node: ast.AST) -> KeywordInfo | None: + if isinstance(node, ast.Dict): + keywords: list[ast.keyword] = [] + for k, v in zip(node.keys, node.values): + if isinstance(k, ast.Constant) and isinstance(k.value, str): + if k.value == "tagName": + # this is a vdom dict declaration + return None + keywords.append(ast.keyword(arg=conv_attr_name(k.value), value=v)) + else: + return KeywordInfo( + replace=True, + keywords=[ast.keyword(arg=None, value=node)], + ) + return KeywordInfo(replace=False, keywords=keywords) + elif ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "dict" + and isinstance(node.func.ctx, ast.Load) + ): + keywords = [ast.keyword(arg=None, value=a) for a in node.args] + for kw in node.keywords: + if kw.arg == "tagName": + # this is a vdom dict declaration + return None + if kw.arg is not None: + keywords.append(ast.keyword(arg=conv_attr_name(kw.arg), value=kw.value)) + else: + keywords.append(kw) + return KeywordInfo(replace=False, keywords=keywords) + return None + + +def find_comments(lines: list[str]) -> list[str]: + iter_lines = iter(lines) + return [ + token + for token_type, token, _, _, _ in generate_tokens(lambda: next(iter_lines)) + if token_type == COMMENT_TOKEN + ] + + +def walk_with_parent( + node: ast.AST, parents: tuple[ast.AST, ...] = () +) -> Iterator[tuple[tuple[ast.AST, ...], ast.AST]]: + parents = (node,) + parents + for child in ast.iter_child_nodes(node): + yield parents, child + yield from walk_with_parent(child, parents) + + +def conv_attr_name(name: str) -> str: + new_name = CAMEL_CASE_SUB_PATTERN.sub("_", name).replace("-", "_").lower() + return f"{new_name}_" if new_name in kwlist else new_name + + +@dataclass +class KeywordInfo: + replace: bool + keywords: list[ast.keyword] diff --git a/src/idom/_option.py b/src/idom/_option.py index d9d8e9d79..50df79b11 100644 --- a/src/idom/_option.py +++ b/src/idom/_option.py @@ -1,11 +1,10 @@ from __future__ import annotations import os -from inspect import currentframe from logging import getLogger -from types import FrameType -from typing import Any, Callable, Generic, Iterator, TypeVar, cast -from warnings import warn +from typing import Any, Callable, Generic, TypeVar, cast + +from idom._warnings import warn _O = TypeVar("_O") @@ -129,25 +128,5 @@ def current(self) -> _O: warn( self._deprecation_message, DeprecationWarning, - stacklevel=_frame_depth_in_module() + 1, ) return super().current - - -def _frame_depth_in_module() -> int: - depth = 0 - for frame in _iter_frames(2): - if frame.f_globals.get("__name__") != __name__: - break - depth += 1 - return depth - - -def _iter_frames(index: int = 1) -> Iterator[FrameType]: - frame = currentframe() - while frame is not None: - if index == 0: - yield frame - else: - index -= 1 - frame = frame.f_back diff --git a/src/idom/_warnings.py b/src/idom/_warnings.py new file mode 100644 index 000000000..827b66be0 --- /dev/null +++ b/src/idom/_warnings.py @@ -0,0 +1,31 @@ +from functools import wraps +from inspect import currentframe +from types import FrameType +from typing import Any, Iterator +from warnings import warn as _warn + + +@wraps(_warn) +def warn(*args: Any, **kwargs: Any) -> Any: + # warn at call site outside of IDOM + _warn(*args, stacklevel=_frame_depth_in_module() + 1, **kwargs) # type: ignore + + +def _frame_depth_in_module() -> int: + depth = 0 + for frame in _iter_frames(2): + module_name = frame.f_globals.get("__name__") + if not module_name or not module_name.startswith("idom."): + break + depth += 1 + return depth + + +def _iter_frames(index: int = 1) -> Iterator[FrameType]: + frame = currentframe() + while frame is not None: + if index == 0: + yield frame + else: + index -= 1 + frame = frame.f_back diff --git a/src/idom/backend/_common.py b/src/idom/backend/_common.py index dd7916353..f1db24818 100644 --- a/src/idom/backend/_common.py +++ b/src/idom/backend/_common.py @@ -113,11 +113,9 @@ class CommonOptions: head: Sequence[VdomDict] | VdomDict | str = ( html.title("IDOM"), html.link( - { - "rel": "icon", - "href": "_idom/assets/idom-logo-square-small.svg", - "type": "image/svg+xml", - } + rel="icon", + href="_idom/assets/idom-logo-square-small.svg", + type="image/svg+xml", ), ) """Add elements to the ```` of the application. diff --git a/src/idom/core/component.py b/src/idom/core/component.py index ff4e4c655..a7d4248e3 100644 --- a/src/idom/core/component.py +++ b/src/idom/core/component.py @@ -2,7 +2,7 @@ import inspect from functools import wraps -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Callable, Optional from .types import ComponentType, VdomDict @@ -41,8 +41,8 @@ def __init__( self, function: Callable[..., ComponentType | VdomDict | str | None], key: Optional[Any], - args: Tuple[Any, ...], - kwargs: Dict[str, Any], + args: tuple[Any, ...], + kwargs: dict[str, Any], sig: inspect.Signature, ) -> None: self.key = key diff --git a/src/idom/core/hooks.py b/src/idom/core/hooks.py index 53bce9087..578c54b0a 100644 --- a/src/idom/core/hooks.py +++ b/src/idom/core/hooks.py @@ -8,9 +8,7 @@ Any, Awaitable, Callable, - Dict, Generic, - List, NewType, Optional, Sequence, @@ -625,7 +623,7 @@ def __init__( self._rendered_atleast_once = False self._current_state_index = 0 self._state: Tuple[Any, ...] = () - self._event_effects: Dict[EffectType, List[Callable[[], None]]] = { + self._event_effects: dict[EffectType, list[Callable[[], None]]] = { COMPONENT_DID_RENDER_EFFECT: [], LAYOUT_DID_RENDER_EFFECT: [], COMPONENT_WILL_UNMOUNT_EFFECT: [], diff --git a/src/idom/core/layout.py b/src/idom/core/layout.py index a0bbad042..4338ae0fd 100644 --- a/src/idom/core/layout.py +++ b/src/idom/core/layout.py @@ -8,15 +8,11 @@ from typing import ( Any, Callable, - Dict, Generic, Iterator, - List, NamedTuple, NewType, Optional, - Set, - Tuple, TypeVar, cast, ) @@ -217,7 +213,7 @@ def _render_model_attributes( self, old_state: Optional[_ModelState], new_state: _ModelState, - raw_model: Dict[str, Any], + raw_model: dict[str, Any], ) -> None: # extract event handlers from 'eventHandlers' and 'attributes' handlers_by_event: EventHandlerDict = raw_model.get("eventHandlers", {}) @@ -385,7 +381,7 @@ def _render_model_children_without_old_state( self, exit_stack: ExitStack, new_state: _ModelState, - raw_children: List[Any], + raw_children: list[Any], ) -> None: child_type_key_tuples = list(_process_child_type_and_key(raw_children)) @@ -412,7 +408,7 @@ def _render_model_children_without_old_state( else: new_state.append_child(child) - def _unmount_model_states(self, old_states: List[_ModelState]) -> None: + def _unmount_model_states(self, old_states: list[_ModelState]) -> None: to_unmount = old_states[::-1] # unmount in reversed order of rendering while to_unmount: model_state = to_unmount.pop() @@ -561,8 +557,8 @@ def __init__( key: Any, model: Ref[VdomJson], patch_path: str, - children_by_key: Dict[str, _ModelState], - targets_by_event: Dict[str, str], + children_by_key: dict[str, _ModelState], + targets_by_event: dict[str, str], life_cycle_state: Optional[_LifeCycleState] = None, ): self.index = index @@ -661,7 +657,7 @@ class _ThreadSafeQueue(Generic[_Type]): def __init__(self) -> None: self._loop = asyncio.get_running_loop() self._queue: asyncio.Queue[_Type] = asyncio.Queue() - self._pending: Set[_Type] = set() + self._pending: set[_Type] = set() def put(self, value: _Type) -> None: if value not in self._pending: @@ -679,8 +675,8 @@ async def get(self) -> _Type: def _process_child_type_and_key( - children: List[Any], -) -> Iterator[Tuple[Any, _ElementType, Any]]: + children: list[Any], +) -> Iterator[tuple[Any, _ElementType, Any]]: for index, child in enumerate(children): if isinstance(child, dict): child_type = _DICT_TYPE diff --git a/src/idom/core/types.py b/src/idom/core/types.py index 0fd78ec22..38a17481a 100644 --- a/src/idom/core/types.py +++ b/src/idom/core/types.py @@ -2,25 +2,22 @@ import sys from collections import namedtuple -from collections.abc import Sequence from types import TracebackType from typing import ( TYPE_CHECKING, Any, Callable, - Dict, Generic, - Iterable, - List, Mapping, NamedTuple, Optional, + Sequence, Type, TypeVar, Union, ) -from typing_extensions import Literal, Protocol, TypedDict, runtime_checkable +from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, runtime_checkable _Type = TypeVar("_Type") @@ -62,10 +59,13 @@ class ComponentType(Protocol): This is used to see if two component instances share the same definition. """ - def render(self) -> VdomDict | ComponentType | str | None: + def render(self) -> RenderResult: """Render the component's view model.""" +RenderResult = Union["VdomDict", ComponentType, str, None] + + _Render = TypeVar("_Render", covariant=True) _Event = TypeVar("_Event", contravariant=True) @@ -98,23 +98,16 @@ async def __aexit__( VdomChild = Union[ComponentType, "VdomDict", str] """A single child element of a :class:`VdomDict`""" -VdomChildren = "Sequence[VdomChild]" +VdomChildren = Sequence[VdomChild] """Describes a series of :class:`VdomChild` elements""" -VdomAttributesAndChildren = Union[ - Mapping[str, Any], # this describes both VdomDict and VdomAttributes - Iterable[VdomChild], - VdomChild, -] -"""Useful for the ``*attributes_and_children`` parameter in :func:`idom.core.vdom.vdom`""" - class _VdomDictOptional(TypedDict, total=False): key: Key | None children: Sequence[ # recursive types are not allowed yet: # https://github.com/python/mypy/issues/731 - Union[ComponentType, Dict[str, Any], str, Any] + Union[ComponentType, dict[str, Any], str, Any] ] attributes: VdomAttributes eventHandlers: EventHandlerDict # noqa @@ -139,9 +132,9 @@ class ImportSourceDict(TypedDict): class _OptionalVdomJson(TypedDict, total=False): key: Key error: str - children: List[Any] - attributes: Dict[str, Any] - eventHandlers: Dict[str, _JsonEventTarget] # noqa + children: list[Any] + attributes: dict[str, Any] + eventHandlers: dict[str, _JsonEventTarget] # noqa importSource: _JsonImportSource # noqa @@ -167,7 +160,7 @@ class _JsonImportSource(TypedDict): EventHandlerMapping = Mapping[str, "EventHandlerType"] """A generic mapping between event names to their handlers""" -EventHandlerDict = Dict[str, "EventHandlerType"] +EventHandlerDict: TypeAlias = "dict[str, EventHandlerType]" """A dict mapping between event names to their handlers""" @@ -208,9 +201,9 @@ class VdomDictConstructor(Protocol): def __call__( self, - *attributes_and_children: VdomAttributesAndChildren, - key: str = ..., - event_handlers: Optional[EventHandlerMapping] = ..., + *children: VdomChild | VdomChildren, + key: Key | None = None, + **attributes: Any, ) -> VdomDict: ... diff --git a/src/idom/core/vdom.py b/src/idom/core/vdom.py index 78bfb3725..6b4d5c5d4 100644 --- a/src/idom/core/vdom.py +++ b/src/idom/core/vdom.py @@ -1,11 +1,12 @@ from __future__ import annotations import logging -from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, cast +from typing import Any, DefaultDict, Mapping, cast from fastjsonschema import compile as compile_json_schema -from typing_extensions import Protocol +from typing_extensions import TypeGuard +from idom._warnings import warn from idom.config import IDOM_DEBUG_MODE from idom.core.events import ( EventHandler, @@ -15,11 +16,13 @@ from idom.core.types import ( ComponentType, EventHandlerDict, - EventHandlerMapping, EventHandlerType, ImportSourceDict, - VdomAttributesAndChildren, + Key, + VdomChild, + VdomChildren, VdomDict, + VdomDictConstructor, VdomJson, ) @@ -130,12 +133,11 @@ def is_vdom(value: Any) -> bool: def vdom( tag: str, - *attributes_and_children: VdomAttributesAndChildren, - key: str | int | None = None, - event_handlers: Optional[EventHandlerMapping] = None, - import_source: Optional[ImportSourceDict] = None, + *children: VdomChild | VdomChildren, + key: Key | None = None, + **attributes: Any, ) -> VdomDict: - """A helper function for creating VDOM dictionaries. + """A helper function for creating VDOM elements. Parameters: tag: @@ -157,16 +159,43 @@ def vdom( """ model: VdomDict = {"tagName": tag} - attributes, children = coalesce_attributes_and_children(attributes_and_children) - attributes, event_handlers = separate_attributes_and_event_handlers( - attributes, event_handlers or {} - ) + flattened_children: list[VdomChild] = [] + for child in children: + if isinstance(child, dict) and "tagName" not in child: # pragma: no cover + warn( + ( + "Element constructor signatures have changed! This will be an error " + "in a future release. All element constructors now have the " + "following usage where attributes may be snake_case keyword " + "arguments: " + "\n\n" + ">>> html.div(*children, key=key, **attributes) " + "\n\n" + "A CLI tool for automatically updating code to the latest API has " + "been provided with this release of IDOM (e.g. 'idom " + "update-html-usages'). However, it may not resolve all issues " + "arrising from this change. Start a discussion if you need help " + "transitioning to this new interface: " + "https://github.com/idom-team/idom/discussions/new?category=question" + ), + DeprecationWarning, + ) + attributes.update(child) + if _is_single_child(child): + flattened_children.append(child) + else: + # FIXME: Types do not narrow in negative case of TypeGaurd + # This cannot be fixed until there is some sort of "StrictTypeGuard". + # See: https://github.com/python/typing/discussions/1013 + flattened_children.extend(child) # type: ignore + + attributes, event_handlers = separate_attributes_and_event_handlers(attributes) if attributes: model["attributes"] = attributes - if children: - model["children"] = children + if flattened_children: + model["children"] = flattened_children if event_handlers: model["eventHandlers"] = event_handlers @@ -174,26 +203,18 @@ def vdom( if key is not None: model["key"] = key - if import_source is not None: - model["importSource"] = import_source - return model -class _VdomDictConstructor(Protocol): - def __call__( - self, - *attributes_and_children: VdomAttributesAndChildren, - key: str | int | None = ..., - event_handlers: Optional[EventHandlerMapping] = ..., - import_source: Optional[ImportSourceDict] = ..., - ) -> VdomDict: - ... +def with_import_source(element: VdomDict, import_source: ImportSourceDict) -> VdomDict: + return {**element, "importSource": import_source} # type: ignore def make_vdom_constructor( - tag: str, allow_children: bool = True -) -> _VdomDictConstructor: + tag: str, + allow_children: bool = True, + import_source: ImportSourceDict | None = None, +) -> VdomDictConstructor: """Return a constructor for VDOM dictionaries with the given tag name. The resulting callable will have the same interface as :func:`vdom` but without its @@ -201,20 +222,17 @@ def make_vdom_constructor( """ def constructor( - *attributes_and_children: VdomAttributesAndChildren, - key: str | int | None = None, - event_handlers: Optional[EventHandlerMapping] = None, - import_source: Optional[ImportSourceDict] = None, + *children: VdomChild | VdomChildren, + key: Key | None = None, + **attributes: Any, ) -> VdomDict: - model = vdom( - tag, - *attributes_and_children, - key=key, - event_handlers=event_handlers, - import_source=import_source, - ) - if not allow_children and "children" in model: + if not allow_children and children: raise TypeError(f"{tag!r} nodes cannot have children.") + + model = vdom(tag, *children, key=key, **attributes) + if import_source is not None: + model = with_import_source(model, import_source) + return model # replicate common function attributes @@ -233,36 +251,11 @@ def constructor( return constructor -def coalesce_attributes_and_children( - values: Sequence[Any], -) -> Tuple[Mapping[str, Any], List[Any]]: - if not values: - return {}, [] - - children_or_iterables: Sequence[Any] - attributes, *children_or_iterables = values - if not _is_attributes(attributes): - attributes = {} - children_or_iterables = values - - children: List[Any] = [] - for child in children_or_iterables: - if _is_single_child(child): - children.append(child) - else: - children.extend(child) - - return attributes, children - - def separate_attributes_and_event_handlers( - attributes: Mapping[str, Any], event_handlers: EventHandlerMapping -) -> Tuple[Dict[str, Any], EventHandlerDict]: + attributes: Mapping[str, Any] +) -> tuple[dict[str, Any], EventHandlerDict]: separated_attributes = {} - separated_event_handlers: Dict[str, List[EventHandlerType]] = {} - - for k, v in event_handlers.items(): - separated_event_handlers[k] = [v] + separated_handlers: DefaultDict[str, list[EventHandlerType]] = DefaultDict(list) for k, v in attributes.items(): @@ -271,7 +264,8 @@ def separate_attributes_and_event_handlers( if callable(v): handler = EventHandler(to_event_handler_function(v)) elif ( - # isinstance check on protocols is slow, function attr check is a quick filter + # isinstance check on protocols is slow - use function attr pre-check as a + # quick filter before actually performing slow EventHandlerType type check hasattr(v, "function") and isinstance(v, EventHandlerType) ): @@ -280,23 +274,16 @@ def separate_attributes_and_event_handlers( separated_attributes[k] = v continue - if k not in separated_event_handlers: - separated_event_handlers[k] = [handler] - else: - separated_event_handlers[k].append(handler) + separated_handlers[k].append(handler) flat_event_handlers_dict = { - k: merge_event_handlers(h) for k, h in separated_event_handlers.items() + k: merge_event_handlers(h) for k, h in separated_handlers.items() } return separated_attributes, flat_event_handlers_dict -def _is_attributes(value: Any) -> bool: - return isinstance(value, Mapping) and "tagName" not in value - - -def _is_single_child(value: Any) -> bool: +def _is_single_child(value: Any) -> TypeGuard[VdomChild]: if isinstance(value, (str, Mapping)) or not hasattr(value, "__iter__"): return True if IDOM_DEBUG_MODE.current: diff --git a/src/idom/html.py b/src/idom/html.py index 964af5d6e..8b4b3edfd 100644 --- a/src/idom/html.py +++ b/src/idom/html.py @@ -157,10 +157,10 @@ from __future__ import annotations -from typing import Any, Mapping +from typing import Any from idom.core.types import Key, VdomDict -from idom.core.vdom import coalesce_attributes_and_children, make_vdom_constructor +from idom.core.vdom import make_vdom_constructor, vdom __all__ = ( @@ -280,18 +280,7 @@ def _(*children: Any, key: Key | None = None) -> VdomDict: """An HTML fragment - this element will not appear in the DOM""" - attributes, coalesced_children = coalesce_attributes_and_children(children) - if attributes: - raise TypeError("Fragments cannot have attributes") - model: VdomDict = {"tagName": ""} - - if coalesced_children: - model["children"] = coalesced_children - - if key is not None: - model["key"] = key - - return model + return vdom("", *children, key=key) # Dcument metadata @@ -389,10 +378,7 @@ def _(*children: Any, key: Key | None = None) -> VdomDict: noscript = make_vdom_constructor("noscript") -def script( - *attributes_and_children: Mapping[str, Any] | str, - key: str | int | None = None, -) -> VdomDict: +def script(*children: str, key: Key | None = None, **attributes: Any) -> VdomDict: """Create a new `<{script}> `__ element. This behaves slightly differently than a normal script element in that it may be run @@ -406,29 +392,20 @@ def script( function that is called when the script element is removed from the tree, or when the script content changes. """ - model: VdomDict = {"tagName": "script"} - - attributes, children = coalesce_attributes_and_children(attributes_and_children) - if children: if len(children) > 1: raise ValueError("'script' nodes may have, at most, one child.") elif not isinstance(children[0], str): raise ValueError("The child of a 'script' must be a string.") else: - model["children"] = children if key is None: key = children[0] if attributes: - model["attributes"] = attributes if key is None and not children and "src" in attributes: key = attributes["src"] - if key is not None: - model["key"] = key - - return model + return vdom("script", *children, key=key, **attributes) # Demarcating edits diff --git a/src/idom/sample.py b/src/idom/sample.py index 908de34b7..45ff87076 100644 --- a/src/idom/sample.py +++ b/src/idom/sample.py @@ -8,14 +8,12 @@ @component def SampleApp() -> VdomDict: return html.div( - {"id": "sample", "style": {"padding": "15px"}}, html.h1("Sample Application"), html.p( "This is a basic application made with IDOM. Click ", - html.a( - {"href": "https://pypi.org/project/idom/", "target": "_blank"}, - "here", - ), + html.a("here", href="https://pypi.org/project/idom/", target="_blank"), " to learn more.", ), + id="sample", + style={"padding": "15px"}, ) diff --git a/src/idom/types.py b/src/idom/types.py index 73ffef03b..2931e8636 100644 --- a/src/idom/types.py +++ b/src/idom/types.py @@ -20,7 +20,6 @@ RootComponentConstructor, State, VdomAttributes, - VdomAttributesAndChildren, VdomChild, VdomChildren, VdomDict, @@ -46,7 +45,6 @@ "RootComponentConstructor", "State", "VdomAttributes", - "VdomAttributesAndChildren", "VdomChild", "VdomChildren", "VdomDict", diff --git a/src/idom/utils.py b/src/idom/utils.py index 1deab122d..14a27e27f 100644 --- a/src/idom/utils.py +++ b/src/idom/utils.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from itertools import chain from typing import Any, Callable, Generic, Iterable, TypeVar, cast @@ -154,7 +153,7 @@ def _etree_to_vdom( vdom: VdomDict if hasattr(idom.html, node.tag): - vdom = getattr(idom.html, node.tag)(attributes, *children, key=key) + vdom = getattr(idom.html, node.tag)(*children, key=key, **attributes) else: vdom = {"tagName": node.tag} if children: @@ -233,9 +232,9 @@ def _mutate_vdom(vdom: VdomDict) -> None: # Convince type checker that it's safe to mutate attributes assert isinstance(vdom["attributes"], dict) - # Convert style attribute from str -> dict with camelCase keys + # Convert style attribute from str -> dict with snake case keys vdom["attributes"]["style"] = { - _hypen_to_camel_case(key.strip()): value.strip() + key.strip().replace("-", "_"): value.strip() for key, value in ( part.split(":", 1) for part in vdom["attributes"]["style"].split(";") @@ -266,12 +265,6 @@ def _generate_vdom_children( ) -def _hypen_to_camel_case(string: str) -> str: - """Convert a hypenated string to camelCase.""" - first, _, remainder = string.partition("-") - return first.lower() + remainder.title().replace("-", "") - - def del_html_head_body_transform(vdom: VdomDict) -> VdomDict: """Transform intended for use with `html_to_vdom`. @@ -292,18 +285,20 @@ def _vdom_attr_to_html_str(key: str, value: Any) -> tuple[str, str]: value = ";".join( # We lower only to normalize - CSS is case-insensitive: # https://www.w3.org/TR/css-fonts-3/#font-family-casing - f"{_CAMEL_CASE_SUB_PATTERN.sub('-', k).lower()}:{v}" + f"{k.replace('_', '-').lower()}:{v}" for k, v in value.items() ) elif ( # camel to data-* attributes - key.startswith("data") + key.startswith("data_") # camel to aria-* attributes - or key.startswith("aria") + or key.startswith("aria_") # handle special cases or key in _DASHED_HTML_ATTRS ): - key = _CAMEL_CASE_SUB_PATTERN.sub("-", key) + key = key.replace("_", "-") + else: + key = key.replace("_", "") assert not callable( value @@ -314,9 +309,6 @@ def _vdom_attr_to_html_str(key: str, value: Any) -> tuple[str, str]: return key.lower(), str(value) -# Pattern for delimitting camelCase names (e.g. camelCase to camel-case) -_CAMEL_CASE_SUB_PATTERN = re.compile(r"(? VdomDictConstructor: - return partial( - make_vdom_constructor( - name, - allow_children=allow_children, - ), + return make_vdom_constructor( + name, + allow_children=allow_children, import_source=ImportSourceDict( source=web_module.source, sourceType=web_module.source_type, diff --git a/src/idom/widgets.py b/src/idom/widgets.py index 6e114aff2..a8899b757 100644 --- a/src/idom/widgets.py +++ b/src/idom/widgets.py @@ -1,14 +1,14 @@ from __future__ import annotations from base64 import b64encode -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, TypeVar, Union -from warnings import warn +from typing import Any, Callable, Iterable, Tuple, TypeVar, Union from typing_extensions import Protocol import idom from . import html +from ._warnings import warn from .core.types import ComponentConstructor, VdomDict from .testing.backend import _hotswap, _MountFunc @@ -16,7 +16,7 @@ def image( format: str, value: Union[str, bytes] = "", - attributes: Optional[Dict[str, Any]] = None, + **attributes: Any, ) -> VdomDict: """Utility for constructing an image from a string or bytes @@ -33,19 +33,19 @@ def image( base64_value = b64encode(bytes_value).decode() src = f"data:image/{format};base64,{base64_value}" - return {"tagName": "img", "attributes": {"src": src, **(attributes or {})}} + return {"tagName": "img", "attributes": {"src": src, **attributes}} _Value = TypeVar("_Value") def use_linked_inputs( - attributes: Sequence[Dict[str, Any]], + attributes: Iterable[dict[str, Any]], on_change: Callable[[_Value], None] = lambda value: None, cast: _CastFunc[_Value] = lambda value: value, initial_value: str = "", ignore_empty: bool = True, -) -> List[VdomDict]: +) -> list[VdomDict]: """Return a list of linked inputs equal to the number of given attributes. Parameters: @@ -67,7 +67,7 @@ def use_linked_inputs( """ value, set_value = idom.hooks.use_state(initial_value) - def sync_inputs(event: Dict[str, Any]) -> None: + def sync_inputs(event: dict[str, Any]) -> None: new_value = event["target"]["value"] set_value(new_value) if not new_value and ignore_empty: @@ -82,7 +82,7 @@ def sync_inputs(event: Dict[str, Any]) -> None: key = attrs.pop("key", None) attrs.update({"onChange": sync_inputs, "value": value}) - inputs.append(html.input(attrs, key=key)) + inputs.append(html.input(key=key, **attrs)) return inputs diff --git a/tests/test__console/__init__.py b/tests/test__console/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test__console/test_update_html_usages.py b/tests/test__console/test_update_html_usages.py new file mode 100644 index 000000000..2b5bb34c4 --- /dev/null +++ b/tests/test__console/test_update_html_usages.py @@ -0,0 +1,244 @@ +import sys +from pathlib import Path +from textwrap import dedent + +import pytest +from click.testing import CliRunner + +from idom._console.update_html_usages import generate_rewrite, update_html_usages + + +if sys.version_info < (3, 9): + pytestmark = pytest.mark.skip(reason="ast.unparse is Python>=3.9") + + +def test_update_html_usages(tmp_path): + runner = CliRunner() + + tempfile: Path = tmp_path / "temp.py" + tempfile.write_text("html.div({'className': test})") + result = runner.invoke( + update_html_usages, + args=[str(tmp_path)], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + assert tempfile.read_text() == "html.div(class_name=test)" + + +def test_update_html_usages_no_files(): + runner = CliRunner() + + result = runner.invoke( + update_html_usages, args=["directory-does-no-exist"], catch_exceptions=False + ) + + assert result.exit_code != 0 + + +@pytest.mark.parametrize( + "source, expected", + [ + ( + 'html.div({"className": "test"})', + "html.div(class_name='test')", + ), + ( + 'vdom("div", {"className": "test"})', + "vdom('div', class_name='test')", + ), + ( + 'html.div({variable: "test", **other, "key": value})', + "html.div(**{variable: 'test', **other, 'key': value})", + ), + ( + 'html.div(dict(other, className="test", **another))', + "html.div(**other, class_name='test', **another)", + ), + ( + 'html.div({"className": "outer"}, html.div({"className": "inner"}))', + "html.div(html.div(class_name='inner'), class_name='outer')", + ), + ( + 'html.div({"className": "outer"}, html.div({"className": "inner"}))', + "html.div(html.div(class_name='inner'), class_name='outer')", + ), + ( + '["before", html.div({"className": "test"}), "after"]', + "['before', html.div(class_name='test'), 'after']", + ), + ( + """ + html.div( + {"className": "outer"}, + html.div({"className": "inner"}), + html.div({"className": "inner"}), + ) + """, + "html.div(html.div(class_name='inner'), html.div(class_name='inner'), class_name='outer')", + ), + ( + 'html.div(dict(className="test"))', + "html.div(class_name='test')", + ), + # when to not attempt conversion + ( + 'html.div(ignore, {"className": "test"})', + None, + ), + ( + "html.div()", + None, + ), + ( + 'html.vdom("div")', + None, + ), + ( + 'html.div({"tagName": "test"})', + None, + ), + ( + 'html.div(dict(tagName="test"))', + None, + ), + ( + 'html.not_an_html_tag({"className": "test"})', + None, + ), + ( + 'html.div(class_name="test")', + None, + ), + ( + # we don't try to interpret the logic here + '(div or button)({"className": "test"})', + None, + ), + # avoid unnecessary changes + ( + """ + def my_function(): + x = 1 # some comment + return html.div({"className": "test"}) + """, + """ + def my_function(): + x = 1 # some comment + return html.div(class_name='test') + """, + ), + ( + """ + if condition: + # some comment + dom = html.div({"className": "test"}) + """, + """ + if condition: + # some comment + dom = html.div(class_name='test') + """, + ), + ( + """ + [ + html.div({"className": "test"}), + html.div({"className": "test"}), + ] + """, + """ + [ + html.div(class_name='test'), + html.div(class_name='test'), + ] + """, + ), + ( + """ + @deco( + html.div({"className": "test"}), + html.div({"className": "test"}), + ) + def func(): + # comment + x = [ + 1 + ] + """, + """ + @deco( + html.div(class_name='test'), + html.div(class_name='test'), + ) + def func(): + # comment + x = [ + 1 + ] + """, + ), + ( + """ + @deco(html.div({"className": "test"}), html.div({"className": "test"})) + def func(): + # comment + x = [ + 1 + ] + """, + """ + @deco(html.div(class_name='test'), html.div(class_name='test')) + def func(): + # comment + x = [ + 1 + ] + """, + ), + ( + """ + ( + result + if condition + else html.div({"className": "test"}) + ) + """, + """ + ( + result + if condition + else html.div(class_name='test') + ) + """, + ), + # best effort to preserve comments + ( + """ + x = 1 + html.div( + # comment 1 + {"className": "outer"}, + # comment 2 + html.div({"className": "inner"}), + ) + """, + """ + x = 1 + # comment 1 + # comment 2 + html.div(html.div(class_name='inner'), class_name='outer') + """, + ), + ], + ids=lambda item: " ".join(map(str.strip, item.split())) + if isinstance(item, str) + else item, +) +def test_generate_rewrite(source, expected): + actual = generate_rewrite(Path("test.py"), dedent(source).strip()) + if isinstance(expected, str): + expected = dedent(expected).strip() + + assert actual == expected diff --git a/tests/test_backend/test__common.py b/tests/test_backend/test__common.py index 3a3b648d5..c7b68401b 100644 --- a/tests/test_backend/test__common.py +++ b/tests/test_backend/test__common.py @@ -44,7 +44,7 @@ def test_catch_unsafe_relative_path_traversal(tmp_path, bad_path): ), ( html.head( - html.meta({"charset": "utf-8"}), + html.meta(charset="utf-8"), html.title("example"), ), # we strip the head element @@ -52,14 +52,14 @@ def test_catch_unsafe_relative_path_traversal(tmp_path, bad_path): ), ( html._( - html.meta({"charset": "utf-8"}), + html.meta(charset="utf-8"), html.title("example"), ), 'example', ), ( [ - html.meta({"charset": "utf-8"}), + html.meta(charset="utf-8"), html.title("example"), ], 'example', diff --git a/tests/test_backend/test_all.py b/tests/test_backend/test_all.py index 98036cb16..a6417580e 100644 --- a/tests/test_backend/test_all.py +++ b/tests/test_backend/test_all.py @@ -39,7 +39,7 @@ async def display(page, request): async def test_display_simple_hello_world(display: DisplayFixture): @idom.component def Hello(): - return idom.html.p({"id": "hello"}, ["Hello World"]) + return idom.html.p(["Hello World"], id="hello") await display.show(Hello) @@ -56,11 +56,9 @@ async def test_display_simple_click_counter(display: DisplayFixture): def Counter(): count, set_count = idom.hooks.use_state(0) return idom.html.button( - { - "id": "counter", - "onClick": lambda event: set_count(lambda old_count: old_count + 1), - }, f"Count: {count}", + id="counter", + on_click=lambda event: set_count(lambda old_count: old_count + 1), ) await display.show(Counter) @@ -85,7 +83,7 @@ async def test_use_connection(display: DisplayFixture): @idom.component def ShowScope(): conn.current = idom.use_connection() - return html.pre({"id": "scope"}, str(conn.current)) + return html.pre(str(conn.current), id="scope") await display.show(ShowScope) @@ -99,7 +97,7 @@ async def test_use_scope(display: DisplayFixture): @idom.component def ShowScope(): scope.current = idom.use_scope() - return html.pre({"id": "scope"}, str(scope.current)) + return html.pre(str(scope.current), id="scope") await display.show(ShowScope) @@ -147,7 +145,7 @@ async def test_use_request(display: DisplayFixture, hook_name): @idom.component def ShowRoute(): hook_val.current = hook() - return html.pre({"id": "hook"}, str(hook_val.current)) + return html.pre(str(hook_val.current), id="hook") await display.show(ShowRoute) diff --git a/tests/test_client.py b/tests/test_client.py index 0e48e3390..c6646f302 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -22,7 +22,7 @@ async def test_automatic_reconnect(browser: Browser): @idom.component def OldComponent(): - return idom.html.p({"id": "old-component"}, "old") + return idom.html.p("old", id="old-component") async with AsyncExitStack() as exit_stack: server = await exit_stack.enter_async_context(BackendFixture(port=port)) @@ -43,7 +43,7 @@ def OldComponent(): @idom.component def NewComponent(): state, set_state.current = idom.hooks.use_state(0) - return idom.html.p({"id": f"new-component-{state}"}, f"new-{state}") + return idom.html.p(f"new-{state}", id=f"new-component-{state}") async with AsyncExitStack() as exit_stack: server = await exit_stack.enter_async_context(BackendFixture(port=port)) @@ -76,12 +76,10 @@ def ButtonWithChangingColor(): color_toggle, set_color_toggle = idom.hooks.use_state(True) color = "red" if color_toggle else "blue" return idom.html.button( - { - "id": "my-button", - "onClick": lambda event: set_color_toggle(not color_toggle), - "style": {"backgroundColor": color, "color": "white"}, - }, f"color: {color}", + id="my-button", + on_click=lambda event: set_color_toggle(not color_toggle), + style={"background_color": color, "color": "white"}, ) await display.show(ButtonWithChangingColor) @@ -117,7 +115,7 @@ async def handle_change(event): await asyncio.sleep(delay) set_value(event["target"]["value"]) - return idom.html.input({"onChange": handle_change, "id": "test-input"}) + return idom.html.input(on_change=handle_change, id="test-input") await display.show(SomeComponent) diff --git a/tests/test_core/test_component.py b/tests/test_core/test_component.py index 28c8b00f2..9c2513a03 100644 --- a/tests/test_core/test_component.py +++ b/tests/test_core/test_component.py @@ -35,11 +35,11 @@ def SimpleParamComponent(tag): async def test_component_with_var_args(): @idom.component def ComponentWithVarArgsAndKwargs(*args, **kwargs): - return idom.html.div(kwargs, args) + return idom.html.div(*args, **kwargs) - assert ComponentWithVarArgsAndKwargs("hello", "world", myAttr=1).render() == { + assert ComponentWithVarArgsAndKwargs("hello", "world", my_attr=1).render() == { "tagName": "div", - "attributes": {"myAttr": 1}, + "attributes": {"my_attr": 1}, "children": ["hello", "world"], } @@ -47,7 +47,7 @@ def ComponentWithVarArgsAndKwargs(*args, **kwargs): async def test_display_simple_hello_world(display: DisplayFixture): @idom.component def Hello(): - return idom.html.p({"id": "hello"}, ["Hello World"]) + return idom.html.p(["Hello World"], id="hello") await display.show(Hello) @@ -58,10 +58,10 @@ async def test_pre_tags_are_rendered_correctly(display: DisplayFixture): @idom.component def PreFormated(): return idom.html.pre( - {"id": "pre-form-test"}, idom.html.span("this", idom.html.span("is"), "some"), "pre-formated", " text", + id="pre-form-test", ) await display.show(PreFormated) diff --git a/tests/test_core/test_events.py b/tests/test_core/test_events.py index 89f1dfa4c..3eee82e6c 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 idom.html.input({"onKeyDown": on_key_down, "id": "input"}) + return idom.html.input(on_key_down=on_key_down, id="input") await display.show(Input) @@ -170,9 +170,9 @@ async def on_click(event): set_clicked(True) if not clicked: - return idom.html.button({"onClick": on_click, "id": "click"}, ["Click Me!"]) + return idom.html.button(["Click Me!"], on_click=on_click, id="click") else: - return idom.html.p({"id": "complete"}, ["Complete"]) + return idom.html.p(["Complete"], id="complete") await display.show(Button) @@ -194,26 +194,14 @@ def outer_click_is_not_triggered(event): assert False outer = idom.html.div( - { - "style": { - "height": "35px", - "width": "35px", - "backgroundColor": "red", - }, - "onClick": outer_click_is_not_triggered, - "id": "outer", - }, idom.html.div( - { - "style": { - "height": "30px", - "width": "30px", - "backgroundColor": "blue", - }, - "onClick": inner_click_no_op, - "id": "inner", - }, + style={"height": "30px", "width": "30px", "backgroundColor": "blue"}, + on_click=inner_click_no_op, + id="inner", ), + style={"height": "35px", "width": "35px", "backgroundColor": "red"}, + on_click=outer_click_is_not_triggered, + id="outer", ) return outer diff --git a/tests/test_core/test_hooks.py b/tests/test_core/test_hooks.py index 293e773a2..9654eaddb 100644 --- a/tests/test_core/test_hooks.py +++ b/tests/test_core/test_hooks.py @@ -181,18 +181,14 @@ def TestComponent(): render_count.current += 1 return idom.html.div( idom.html.button( - { - "id": "r_1", - "onClick": event_count_tracker(lambda event: set_state(r_1)), - }, "r_1", + id="r_1", + on_click=event_count_tracker(lambda event: set_state(r_1)), ), idom.html.button( - { - "id": "r_2", - "onClick": event_count_tracker(lambda event: set_state(r_2)), - }, "r_2", + id="r_2", + on_click=event_count_tracker(lambda event: set_state(r_2)), ), f"Last state: {'r_1' if state is r_1 else 'r_2'}", ) @@ -237,9 +233,9 @@ async def on_change(event): set_message(event["target"]["value"]) if message is None: - return idom.html.input({"id": "input", "onChange": on_change}) + return idom.html.input(id="input", on_change=on_change) else: - return idom.html.p({"id": "complete"}, ["Complete"]) + return idom.html.p(["Complete"], id="complete") await display.show(Input) @@ -261,13 +257,9 @@ def double_set_state(event): set_state_2(state_2 + 1) return idom.html.div( - idom.html.div( - {"id": "first", "data-value": state_1}, f"value is: {state_1}" - ), - idom.html.div( - {"id": "second", "data-value": state_2}, f"value is: {state_2}" - ), - idom.html.button({"id": "button", "onClick": double_set_state}, "click me"), + idom.html.div(f"value is: {state_1}", id="first", data_value=state_1), + idom.html.div(f"value is: {state_2}", id="second", data_value=state_2), + idom.html.button("click me", id="button", on_click=double_set_state), ) await display.show(SomeComponent) diff --git a/tests/test_core/test_layout.py b/tests/test_core/test_layout.py index 805ff5653..f69367586 100644 --- a/tests/test_core/test_layout.py +++ b/tests/test_core/test_layout.py @@ -507,10 +507,8 @@ def bad_trigger(): raise ValueError("Called bad trigger") children = [ - idom.html.button( - {"onClick": good_trigger, "id": "good"}, "good", key="good" - ), - idom.html.button({"onClick": bad_trigger, "id": "bad"}, "bad", key="bad"), + idom.html.button("good", key="good", on_click=good_trigger, id="good"), + idom.html.button("bad", key="bad", on_click=bad_trigger, id="bad"), ] if reverse_children: @@ -567,7 +565,7 @@ def callback(): def callback(): raise ValueError("Called bad trigger") - return idom.html.button({"onClick": callback, "id": "good"}, "good") + return idom.html.button("good", on_click=callback, id="good") async with idom.Layout(RootComponent()) as layout: await layout.render() @@ -649,8 +647,8 @@ def HasEventHandlerAtRoot(): value, set_value = idom.hooks.use_state(False) set_value(not value) # trigger renders forever event_handler.current = weakref(set_value) - button = idom.html.button({"onClick": set_value}, "state is: ", value) - event_handler.current = weakref(button["eventHandlers"]["onClick"].function) + button = idom.html.button("state is: ", value, on_click=set_value) + event_handler.current = weakref(button["eventHandlers"]["on_click"].function) return button async with idom.Layout(HasEventHandlerAtRoot()) as layout: @@ -671,8 +669,8 @@ def HasNestedEventHandler(): value, set_value = idom.hooks.use_state(False) set_value(not value) # trigger renders forever event_handler.current = weakref(set_value) - button = idom.html.button({"onClick": set_value}, "state is: ", value) - event_handler.current = weakref(button["eventHandlers"]["onClick"].function) + button = idom.html.button("state is: ", value, on_click=set_value) + event_handler.current = weakref(button["eventHandlers"]["on_click"].function) return idom.html.div(idom.html.div(button)) async with idom.Layout(HasNestedEventHandler()) as layout: @@ -753,7 +751,7 @@ def ComponentWithBadEventHandler(): def raise_error(): raise Exception("bad event handler") - return idom.html.button({"onClick": raise_error}) + return idom.html.button(on_click=raise_error) with assert_idom_did_log(match_error="bad event handler"): @@ -850,7 +848,7 @@ def SomeComponent(): return idom.html.div( [ idom.html.div( - idom.html.input({"onChange": lambda event: None}), + idom.html.input(on_change=lambda event: None), key=str(i), ) for i in items @@ -909,14 +907,14 @@ def Root(): toggle, toggle_type.current = use_toggle(True) handler = element_static_handler.use(lambda: None) if toggle: - return html.div(html.button({"onEvent": handler})) + return html.div(html.button(on_event=handler)) else: return html.div(SomeComponent()) @idom.component def SomeComponent(): handler = component_static_handler.use(lambda: None) - return html.button({"onAnotherEvent": handler}) + return html.button(on_another_event=handler) async with idom.Layout(Root()) as layout: await layout.render() @@ -999,8 +997,7 @@ def Parent(): state, set_state = use_state(0) return html.div( html.button( - {"onClick": set_child_key_num.use(lambda: set_state(state + 1))}, - "click me", + "click me", on_click=set_child_key_num.use(lambda: set_state(state + 1)) ), Child("some-key"), Child(f"key-{state}"), @@ -1073,7 +1070,7 @@ async def test_changing_event_handlers_in_the_next_render(): def Root(): event_name, set_event_name.current = use_state("first") return html.button( - {event_name: event_handler.use(lambda: did_trigger.set_current(True))} + **{event_name: event_handler.use(lambda: did_trigger.set_current(True))} ) async with Layout(Root()) as layout: diff --git a/tests/test_core/test_serve.py b/tests/test_core/test_serve.py index 20eb9fa86..94b403ebc 100644 --- a/tests/test_core/test_serve.py +++ b/tests/test_core/test_serve.py @@ -11,7 +11,7 @@ from tests.tooling.common import event_message -EVENT_NAME = "onEvent" +EVENT_NAME = "on_event" STATIC_EVENT_HANDLER = StaticEventHandler() @@ -85,7 +85,7 @@ def Counter(): initial_value=0, ) handler = STATIC_EVENT_HANDLER.use(lambda: change_count(1)) - return idom.html.div({EVENT_NAME: handler, "count": count}) + return idom.html.div(**{EVENT_NAME: handler}, count=count) async def test_dispatch(): @@ -115,8 +115,8 @@ async def handle_event(): second_event_did_execute.set() return idom.html.div( - idom.html.button({"onClick": block_forever}), - idom.html.button({"onClick": handle_event}), + idom.html.button(on_click=block_forever), + idom.html.button(on_click=handle_event), ) send_queue = asyncio.Queue() diff --git a/tests/test_core/test_vdom.py b/tests/test_core/test_vdom.py index aeca2b4ea..f9f99e5a8 100644 --- a/tests/test_core/test_vdom.py +++ b/tests/test_core/test_vdom.py @@ -7,11 +7,16 @@ from idom.config import IDOM_DEBUG_MODE from idom.core.events import EventHandler from idom.core.types import VdomDict -from idom.core.vdom import is_vdom, make_vdom_constructor, validate_vdom_json +from idom.core.vdom import ( + is_vdom, + make_vdom_constructor, + validate_vdom_json, + with_import_source, +) FAKE_EVENT_HANDLER = EventHandler(lambda data: None) -FAKE_EVENT_HANDLER_DICT = {"onEvent": FAKE_EVENT_HANDLER} +FAKE_EVENT_HANDLER_DICT = {"on_event": FAKE_EVENT_HANDLER} @pytest.mark.parametrize( @@ -36,7 +41,7 @@ def test_is_vdom(result, value): {"tagName": "div", "children": [{"tagName": "div"}]}, ), ( - idom.vdom("div", {"style": {"backgroundColor": "red"}}), + idom.vdom("div", style={"backgroundColor": "red"}), {"tagName": "div", "attributes": {"style": {"backgroundColor": "red"}}}, ), ( @@ -48,11 +53,7 @@ def test_is_vdom(result, value): }, ), ( - idom.vdom("div", event_handlers=FAKE_EVENT_HANDLER_DICT), - {"tagName": "div", "eventHandlers": FAKE_EVENT_HANDLER_DICT}, - ), - ( - idom.vdom("div", {"onEvent": FAKE_EVENT_HANDLER}), + idom.vdom("div", on_event=FAKE_EVENT_HANDLER), {"tagName": "div", "eventHandlers": FAKE_EVENT_HANDLER_DICT}, ), ( @@ -78,9 +79,9 @@ def test_is_vdom(result, value): {"tagName": "div", "children": [1, 4, 9]}, ), ( - idom.vdom( - "MyComponent", - import_source={"source": "./some-script.js", "fallback": "loading..."}, + with_import_source( + idom.vdom("MyComponent"), + {"source": "./some-script.js", "fallback": "loading..."}, ), { "tagName": "MyComponent", @@ -99,44 +100,23 @@ def test_simple_node_construction(actual, expected): async def test_callable_attributes_are_cast_to_event_handlers(): params_from_calls = [] - node = idom.vdom("div", {"onEvent": lambda *args: params_from_calls.append(args)}) + node = idom.vdom("div", on_event=lambda *args: params_from_calls.append(args)) event_handlers = node.pop("eventHandlers") assert node == {"tagName": "div"} - handler = event_handlers["onEvent"] - assert event_handlers == {"onEvent": EventHandler(handler.function)} + handler = event_handlers["on_event"] + assert event_handlers == {"on_event": EventHandler(handler.function)} await handler.function([1, 2]) await handler.function([3, 4, 5]) assert params_from_calls == [(1, 2), (3, 4, 5)] -async def test_event_handlers_and_callable_attributes_are_automatically_merged(): - calls = [] - - node = idom.vdom( - "div", - {"onEvent": lambda: calls.append("callable_attr")}, - event_handlers={ - "onEvent": EventHandler(lambda data: calls.append("normal_event_handler")) - }, - ) - - event_handlers = node.pop("eventHandlers") - assert node == {"tagName": "div"} - - handler = event_handlers["onEvent"] - assert event_handlers == {"onEvent": EventHandler(handler.function)} - - await handler.function([]) - assert calls == ["normal_event_handler", "callable_attr"] - - def test_make_vdom_constructor(): elmt = make_vdom_constructor("some-tag") - assert elmt({"data": 1}, [elmt()]) == { + assert elmt(elmt(), data=1) == { "tagName": "some-tag", "children": [{"tagName": "some-tag"}], "attributes": {"data": 1}, diff --git a/tests/test_html.py b/tests/test_html.py index 794a16474..b296447a3 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -13,7 +13,7 @@ async def test_script_mount_unmount(display: DisplayFixture): def Root(): is_mounted, toggle_is_mounted.current = use_toggle(True) return html.div( - html.div({"id": "mount-state", "data-value": False}), + html.div(id="mount-state", data_value=False), HasScript() if is_mounted else html.div(), ) @@ -53,8 +53,8 @@ async def test_script_re_run_on_content_change(display: DisplayFixture): def HasScript(): count, incr_count.current = use_counter(1) return html.div( - html.div({"id": "mount-count", "data-value": 0}), - html.div({"id": "unmount-count", "data-value": 0}), + html.div(id="mount-count", data_value=0), + html.div(id="unmount-count", data_value=0), html.script( f"""() => {{ const mountCountEl = document.getElementById("mount-count"); @@ -101,11 +101,9 @@ def HasScript(): return html.div() else: return html.div( - html.div({"id": "run-count", "data-value": 0}), + html.div(id="run-count", data_value=0), html.script( - { - "src": f"/_idom/modules/{file_name_template.format(src_id=src_id)}" - } + src=f"/_idom/modules/{file_name_template.format(src_id=src_id)}" ), ) @@ -151,5 +149,5 @@ def test_simple_fragment(): def test_fragment_can_have_no_attributes(): - with pytest.raises(TypeError, match="Fragments cannot have attributes"): - html._({"some-attribute": 1}) + with pytest.raises(TypeError): + html._(some_attribute=1) diff --git a/tests/test_testing.py b/tests/test_testing.py index 27afa980a..9b4941d4c 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -181,7 +181,7 @@ def make_next_count_constructor(count): def constructor(): count.current += 1 - return html.div({"id": f"hotswap-{count.current}"}, count.current) + return html.div(count.current, id=f"hotswap-{count.current}") return constructor @@ -192,7 +192,7 @@ def ButtonSwapsDivs(): async def on_click(event): mount(make_next_count_constructor(count)) - incr = html.button({"onClick": on_click, "id": "incr-button"}, "incr") + incr = html.button("incr", on_click=on_click, id="incr-button") mount, make_hostswap = _hotswap(update_on_change=True) mount(make_next_count_constructor(count)) diff --git a/tests/test_utils.py b/tests/test_utils.py index f7518b014..3fc5ef2f0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -46,12 +46,20 @@ def test_ref_repr(): @pytest.mark.parametrize( "case", [ - {"source": "
", "model": {"tagName": "div"}}, + { + "source": "
", + "model": {"tagName": "div"}, + }, + { + "source": "
", + # we don't touch attribute values + "model": {"tagName": "div", "attributes": {"some-attribute": "thing"}}, + }, { "source": "
", "model": { "tagName": "div", - "attributes": {"style": {"backgroundColor": "blue"}}, + "attributes": {"style": {"background_color": "blue"}}, }, }, { @@ -137,7 +145,7 @@ def test_html_to_vdom_with_style_attr(): source = '

Hello World.

' expected = { - "attributes": {"style": {"backgroundColor": "green", "color": "red"}}, + "attributes": {"style": {"background_color": "green", "color": "red"}}, "children": ["Hello World."], "tagName": "p", } @@ -205,17 +213,15 @@ def test_del_html_body_transform(): f"
{html_escape(str(SOME_OBJECT))}
", ), ( - html.div({"someAttribute": SOME_OBJECT}), + html.div(some_attribute=SOME_OBJECT), f'
', ), ( - html.div( - "hello", html.a({"href": "https://example.com"}, "example"), "world" - ), + html.div("hello", html.a("example", href="https://example.com"), "world"), '
helloexampleworld
', ), ( - html.button({"onClick": lambda event: None}), + html.button(on_click=lambda event: None), "", ), ( @@ -227,17 +233,17 @@ def test_del_html_body_transform(): "
hello
world", ), ( - html.div({"style": {"backgroundColor": "blue", "marginLeft": "10px"}}), + html.div(style={"background_color": "blue", "margin_left": "10px"}), '
', ), ( - html.div({"style": "background-color:blue;margin-left:10px"}), + html.div(style="background-color:blue;margin-left:10px"), '
', ), ( html._( html.div("hello"), - html.a({"href": "https://example.com"}, "example"), + html.a("example", href="https://example.com"), ), '
hello
example', ), @@ -245,16 +251,14 @@ def test_del_html_body_transform(): html.div( html._( html.div("hello"), - html.a({"href": "https://example.com"}, "example"), + html.a("example", href="https://example.com"), ), html.button(), ), '
hello
example
', ), ( - html.div( - {"dataSomething": 1, "dataSomethingElse": 2, "dataisnotdashed": 3} - ), + html.div(data_something=1, data_something_else=2, dataisnotdashed=3), '
', ), ], diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index 497b89787..1131ea87c 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -32,7 +32,7 @@ async def test_that_js_module_unmount_is_called(display: DisplayFixture): @idom.component def ShowCurrentComponent(): current_component, set_current_component.current = idom.hooks.use_state( - lambda: SomeComponent({"id": "some-component", "text": "initial component"}) + lambda: SomeComponent(id="some-component", text="initial component") ) return current_component @@ -41,7 +41,7 @@ def ShowCurrentComponent(): await display.page.wait_for_selector("#some-component", state="attached") set_current_component.current( - idom.html.h1({"id": "some-other-component"}, "some other component") + idom.html.h1("some other component", id="some-other-component") ) # the new component has been displayed @@ -68,7 +68,7 @@ async def test_module_from_url(browser): @idom.component def ShowSimpleButton(): - return SimpleButton({"id": "my-button"}) + return SimpleButton(id="my-button") async with BackendFixture(app=app, implementation=sanic_implementation) as server: async with DisplayFixture(server, browser) as display: @@ -105,7 +105,8 @@ async def test_module_from_file(display: DisplayFixture): @idom.component def ShowSimpleButton(): return SimpleButton( - {"id": "my-button", "onClick": lambda event: is_clicked.set_current(True)} + id="my-button", + on_click=lambda event: is_clicked.set_current(True), ) await display.show(ShowSimpleButton) @@ -198,11 +199,11 @@ async def test_module_exports_multiple_components(display: DisplayFixture): ["Header1", "Header2"], ) - await display.show(lambda: Header1({"id": "my-h1"}, "My Header 1")) + await display.show(lambda: Header1("My Header 1", id="my-h1")) await display.page.wait_for_selector("#my-h1", state="attached") - await display.show(lambda: Header2({"id": "my-h2"}, "My Header 2")) + await display.show(lambda: Header2("My Header 2", id="my-h2")) await display.page.wait_for_selector("#my-h2", state="attached") @@ -215,9 +216,9 @@ async def test_imported_components_can_render_children(display: DisplayFixture): await display.show( lambda: Parent( - Child({"index": 1}), - Child({"index": 2}), - Child({"index": 3}), + Child(index=1), + Child(index=2), + Child(index=3), ) ) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index cd6f9b2c2..dd5aa21ab 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -19,14 +19,14 @@ async def test_image_from_string(display: DisplayFixture): src = IMAGE_SRC_BYTES.decode() - await display.show(lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"})) + await display.show(lambda: idom.widgets.image("svg", src, id="a-circle-1")) client_img = await display.page.wait_for_selector("#a-circle-1") assert BASE64_IMAGE_SRC in (await client_img.get_attribute("src")) async def test_image_from_bytes(display: DisplayFixture): src = IMAGE_SRC_BYTES - await display.show(lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"})) + await display.show(lambda: idom.widgets.image("svg", src, id="a-circle-1")) client_img = await display.page.wait_for_selector("#a-circle-1") assert BASE64_IMAGE_SRC in (await client_img.get_attribute("src"))