From d8d0fd2eb8412d0f58b372b0ca05fc32acbdadc4 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 7 Dec 2025 02:11:35 -0800 Subject: [PATCH 1/7] Rename `reactpy.web.export` --- src/build_scripts/copy_dir.py | 1 - src/reactpy/web/__init__.py | 2 ++ src/reactpy/web/module.py | 39 ++++++++++++++++++++++++++++++++--- tests/conftest.py | 6 +++--- tests/test_web/test_module.py | 30 +++++++++++++++++---------- 5 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/build_scripts/copy_dir.py b/src/build_scripts/copy_dir.py index 34c87bf4d..6f0ffc686 100644 --- a/src/build_scripts/copy_dir.py +++ b/src/build_scripts/copy_dir.py @@ -3,7 +3,6 @@ # dependencies = [] # /// -# ruff: noqa: INP001 import logging import shutil import sys diff --git a/src/reactpy/web/__init__.py b/src/reactpy/web/__init__.py index f27d58ff9..aeb7c4b12 100644 --- a/src/reactpy/web/__init__.py +++ b/src/reactpy/web/__init__.py @@ -1,5 +1,6 @@ from reactpy.web.module import ( export, + import_components, module_from_file, module_from_string, module_from_url, @@ -7,6 +8,7 @@ __all__ = [ "export", + "import_components", "module_from_file", "module_from_string", "module_from_url", diff --git a/src/reactpy/web/module.py b/src/reactpy/web/module.py index bd35f92cb..4dd6e031e 100644 --- a/src/reactpy/web/module.py +++ b/src/reactpy/web/module.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, NewType, overload +from reactpy._warnings import warn from reactpy.config import REACTPY_DEBUG, REACTPY_WEB_MODULES_DIR from reactpy.core.vdom import Vdom from reactpy.types import ImportSourceDict, VdomConstructor @@ -222,7 +223,7 @@ class WebModule: @overload -def export( +def import_components( web_module: WebModule, export_names: str, fallback: Any | None = ..., @@ -231,7 +232,7 @@ def export( @overload -def export( +def import_components( web_module: WebModule, export_names: list[str] | tuple[str, ...], fallback: Any | None = ..., @@ -239,7 +240,7 @@ def export( ) -> list[VdomConstructor]: ... -def export( +def import_components( web_module: WebModule, export_names: str | list[str] | tuple[str, ...], fallback: Any | None = None, @@ -281,6 +282,38 @@ def export( ] +@overload +def export( + web_module: WebModule, + export_names: str, + fallback: Any | None = ..., + allow_children: bool = ..., +) -> VdomConstructor: ... + + +@overload +def export( + web_module: WebModule, + export_names: list[str] | tuple[str, ...], + fallback: Any | None = ..., + allow_children: bool = ..., +) -> list[VdomConstructor]: ... + + +def export( + web_module: WebModule, + export_names: str | list[str] | tuple[str, ...], + fallback: Any | None = None, + allow_children: bool = True, +) -> VdomConstructor | list[VdomConstructor]: + warn( + "The 'export' function is deprecated and will be removed in a future release. " + "Use 'import_components' instead.", + DeprecationWarning, + ) + return import_components(web_module, export_names, fallback, allow_children) + + def _make_export( web_module: WebModule, name: str, diff --git a/tests/conftest.py b/tests/conftest.py index 368078e74..4fb087774 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,8 +36,8 @@ def pytest_addoption(parser: Parser) -> None: @pytest.fixture(autouse=True, scope="session") def install_playwright(): - subprocess.run(["playwright", "install", "chromium"], check=True) # noqa: S607, S603 - subprocess.run(["playwright", "install-deps"], check=True) # noqa: S607, S603 + subprocess.run(["playwright", "install", "chromium"], check=True) # noqa: S607 + subprocess.run(["playwright", "install-deps"], check=True) # noqa: S607 @pytest.fixture(autouse=True, scope="session") @@ -49,7 +49,7 @@ def rebuild(): # passed to the subprocess. env = os.environ.copy() env.pop("HATCH_ENV_ACTIVE", None) - subprocess.run(["hatch", "build", "-t", "wheel"], check=True, env=env) # noqa: S607, S603 + subprocess.run(["hatch", "build", "-t", "wheel"], check=True, env=env) # noqa: S607 @pytest.fixture(autouse=True, scope="function") diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index d233396fc..a836cb2fb 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -19,7 +19,7 @@ async def test_that_js_module_unmount_is_called(display: DisplayFixture): - SomeComponent = reactpy.web.export( + SomeComponent = reactpy.web.import_components( reactpy.web.module_from_file( "set-flag-when-unmount-is-called", JS_FIXTURES_DIR / "set-flag-when-unmount-is-called.js", @@ -52,7 +52,7 @@ def ShowCurrentComponent(): async def test_module_from_url(browser): - SimpleButton = reactpy.web.export( + SimpleButton = reactpy.web.import_components( reactpy.web.module_from_url("/static/simple-button.js", resolve_exports=False), "SimpleButton", ) @@ -72,7 +72,7 @@ def ShowSimpleButton(): async def test_module_from_file(display: DisplayFixture): - SimpleButton = reactpy.web.export( + SimpleButton = reactpy.web.import_components( reactpy.web.module_from_file( "simple-button", JS_FIXTURES_DIR / "simple-button.js" ), @@ -163,14 +163,14 @@ def test_module_missing_exports(): module = WebModule("test", NAME_SOURCE, None, {"a", "b", "c"}, None, False) with pytest.raises(ValueError, match="does not export 'x'"): - reactpy.web.export(module, "x") + reactpy.web.import_components(module, "x") with pytest.raises(ValueError, match=r"does not export \['x', 'y'\]"): - reactpy.web.export(module, ["x", "y"]) + reactpy.web.import_components(module, ["x", "y"]) async def test_module_exports_multiple_components(display: DisplayFixture): - Header1, Header2 = reactpy.web.export( + Header1, Header2 = reactpy.web.import_components( reactpy.web.module_from_file( "exports-two-components", JS_FIXTURES_DIR / "exports-two-components.js" ), @@ -190,7 +190,7 @@ async def test_imported_components_can_render_children(display: DisplayFixture): module = reactpy.web.module_from_file( "component-can-have-child", JS_FIXTURES_DIR / "component-can-have-child.js" ) - Parent, Child = reactpy.web.export(module, ["Parent", "Child"]) + Parent, Child = reactpy.web.import_components(module, ["Parent", "Child"]) await display.show( lambda: Parent( @@ -222,7 +222,7 @@ async def test_keys_properly_propagated(display: DisplayFixture): module = reactpy.web.module_from_file( "keys-properly-propagated", JS_FIXTURES_DIR / "keys-properly-propagated.js" ) - GridLayout = reactpy.web.export(module, "GridLayout") + GridLayout = reactpy.web.import_components(module, "GridLayout") await display.show( lambda: GridLayout( @@ -277,7 +277,7 @@ async def test_subcomponent_notation_as_str_attrs(display: DisplayFixture): "subcomponent-notation", JS_FIXTURES_DIR / "subcomponent-notation.js", ) - InputGroup, InputGroupText, FormControl, FormLabel = reactpy.web.export( + InputGroup, InputGroupText, FormControl, FormLabel = reactpy.web.import_components( module, ["InputGroup", "InputGroup.Text", "Form.Control", "Form.Label"] ) @@ -337,7 +337,7 @@ async def test_subcomponent_notation_as_obj_attrs(display: DisplayFixture): "subcomponent-notation", JS_FIXTURES_DIR / "subcomponent-notation.js", ) - InputGroup, Form = reactpy.web.export(module, ["InputGroup", "Form"]) + InputGroup, Form = reactpy.web.import_components(module, ["InputGroup", "Form"]) content = reactpy.html.div( {"id": "the-parent"}, @@ -394,7 +394,7 @@ async def test_callable_prop_with_javacript(display: DisplayFixture): module = reactpy.web.module_from_file( "callable-prop", JS_FIXTURES_DIR / "callable-prop.js" ) - Component = reactpy.web.export(module, "Component") + Component = reactpy.web.import_components(module, "Component") @reactpy.component def App(): @@ -415,3 +415,11 @@ def test_module_from_string(): reactpy.web.module_from_string("temp", "old") with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): reactpy.web.module_from_string("temp", "new") + + +def test_deprecated_export(): + module = reactpy.web.module_from_string( + "temp", "export function Component() { return 'hello' }" + ) + with pytest.warns(DeprecationWarning, match="The 'export' function is deprecated"): + reactpy.web.export(module, "Component") From a34983196923e845a97e245c5d8696067c05a223 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 7 Dec 2025 02:54:07 -0800 Subject: [PATCH 2/7] Revert "Rename `reactpy.web.export`" This reverts commit d8d0fd2eb8412d0f58b372b0ca05fc32acbdadc4. --- src/build_scripts/copy_dir.py | 1 + src/reactpy/web/__init__.py | 2 -- src/reactpy/web/module.py | 39 +++-------------------------------- tests/conftest.py | 6 +++--- tests/test_web/test_module.py | 30 ++++++++++----------------- 5 files changed, 18 insertions(+), 60 deletions(-) diff --git a/src/build_scripts/copy_dir.py b/src/build_scripts/copy_dir.py index 6f0ffc686..34c87bf4d 100644 --- a/src/build_scripts/copy_dir.py +++ b/src/build_scripts/copy_dir.py @@ -3,6 +3,7 @@ # dependencies = [] # /// +# ruff: noqa: INP001 import logging import shutil import sys diff --git a/src/reactpy/web/__init__.py b/src/reactpy/web/__init__.py index aeb7c4b12..f27d58ff9 100644 --- a/src/reactpy/web/__init__.py +++ b/src/reactpy/web/__init__.py @@ -1,6 +1,5 @@ from reactpy.web.module import ( export, - import_components, module_from_file, module_from_string, module_from_url, @@ -8,7 +7,6 @@ __all__ = [ "export", - "import_components", "module_from_file", "module_from_string", "module_from_url", diff --git a/src/reactpy/web/module.py b/src/reactpy/web/module.py index 4dd6e031e..bd35f92cb 100644 --- a/src/reactpy/web/module.py +++ b/src/reactpy/web/module.py @@ -7,7 +7,6 @@ from pathlib import Path from typing import Any, NewType, overload -from reactpy._warnings import warn from reactpy.config import REACTPY_DEBUG, REACTPY_WEB_MODULES_DIR from reactpy.core.vdom import Vdom from reactpy.types import ImportSourceDict, VdomConstructor @@ -223,7 +222,7 @@ class WebModule: @overload -def import_components( +def export( web_module: WebModule, export_names: str, fallback: Any | None = ..., @@ -232,7 +231,7 @@ def import_components( @overload -def import_components( +def export( web_module: WebModule, export_names: list[str] | tuple[str, ...], fallback: Any | None = ..., @@ -240,7 +239,7 @@ def import_components( ) -> list[VdomConstructor]: ... -def import_components( +def export( web_module: WebModule, export_names: str | list[str] | tuple[str, ...], fallback: Any | None = None, @@ -282,38 +281,6 @@ def import_components( ] -@overload -def export( - web_module: WebModule, - export_names: str, - fallback: Any | None = ..., - allow_children: bool = ..., -) -> VdomConstructor: ... - - -@overload -def export( - web_module: WebModule, - export_names: list[str] | tuple[str, ...], - fallback: Any | None = ..., - allow_children: bool = ..., -) -> list[VdomConstructor]: ... - - -def export( - web_module: WebModule, - export_names: str | list[str] | tuple[str, ...], - fallback: Any | None = None, - allow_children: bool = True, -) -> VdomConstructor | list[VdomConstructor]: - warn( - "The 'export' function is deprecated and will be removed in a future release. " - "Use 'import_components' instead.", - DeprecationWarning, - ) - return import_components(web_module, export_names, fallback, allow_children) - - def _make_export( web_module: WebModule, name: str, diff --git a/tests/conftest.py b/tests/conftest.py index 4fb087774..368078e74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,8 +36,8 @@ def pytest_addoption(parser: Parser) -> None: @pytest.fixture(autouse=True, scope="session") def install_playwright(): - subprocess.run(["playwright", "install", "chromium"], check=True) # noqa: S607 - subprocess.run(["playwright", "install-deps"], check=True) # noqa: S607 + subprocess.run(["playwright", "install", "chromium"], check=True) # noqa: S607, S603 + subprocess.run(["playwright", "install-deps"], check=True) # noqa: S607, S603 @pytest.fixture(autouse=True, scope="session") @@ -49,7 +49,7 @@ def rebuild(): # passed to the subprocess. env = os.environ.copy() env.pop("HATCH_ENV_ACTIVE", None) - subprocess.run(["hatch", "build", "-t", "wheel"], check=True, env=env) # noqa: S607 + subprocess.run(["hatch", "build", "-t", "wheel"], check=True, env=env) # noqa: S607, S603 @pytest.fixture(autouse=True, scope="function") diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index a836cb2fb..d233396fc 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -19,7 +19,7 @@ async def test_that_js_module_unmount_is_called(display: DisplayFixture): - SomeComponent = reactpy.web.import_components( + SomeComponent = reactpy.web.export( reactpy.web.module_from_file( "set-flag-when-unmount-is-called", JS_FIXTURES_DIR / "set-flag-when-unmount-is-called.js", @@ -52,7 +52,7 @@ def ShowCurrentComponent(): async def test_module_from_url(browser): - SimpleButton = reactpy.web.import_components( + SimpleButton = reactpy.web.export( reactpy.web.module_from_url("/static/simple-button.js", resolve_exports=False), "SimpleButton", ) @@ -72,7 +72,7 @@ def ShowSimpleButton(): async def test_module_from_file(display: DisplayFixture): - SimpleButton = reactpy.web.import_components( + SimpleButton = reactpy.web.export( reactpy.web.module_from_file( "simple-button", JS_FIXTURES_DIR / "simple-button.js" ), @@ -163,14 +163,14 @@ def test_module_missing_exports(): module = WebModule("test", NAME_SOURCE, None, {"a", "b", "c"}, None, False) with pytest.raises(ValueError, match="does not export 'x'"): - reactpy.web.import_components(module, "x") + reactpy.web.export(module, "x") with pytest.raises(ValueError, match=r"does not export \['x', 'y'\]"): - reactpy.web.import_components(module, ["x", "y"]) + reactpy.web.export(module, ["x", "y"]) async def test_module_exports_multiple_components(display: DisplayFixture): - Header1, Header2 = reactpy.web.import_components( + Header1, Header2 = reactpy.web.export( reactpy.web.module_from_file( "exports-two-components", JS_FIXTURES_DIR / "exports-two-components.js" ), @@ -190,7 +190,7 @@ async def test_imported_components_can_render_children(display: DisplayFixture): module = reactpy.web.module_from_file( "component-can-have-child", JS_FIXTURES_DIR / "component-can-have-child.js" ) - Parent, Child = reactpy.web.import_components(module, ["Parent", "Child"]) + Parent, Child = reactpy.web.export(module, ["Parent", "Child"]) await display.show( lambda: Parent( @@ -222,7 +222,7 @@ async def test_keys_properly_propagated(display: DisplayFixture): module = reactpy.web.module_from_file( "keys-properly-propagated", JS_FIXTURES_DIR / "keys-properly-propagated.js" ) - GridLayout = reactpy.web.import_components(module, "GridLayout") + GridLayout = reactpy.web.export(module, "GridLayout") await display.show( lambda: GridLayout( @@ -277,7 +277,7 @@ async def test_subcomponent_notation_as_str_attrs(display: DisplayFixture): "subcomponent-notation", JS_FIXTURES_DIR / "subcomponent-notation.js", ) - InputGroup, InputGroupText, FormControl, FormLabel = reactpy.web.import_components( + InputGroup, InputGroupText, FormControl, FormLabel = reactpy.web.export( module, ["InputGroup", "InputGroup.Text", "Form.Control", "Form.Label"] ) @@ -337,7 +337,7 @@ async def test_subcomponent_notation_as_obj_attrs(display: DisplayFixture): "subcomponent-notation", JS_FIXTURES_DIR / "subcomponent-notation.js", ) - InputGroup, Form = reactpy.web.import_components(module, ["InputGroup", "Form"]) + InputGroup, Form = reactpy.web.export(module, ["InputGroup", "Form"]) content = reactpy.html.div( {"id": "the-parent"}, @@ -394,7 +394,7 @@ async def test_callable_prop_with_javacript(display: DisplayFixture): module = reactpy.web.module_from_file( "callable-prop", JS_FIXTURES_DIR / "callable-prop.js" ) - Component = reactpy.web.import_components(module, "Component") + Component = reactpy.web.export(module, "Component") @reactpy.component def App(): @@ -415,11 +415,3 @@ def test_module_from_string(): reactpy.web.module_from_string("temp", "old") with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): reactpy.web.module_from_string("temp", "new") - - -def test_deprecated_export(): - module = reactpy.web.module_from_string( - "temp", "export function Component() { return 'hello' }" - ) - with pytest.warns(DeprecationWarning, match="The 'export' function is deprecated"): - reactpy.web.export(module, "Component") From ef56ac162ca7cb889275ffdbd6bf08e01f012e0b Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 7 Dec 2025 03:20:04 -0800 Subject: [PATCH 3/7] Deprecate `export` and `module_from_*` (replaced with `import_js_from_*` --- src/reactpy/web/module.py | 239 +++++++++++++++++++++++++++++++++++++- 1 file changed, 237 insertions(+), 2 deletions(-) diff --git a/src/reactpy/web/module.py b/src/reactpy/web/module.py index bd35f92cb..0afbfec92 100644 --- a/src/reactpy/web/module.py +++ b/src/reactpy/web/module.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, NewType, overload +from reactpy._warnings import warn from reactpy.config import REACTPY_DEBUG, REACTPY_WEB_MODULES_DIR from reactpy.core.vdom import Vdom from reactpy.types import ImportSourceDict, VdomConstructor @@ -27,12 +28,233 @@ """A source loaded from a URL, usually a CDN""" +_URL_WEB_MODULE_CACHE: dict[str, WebModule] = {} +_FILE_WEB_MODULE_CACHE: dict[str, WebModule] = {} +_STRING_WEB_MODULE_CACHE: dict[str, WebModule] = {} + + +def import_js_from_url( + url: str, + export_names: str | list[str] | tuple[str, ...], + fallback: Any | None = None, + resolve_exports: bool | None = None, + resolve_exports_depth: int = 5, + unmount_before_update: bool = False, + allow_children: bool = True, +) -> VdomConstructor | list[VdomConstructor]: + """Import a component from a URL. + + Parameters: + url: + The URL to import the component from. + export_names: + One or more names to export. If given as a string, a single component + will be returned. If a list is given, then a list of components will be + returned. + fallback: + What to temporarily display while the module is being loaded. + resolve_exports: + Whether to try and find all the named exports of this module. + resolve_exports_depth: + How deeply to search for those exports. + unmount_before_update: + Cause the component to be unmounted before each update. This option should + only be used if the imported package fails to re-render when props change. + Using this option has negative performance consequences since all DOM + elements must be changed on each render. See :issue:`461` for more info. + allow_children: + Whether or not these components can have children. + """ + if url in _URL_WEB_MODULE_CACHE: + module = _URL_WEB_MODULE_CACHE[url] + else: + module = _module_from_url( + url, + fallback=fallback, + resolve_exports=resolve_exports, + resolve_exports_depth=resolve_exports_depth, + unmount_before_update=unmount_before_update, + ) + _URL_WEB_MODULE_CACHE[url] = module + return _vdom_from_web_module(module, export_names, fallback, allow_children) + + +def import_js_from_file( + name: str, + file: str | Path, + export_names: str | list[str] | tuple[str, ...], + fallback: Any | None = None, + resolve_exports: bool | None = None, + resolve_exports_depth: int = 5, + unmount_before_update: bool = False, + symlink: bool = False, + allow_children: bool = True, +) -> VdomConstructor | list[VdomConstructor]: + """Import a component from a file. + + Parameters: + name: + The name of the package + file: + The file from which the content of the web module will be created. + export_names: + One or more names to export. If given as a string, a single component + will be returned. If a list is given, then a list of components will be + returned. + fallback: + What to temporarily display while the module is being loaded. + resolve_exports: + Whether to try and find all the named exports of this module. + resolve_exports_depth: + How deeply to search for those exports. + unmount_before_update: + Cause the component to be unmounted before each update. This option should + only be used if the imported package fails to re-render when props change. + Using this option has negative performance consequences since all DOM + elements must be changed on each render. See :issue:`461` for more info. + symlink: + Whether the web module should be saved as a symlink to the given ``file``. + allow_children: + Whether or not these components can have children. + """ + if name in _FILE_WEB_MODULE_CACHE: + module = _FILE_WEB_MODULE_CACHE[name] + else: + module = _module_from_file( + name, + file, + fallback=fallback, + resolve_exports=resolve_exports, + resolve_exports_depth=resolve_exports_depth, + unmount_before_update=unmount_before_update, + symlink=symlink, + ) + _FILE_WEB_MODULE_CACHE[name] = module + return _vdom_from_web_module(module, export_names, fallback, allow_children) + + +def import_js_from_string( + name: str, + content: str, + export_names: str | list[str] | tuple[str, ...], + fallback: Any | None = None, + resolve_exports: bool | None = None, + resolve_exports_depth: int = 5, + unmount_before_update: bool = False, + allow_children: bool = True, +) -> VdomConstructor | list[VdomConstructor]: + """Import a component from a string. + + Parameters: + name: + The name of the package + content: + The contents of the web module + export_names: + One or more names to export. If given as a string, a single component + will be returned. If a list is given, then a list of components will be + returned. + fallback: + What to temporarily display while the module is being loaded. + resolve_exports: + Whether to try and find all the named exports of this module. + resolve_exports_depth: + How deeply to search for those exports. + unmount_before_update: + Cause the component to be unmounted before each update. This option should + only be used if the imported package fails to re-render when props change. + Using this option has negative performance consequences since all DOM + elements must be changed on each render. See :issue:`461` for more info. + allow_children: + Whether or not these components can have children. + """ + if name in _STRING_WEB_MODULE_CACHE: + module = _STRING_WEB_MODULE_CACHE[name] + else: + module = _module_from_string( + name, + content, + fallback=fallback, + resolve_exports=resolve_exports, + resolve_exports_depth=resolve_exports_depth, + unmount_before_update=unmount_before_update, + ) + _STRING_WEB_MODULE_CACHE[name] = module + return _vdom_from_web_module(module, export_names, fallback, allow_children) + + def module_from_url( url: str, fallback: Any | None = None, resolve_exports: bool | None = None, resolve_exports_depth: int = 5, unmount_before_update: bool = False, +) -> WebModule: + warn( + "module_from_url is deprecated, use import_js_from_url instead", + DeprecationWarning, + ) + return _module_from_url( + url, + fallback=fallback, + resolve_exports=resolve_exports, + resolve_exports_depth=resolve_exports_depth, + unmount_before_update=unmount_before_update, + ) + + +def module_from_file( + name: str, + file: str | Path, + fallback: Any | None = None, + resolve_exports: bool | None = None, + resolve_exports_depth: int = 5, + unmount_before_update: bool = False, + symlink: bool = False, +) -> WebModule: + warn( + "module_from_file is deprecated, use import_js_from_file instead", + DeprecationWarning, + ) + return _module_from_file( + name, + file, + fallback=fallback, + resolve_exports=resolve_exports, + resolve_exports_depth=resolve_exports_depth, + unmount_before_update=unmount_before_update, + symlink=symlink, + ) + + +def module_from_string( + name: str, + content: str, + fallback: Any | None = None, + resolve_exports: bool | None = None, + resolve_exports_depth: int = 5, + unmount_before_update: bool = False, +) -> WebModule: + warn( + "module_from_string is deprecated, use import_js_from_string instead", + DeprecationWarning, + ) + return _module_from_string( + name, + content, + fallback=fallback, + resolve_exports=resolve_exports, + resolve_exports_depth=resolve_exports_depth, + unmount_before_update=unmount_before_update, + ) + + +def _module_from_url( + url: str, + fallback: Any | None = None, + resolve_exports: bool | None = None, + resolve_exports_depth: int = 5, + unmount_before_update: bool = False, ) -> WebModule: """Load a :class:`WebModule` from a :data:`URL_SOURCE` @@ -70,7 +292,7 @@ def module_from_url( ) -def module_from_file( +def _module_from_file( name: str, file: str | Path, fallback: Any | None = None, @@ -152,7 +374,7 @@ def _copy_file(target: Path, source: Path, symlink: bool) -> None: shutil.copy(source, target) -def module_from_string( +def _module_from_string( name: str, content: str, fallback: Any | None = None, @@ -244,6 +466,19 @@ def export( export_names: str | list[str] | tuple[str, ...], fallback: Any | None = None, allow_children: bool = True, +) -> VdomConstructor | list[VdomConstructor]: + warn( + "export is deprecated, use import_js_from_* functions instead", + DeprecationWarning, + ) + return _vdom_from_web_module(web_module, export_names, fallback, allow_children) + + +def _vdom_from_web_module( + web_module: WebModule, + export_names: str | list[str] | tuple[str, ...], + fallback: Any | None = None, + allow_children: bool = True, ) -> VdomConstructor | list[VdomConstructor]: """Return one or more VDOM constructors from a :class:`WebModule` From 77921563d7fc55668c0cd6259d49d78213a405d4 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 7 Dec 2025 03:31:37 -0800 Subject: [PATCH 4/7] Add tests --- src/reactpy/web/__init__.py | 6 ++ src/reactpy/web/module.py | 8 +- tests/test_web/test_module.py | 160 ++++++++++++++++++++++++++-------- 3 files changed, 136 insertions(+), 38 deletions(-) diff --git a/src/reactpy/web/__init__.py b/src/reactpy/web/__init__.py index f27d58ff9..9cff7e673 100644 --- a/src/reactpy/web/__init__.py +++ b/src/reactpy/web/__init__.py @@ -1,5 +1,8 @@ from reactpy.web.module import ( export, + import_js_from_file, + import_js_from_string, + import_js_from_url, module_from_file, module_from_string, module_from_url, @@ -7,6 +10,9 @@ __all__ = [ "export", + "import_js_from_file", + "import_js_from_string", + "import_js_from_url", "module_from_file", "module_from_string", "module_from_url", diff --git a/src/reactpy/web/module.py b/src/reactpy/web/module.py index 0afbfec92..5d187d031 100644 --- a/src/reactpy/web/module.py +++ b/src/reactpy/web/module.py @@ -189,7 +189,7 @@ def module_from_url( resolve_exports: bool | None = None, resolve_exports_depth: int = 5, unmount_before_update: bool = False, -) -> WebModule: +) -> WebModule: # pragma: no cover warn( "module_from_url is deprecated, use import_js_from_url instead", DeprecationWarning, @@ -211,7 +211,7 @@ def module_from_file( resolve_exports_depth: int = 5, unmount_before_update: bool = False, symlink: bool = False, -) -> WebModule: +) -> WebModule: # pragma: no cover warn( "module_from_file is deprecated, use import_js_from_file instead", DeprecationWarning, @@ -234,7 +234,7 @@ def module_from_string( resolve_exports: bool | None = None, resolve_exports_depth: int = 5, unmount_before_update: bool = False, -) -> WebModule: +) -> WebModule: # pragma: no cover warn( "module_from_string is deprecated, use import_js_from_string instead", DeprecationWarning, @@ -466,7 +466,7 @@ def export( export_names: str | list[str] | tuple[str, ...], fallback: Any | None = None, allow_children: bool = True, -) -> VdomConstructor | list[VdomConstructor]: +) -> VdomConstructor | list[VdomConstructor]: # pragma: no cover warn( "export is deprecated, use import_js_from_* functions instead", DeprecationWarning, diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index d233396fc..560f63e4d 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -4,6 +4,7 @@ from servestatic import ServeStaticASGI import reactpy +import reactpy.web.module from reactpy.executors.asgi.standalone import ReactPy from reactpy.testing import ( BackendFixture, @@ -19,8 +20,8 @@ async def test_that_js_module_unmount_is_called(display: DisplayFixture): - SomeComponent = reactpy.web.export( - reactpy.web.module_from_file( + SomeComponent = reactpy.web.module._vdom_from_web_module( + reactpy.web.module._module_from_file( "set-flag-when-unmount-is-called", JS_FIXTURES_DIR / "set-flag-when-unmount-is-called.js", ), @@ -52,8 +53,10 @@ def ShowCurrentComponent(): async def test_module_from_url(browser): - SimpleButton = reactpy.web.export( - reactpy.web.module_from_url("/static/simple-button.js", resolve_exports=False), + SimpleButton = reactpy.web.module._vdom_from_web_module( + reactpy.web.module._module_from_url( + "/static/simple-button.js", resolve_exports=False + ), "SimpleButton", ) @@ -72,8 +75,8 @@ def ShowSimpleButton(): async def test_module_from_file(display: DisplayFixture): - SimpleButton = reactpy.web.export( - reactpy.web.module_from_file( + SimpleButton = reactpy.web.module._vdom_from_web_module( + reactpy.web.module._module_from_file( "simple-button", JS_FIXTURES_DIR / "simple-button.js" ), "SimpleButton", @@ -98,30 +101,30 @@ def test_module_from_file_source_conflict(tmp_path): first_file = tmp_path / "first.js" with pytest.raises(FileNotFoundError, match="does not exist"): - reactpy.web.module_from_file("temp", first_file) + reactpy.web.module._module_from_file("temp", first_file) first_file.touch() - reactpy.web.module_from_file("temp", first_file) + reactpy.web.module._module_from_file("temp", first_file) second_file = tmp_path / "second.js" second_file.touch() # ok, same content - reactpy.web.module_from_file("temp", second_file) + reactpy.web.module._module_from_file("temp", second_file) third_file = tmp_path / "third.js" third_file.write_text("something-different") with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): - reactpy.web.module_from_file("temp", third_file) + reactpy.web.module._module_from_file("temp", third_file) def test_web_module_from_file_symlink(tmp_path): file = tmp_path / "temp.js" file.touch() - module = reactpy.web.module_from_file("temp", file, symlink=True) + module = reactpy.web.module._module_from_file("temp", file, symlink=True) assert module.file.resolve().read_text() == "" @@ -134,44 +137,44 @@ def test_web_module_from_file_symlink_twice(tmp_path): file_1 = tmp_path / "temp_1.js" file_1.touch() - reactpy.web.module_from_file("temp", file_1, symlink=True) + reactpy.web.module._module_from_file("temp", file_1, symlink=True) with assert_reactpy_did_not_log(r"Existing web module .* will be replaced with"): - reactpy.web.module_from_file("temp", file_1, symlink=True) + reactpy.web.module._module_from_file("temp", file_1, symlink=True) file_2 = tmp_path / "temp_2.js" file_2.write_text("something") with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): - reactpy.web.module_from_file("temp", file_2, symlink=True) + reactpy.web.module._module_from_file("temp", file_2, symlink=True) def test_web_module_from_file_replace_existing(tmp_path): file1 = tmp_path / "temp1.js" file1.touch() - reactpy.web.module_from_file("temp", file1) + reactpy.web.module._module_from_file("temp", file1) file2 = tmp_path / "temp2.js" file2.write_text("something") with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): - reactpy.web.module_from_file("temp", file2) + reactpy.web.module._module_from_file("temp", file2) def test_module_missing_exports(): module = WebModule("test", NAME_SOURCE, None, {"a", "b", "c"}, None, False) with pytest.raises(ValueError, match="does not export 'x'"): - reactpy.web.export(module, "x") + reactpy.web.module._vdom_from_web_module(module, "x") with pytest.raises(ValueError, match=r"does not export \['x', 'y'\]"): - reactpy.web.export(module, ["x", "y"]) + reactpy.web.module._vdom_from_web_module(module, ["x", "y"]) async def test_module_exports_multiple_components(display: DisplayFixture): - Header1, Header2 = reactpy.web.export( - reactpy.web.module_from_file( + Header1, Header2 = reactpy.web.module._vdom_from_web_module( + reactpy.web.module._module_from_file( "exports-two-components", JS_FIXTURES_DIR / "exports-two-components.js" ), ["Header1", "Header2"], @@ -187,10 +190,12 @@ async def test_module_exports_multiple_components(display: DisplayFixture): async def test_imported_components_can_render_children(display: DisplayFixture): - module = reactpy.web.module_from_file( + module = reactpy.web.module._module_from_file( "component-can-have-child", JS_FIXTURES_DIR / "component-can-have-child.js" ) - Parent, Child = reactpy.web.export(module, ["Parent", "Child"]) + Parent, Child = reactpy.web.module._vdom_from_web_module( + module, ["Parent", "Child"] + ) await display.show( lambda: Parent( @@ -219,10 +224,10 @@ async def test_keys_properly_propagated(display: DisplayFixture): This property is required for certain JS components, such as the GridLayout from react-grid-layout. """ - module = reactpy.web.module_from_file( + module = reactpy.web.module._module_from_file( "keys-properly-propagated", JS_FIXTURES_DIR / "keys-properly-propagated.js" ) - GridLayout = reactpy.web.export(module, "GridLayout") + GridLayout = reactpy.web.module._vdom_from_web_module(module, "GridLayout") await display.show( lambda: GridLayout( @@ -273,12 +278,14 @@ async def test_keys_properly_propagated(display: DisplayFixture): async def test_subcomponent_notation_as_str_attrs(display: DisplayFixture): - module = reactpy.web.module_from_file( + module = reactpy.web.module._module_from_file( "subcomponent-notation", JS_FIXTURES_DIR / "subcomponent-notation.js", ) - InputGroup, InputGroupText, FormControl, FormLabel = reactpy.web.export( - module, ["InputGroup", "InputGroup.Text", "Form.Control", "Form.Label"] + InputGroup, InputGroupText, FormControl, FormLabel = ( + reactpy.web.module._vdom_from_web_module( + module, ["InputGroup", "InputGroup.Text", "Form.Control", "Form.Label"] + ) ) content = reactpy.html.div( @@ -333,11 +340,13 @@ async def test_subcomponent_notation_as_str_attrs(display: DisplayFixture): async def test_subcomponent_notation_as_obj_attrs(display: DisplayFixture): - module = reactpy.web.module_from_file( + module = reactpy.web.module._module_from_file( "subcomponent-notation", JS_FIXTURES_DIR / "subcomponent-notation.js", ) - InputGroup, Form = reactpy.web.export(module, ["InputGroup", "Form"]) + InputGroup, Form = reactpy.web.module._vdom_from_web_module( + module, ["InputGroup", "Form"] + ) content = reactpy.html.div( {"id": "the-parent"}, @@ -391,10 +400,10 @@ async def test_subcomponent_notation_as_obj_attrs(display: DisplayFixture): async def test_callable_prop_with_javacript(display: DisplayFixture): - module = reactpy.web.module_from_file( + module = reactpy.web.module._module_from_file( "callable-prop", JS_FIXTURES_DIR / "callable-prop.js" ) - Component = reactpy.web.export(module, "Component") + Component = reactpy.web.module._vdom_from_web_module(module, "Component") @reactpy.component def App(): @@ -411,7 +420,90 @@ def App(): assert await my_div.inner_text() == "PREFIX TEXT: TEST 123" -def test_module_from_string(): - reactpy.web.module_from_string("temp", "old") +def test_import_js_from_string(): + reactpy.web.import_js_from_string("temp", "old", "Component", resolve_exports=False) + reactpy.web.module._STRING_WEB_MODULE_CACHE.clear() with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): - reactpy.web.module_from_string("temp", "new") + reactpy.web.import_js_from_string( + "temp", "new", "Component", resolve_exports=False + ) + + +async def test_import_js_from_url(browser): + SimpleButton = reactpy.web.import_js_from_url( + "/static/simple-button.js", "SimpleButton", resolve_exports=False + ) + + @reactpy.component + def ShowSimpleButton(): + return SimpleButton({"id": "my-button"}) + + app = ReactPy(ShowSimpleButton) + app = ServeStaticASGI(app, JS_FIXTURES_DIR, "/static/") + + async with BackendFixture(app) as server: + async with DisplayFixture(server, browser) as display: + await display.show(ShowSimpleButton) + + await display.page.wait_for_selector("#my-button") + + +async def test_import_js_from_file(display: DisplayFixture): + SimpleButton = reactpy.web.import_js_from_file( + "simple-button", JS_FIXTURES_DIR / "simple-button.js", "SimpleButton" + ) + + is_clicked = reactpy.Ref(False) + + @reactpy.component + def ShowSimpleButton(): + return SimpleButton( + {"id": "my-button", "onClick": lambda event: is_clicked.set_current(True)} + ) + + await display.show(ShowSimpleButton) + + button = await display.page.wait_for_selector("#my-button") + await button.click() + await poll(lambda: is_clicked.current).until_is(True) + + +def test_import_js_from_url_caching(): + url = "https://example.com/module.js" + reactpy.web.module._URL_WEB_MODULE_CACHE.clear() + + # First import + reactpy.web.import_js_from_url(url, "Component", resolve_exports=False) + assert url in reactpy.web.module._URL_WEB_MODULE_CACHE + module1 = reactpy.web.module._URL_WEB_MODULE_CACHE[url] + + # Second import + reactpy.web.import_js_from_url(url, "Component", resolve_exports=False) + assert reactpy.web.module._URL_WEB_MODULE_CACHE[url] is module1 + + +def test_import_js_from_file_caching(tmp_path): + file = tmp_path / "test.js" + file.write_text("export function Component() {}") + name = "test-file-module" + reactpy.web.module._FILE_WEB_MODULE_CACHE.clear() + + reactpy.web.import_js_from_file(name, file, "Component") + assert name in reactpy.web.module._FILE_WEB_MODULE_CACHE + module1 = reactpy.web.module._FILE_WEB_MODULE_CACHE[name] + + reactpy.web.import_js_from_file(name, file, "Component") + assert reactpy.web.module._FILE_WEB_MODULE_CACHE[name] is module1 + + +def test_import_js_from_string_caching(): + name = "test-string-module" + content = "export function Component() {}" + reactpy.web.module._STRING_WEB_MODULE_CACHE.clear() + + reactpy.web.import_js_from_string(name, content, "Component") + assert name in reactpy.web.module._STRING_WEB_MODULE_CACHE + module1 = reactpy.web.module._STRING_WEB_MODULE_CACHE[name] + + reactpy.web.import_js_from_string(name, content, "Component") + assert reactpy.web.module._STRING_WEB_MODULE_CACHE[name] is module1 From 0c52710a1824c0dae8e6dd9e4fe663641bbc9c33 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 7 Dec 2025 03:36:26 -0800 Subject: [PATCH 5/7] Add changelog --- docs/source/about/changelog.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index f64911892..497d44113 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -31,6 +31,9 @@ Unreleased - :pull:`1281` - Added type hints to ``reactpy.html`` attributes. - :pull:`1285` - Added support for nested components in web modules - :pull:`1289` - Added support for inline JavaScript as event handlers or other attributes that expect a callable via ``reactpy.types.InlineJavaScript`` +-:pull:`1307` - Added ``reactpy.web.import_js_from_file`` to import ReactJS components from a file. +-:pull:`1307` - Added ``reactpy.web.import_js_from_url`` to import ReactJS components from a URL. +-:pull:`1307` - Added ``reactpy.web.import_js_from_string`` to import ReactJS components from a string. **Changed** @@ -51,6 +54,13 @@ Unreleased - :pull:`1281` - ``reactpy.types.VdomDictConstructor`` has been renamed to ``reactpy.types.VdomConstructor``. - :pull:`1196` - Rewrite the ``event-to-object`` package to be more robust at handling properties on events. +**Deprecated** +-:pull:`1307` - ``reactpy.web.export`` is deprecated. Use ``reactpy.web.import_js_from_*`` instead. +-:pull:`1307` - ``reactpy.web.module_from_file`` is deprecated. Use ``reactpy.web.import_js_from_file`` instead. +-:pull:`1307` - ``reactpy.web.module_from_url`` is deprecated. Use ``reactpy.web.import_js_from_url`` instead. +-:pull:`1307` - ``reactpy.web.module_from_string`` is deprecated. Use ``reactpy.web.import_js_from_string`` instead. + + **Removed** - :pull:`1255` - Removed the ability to import ``reactpy.html.*`` elements directly. You must now call ``html.*`` to access the elements. From 4bcb55f403402e87f9b895d4676b1070126c608a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 7 Dec 2025 03:58:14 -0800 Subject: [PATCH 6/7] More robust keys for web module caching mechanism --- src/reactpy/web/module.py | 21 ++++++++++++--------- tests/test_web/test_module.py | 27 ++++++++++++++++++--------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/reactpy/web/module.py b/src/reactpy/web/module.py index 5d187d031..7e69d3cf3 100644 --- a/src/reactpy/web/module.py +++ b/src/reactpy/web/module.py @@ -65,8 +65,9 @@ def import_js_from_url( allow_children: Whether or not these components can have children. """ - if url in _URL_WEB_MODULE_CACHE: - module = _URL_WEB_MODULE_CACHE[url] + key = f"{url}{resolve_exports}{resolve_exports_depth}{unmount_before_update}" + if key in _URL_WEB_MODULE_CACHE: + module = _URL_WEB_MODULE_CACHE[key] else: module = _module_from_url( url, @@ -75,7 +76,7 @@ def import_js_from_url( resolve_exports_depth=resolve_exports_depth, unmount_before_update=unmount_before_update, ) - _URL_WEB_MODULE_CACHE[url] = module + _URL_WEB_MODULE_CACHE[key] = module return _vdom_from_web_module(module, export_names, fallback, allow_children) @@ -117,8 +118,9 @@ def import_js_from_file( allow_children: Whether or not these components can have children. """ - if name in _FILE_WEB_MODULE_CACHE: - module = _FILE_WEB_MODULE_CACHE[name] + key = f"{name}{resolve_exports}{resolve_exports_depth}{unmount_before_update}" + if key in _FILE_WEB_MODULE_CACHE: + module = _FILE_WEB_MODULE_CACHE[key] else: module = _module_from_file( name, @@ -129,7 +131,7 @@ def import_js_from_file( unmount_before_update=unmount_before_update, symlink=symlink, ) - _FILE_WEB_MODULE_CACHE[name] = module + _FILE_WEB_MODULE_CACHE[key] = module return _vdom_from_web_module(module, export_names, fallback, allow_children) @@ -168,8 +170,9 @@ def import_js_from_string( allow_children: Whether or not these components can have children. """ - if name in _STRING_WEB_MODULE_CACHE: - module = _STRING_WEB_MODULE_CACHE[name] + key = f"{name}{resolve_exports}{resolve_exports_depth}{unmount_before_update}" + if key in _STRING_WEB_MODULE_CACHE: + module = _STRING_WEB_MODULE_CACHE[key] else: module = _module_from_string( name, @@ -179,7 +182,7 @@ def import_js_from_string( resolve_exports_depth=resolve_exports_depth, unmount_before_update=unmount_before_update, ) - _STRING_WEB_MODULE_CACHE[name] = module + _STRING_WEB_MODULE_CACHE[key] = module return _vdom_from_web_module(module, export_names, fallback, allow_children) diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index 560f63e4d..a28b8f4e9 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -474,12 +474,15 @@ def test_import_js_from_url_caching(): # First import reactpy.web.import_js_from_url(url, "Component", resolve_exports=False) - assert url in reactpy.web.module._URL_WEB_MODULE_CACHE - module1 = reactpy.web.module._URL_WEB_MODULE_CACHE[url] + # Find the key that contains the 'url' substring + key = next(x for x in reactpy.web.module._URL_WEB_MODULE_CACHE.keys() if url in x) + module1 = reactpy.web.module._URL_WEB_MODULE_CACHE[key] + assert module1 + initial_length = len(reactpy.web.module._URL_WEB_MODULE_CACHE) # Second import reactpy.web.import_js_from_url(url, "Component", resolve_exports=False) - assert reactpy.web.module._URL_WEB_MODULE_CACHE[url] is module1 + assert len(reactpy.web.module._URL_WEB_MODULE_CACHE) == initial_length def test_import_js_from_file_caching(tmp_path): @@ -489,11 +492,13 @@ def test_import_js_from_file_caching(tmp_path): reactpy.web.module._FILE_WEB_MODULE_CACHE.clear() reactpy.web.import_js_from_file(name, file, "Component") - assert name in reactpy.web.module._FILE_WEB_MODULE_CACHE - module1 = reactpy.web.module._FILE_WEB_MODULE_CACHE[name] + key = next(x for x in reactpy.web.module._FILE_WEB_MODULE_CACHE.keys() if name in x) + module1 = reactpy.web.module._FILE_WEB_MODULE_CACHE[key] + assert module1 + initial_length = len(reactpy.web.module._FILE_WEB_MODULE_CACHE) reactpy.web.import_js_from_file(name, file, "Component") - assert reactpy.web.module._FILE_WEB_MODULE_CACHE[name] is module1 + assert len(reactpy.web.module._FILE_WEB_MODULE_CACHE) == initial_length def test_import_js_from_string_caching(): @@ -502,8 +507,12 @@ def test_import_js_from_string_caching(): reactpy.web.module._STRING_WEB_MODULE_CACHE.clear() reactpy.web.import_js_from_string(name, content, "Component") - assert name in reactpy.web.module._STRING_WEB_MODULE_CACHE - module1 = reactpy.web.module._STRING_WEB_MODULE_CACHE[name] + key = next( + x for x in reactpy.web.module._STRING_WEB_MODULE_CACHE.keys() if name in x + ) + module1 = reactpy.web.module._STRING_WEB_MODULE_CACHE[key] + assert module1 + initial_length = len(reactpy.web.module._STRING_WEB_MODULE_CACHE) reactpy.web.import_js_from_string(name, content, "Component") - assert reactpy.web.module._STRING_WEB_MODULE_CACHE[name] is module1 + assert len(reactpy.web.module._STRING_WEB_MODULE_CACHE) == initial_length From 9835ba9e90f4dee44a28ed1ae4bb99ada4ad39ef Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 7 Dec 2025 04:12:55 -0800 Subject: [PATCH 7/7] more verbose naming --- docs/source/about/changelog.rst | 14 +-- src/reactpy/web/__init__.py | 12 +- src/reactpy/web/module.py | 192 +++++++++++--------------------- tests/test_web/test_module.py | 40 +++---- 4 files changed, 101 insertions(+), 157 deletions(-) diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index 497d44113..e9bb8514a 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -31,9 +31,9 @@ Unreleased - :pull:`1281` - Added type hints to ``reactpy.html`` attributes. - :pull:`1285` - Added support for nested components in web modules - :pull:`1289` - Added support for inline JavaScript as event handlers or other attributes that expect a callable via ``reactpy.types.InlineJavaScript`` --:pull:`1307` - Added ``reactpy.web.import_js_from_file`` to import ReactJS components from a file. --:pull:`1307` - Added ``reactpy.web.import_js_from_url`` to import ReactJS components from a URL. --:pull:`1307` - Added ``reactpy.web.import_js_from_string`` to import ReactJS components from a string. +-:pull:`1307` - Added ``reactpy.web.reactjs_component_from_file`` to import ReactJS components from a file. +-:pull:`1307` - Added ``reactpy.web.reactjs_component_from_url`` to import ReactJS components from a URL. +-:pull:`1307` - Added ``reactpy.web.reactjs_component_from_string`` to import ReactJS components from a string. **Changed** @@ -55,10 +55,10 @@ Unreleased - :pull:`1196` - Rewrite the ``event-to-object`` package to be more robust at handling properties on events. **Deprecated** --:pull:`1307` - ``reactpy.web.export`` is deprecated. Use ``reactpy.web.import_js_from_*`` instead. --:pull:`1307` - ``reactpy.web.module_from_file`` is deprecated. Use ``reactpy.web.import_js_from_file`` instead. --:pull:`1307` - ``reactpy.web.module_from_url`` is deprecated. Use ``reactpy.web.import_js_from_url`` instead. --:pull:`1307` - ``reactpy.web.module_from_string`` is deprecated. Use ``reactpy.web.import_js_from_string`` instead. +-:pull:`1307` - ``reactpy.web.export`` is deprecated. Use ``reactpy.web.reactjs_component_from_*`` instead. +-:pull:`1307` - ``reactpy.web.module_from_file`` is deprecated. Use ``reactpy.web.reactjs_component_from_file`` instead. +-:pull:`1307` - ``reactpy.web.module_from_url`` is deprecated. Use ``reactpy.web.reactjs_component_from_url`` instead. +-:pull:`1307` - ``reactpy.web.module_from_string`` is deprecated. Use ``reactpy.web.reactjs_component_from_string`` instead. **Removed** diff --git a/src/reactpy/web/__init__.py b/src/reactpy/web/__init__.py index 9cff7e673..bf75cd372 100644 --- a/src/reactpy/web/__init__.py +++ b/src/reactpy/web/__init__.py @@ -1,19 +1,19 @@ from reactpy.web.module import ( export, - import_js_from_file, - import_js_from_string, - import_js_from_url, module_from_file, module_from_string, module_from_url, + reactjs_component_from_file, + reactjs_component_from_string, + reactjs_component_from_url, ) __all__ = [ "export", - "import_js_from_file", - "import_js_from_string", - "import_js_from_url", "module_from_file", "module_from_string", "module_from_url", + "reactjs_component_from_file", + "reactjs_component_from_string", + "reactjs_component_from_url", ] diff --git a/src/reactpy/web/module.py b/src/reactpy/web/module.py index 7e69d3cf3..239cf31b5 100644 --- a/src/reactpy/web/module.py +++ b/src/reactpy/web/module.py @@ -33,12 +33,12 @@ _STRING_WEB_MODULE_CACHE: dict[str, WebModule] = {} -def import_js_from_url( +def reactjs_component_from_url( url: str, - export_names: str | list[str] | tuple[str, ...], + import_names: str | list[str] | tuple[str, ...], fallback: Any | None = None, - resolve_exports: bool | None = None, - resolve_exports_depth: int = 5, + resolve_imports: bool | None = None, + resolve_imports_depth: int = 5, unmount_before_update: bool = False, allow_children: bool = True, ) -> VdomConstructor | list[VdomConstructor]: @@ -47,16 +47,16 @@ def import_js_from_url( Parameters: url: The URL to import the component from. - export_names: - One or more names to export. If given as a string, a single component + import_names: + One or more component names to import. If given as a string, a single component will be returned. If a list is given, then a list of components will be returned. fallback: What to temporarily display while the module is being loaded. - resolve_exports: - Whether to try and find all the named exports of this module. - resolve_exports_depth: - How deeply to search for those exports. + resolve_imports: + Whether to try and find all the named imports of this module. + resolve_imports_depth: + How deeply to search for those imports. unmount_before_update: Cause the component to be unmounted before each update. This option should only be used if the imported package fails to re-render when props change. @@ -65,28 +65,28 @@ def import_js_from_url( allow_children: Whether or not these components can have children. """ - key = f"{url}{resolve_exports}{resolve_exports_depth}{unmount_before_update}" + key = f"{url}{resolve_imports}{resolve_imports_depth}{unmount_before_update}" if key in _URL_WEB_MODULE_CACHE: module = _URL_WEB_MODULE_CACHE[key] else: module = _module_from_url( url, fallback=fallback, - resolve_exports=resolve_exports, - resolve_exports_depth=resolve_exports_depth, + resolve_imports=resolve_imports, + resolve_imports_depth=resolve_imports_depth, unmount_before_update=unmount_before_update, ) _URL_WEB_MODULE_CACHE[key] = module - return _vdom_from_web_module(module, export_names, fallback, allow_children) + return _vdom_from_web_module(module, import_names, fallback, allow_children) -def import_js_from_file( +def reactjs_component_from_file( name: str, file: str | Path, - export_names: str | list[str] | tuple[str, ...], + import_names: str | list[str] | tuple[str, ...], fallback: Any | None = None, - resolve_exports: bool | None = None, - resolve_exports_depth: int = 5, + resolve_imports: bool | None = None, + resolve_imports_depth: int = 5, unmount_before_update: bool = False, symlink: bool = False, allow_children: bool = True, @@ -98,16 +98,16 @@ def import_js_from_file( The name of the package file: The file from which the content of the web module will be created. - export_names: - One or more names to export. If given as a string, a single component + import_names: + One or more component names to import. If given as a string, a single component will be returned. If a list is given, then a list of components will be returned. fallback: What to temporarily display while the module is being loaded. - resolve_exports: - Whether to try and find all the named exports of this module. - resolve_exports_depth: - How deeply to search for those exports. + resolve_imports: + Whether to try and find all the named imports of this module. + resolve_imports_depth: + How deeply to search for those imports. unmount_before_update: Cause the component to be unmounted before each update. This option should only be used if the imported package fails to re-render when props change. @@ -118,7 +118,7 @@ def import_js_from_file( allow_children: Whether or not these components can have children. """ - key = f"{name}{resolve_exports}{resolve_exports_depth}{unmount_before_update}" + key = f"{name}{resolve_imports}{resolve_imports_depth}{unmount_before_update}" if key in _FILE_WEB_MODULE_CACHE: module = _FILE_WEB_MODULE_CACHE[key] else: @@ -126,22 +126,22 @@ def import_js_from_file( name, file, fallback=fallback, - resolve_exports=resolve_exports, - resolve_exports_depth=resolve_exports_depth, + resolve_imports=resolve_imports, + resolve_imports_depth=resolve_imports_depth, unmount_before_update=unmount_before_update, symlink=symlink, ) _FILE_WEB_MODULE_CACHE[key] = module - return _vdom_from_web_module(module, export_names, fallback, allow_children) + return _vdom_from_web_module(module, import_names, fallback, allow_children) -def import_js_from_string( +def reactjs_component_from_string( name: str, content: str, - export_names: str | list[str] | tuple[str, ...], + import_names: str | list[str] | tuple[str, ...], fallback: Any | None = None, - resolve_exports: bool | None = None, - resolve_exports_depth: int = 5, + resolve_imports: bool | None = None, + resolve_imports_depth: int = 5, unmount_before_update: bool = False, allow_children: bool = True, ) -> VdomConstructor | list[VdomConstructor]: @@ -152,16 +152,16 @@ def import_js_from_string( The name of the package content: The contents of the web module - export_names: - One or more names to export. If given as a string, a single component + import_names: + One or more component names to import. If given as a string, a single component will be returned. If a list is given, then a list of components will be returned. fallback: What to temporarily display while the module is being loaded. - resolve_exports: - Whether to try and find all the named exports of this module. - resolve_exports_depth: - How deeply to search for those exports. + resolve_imports: + Whether to try and find all the named imports of this module. + resolve_imports_depth: + How deeply to search for those imports. unmount_before_update: Cause the component to be unmounted before each update. This option should only be used if the imported package fails to re-render when props change. @@ -170,7 +170,7 @@ def import_js_from_string( allow_children: Whether or not these components can have children. """ - key = f"{name}{resolve_exports}{resolve_exports_depth}{unmount_before_update}" + key = f"{name}{resolve_imports}{resolve_imports_depth}{unmount_before_update}" if key in _STRING_WEB_MODULE_CACHE: module = _STRING_WEB_MODULE_CACHE[key] else: @@ -178,12 +178,12 @@ def import_js_from_string( name, content, fallback=fallback, - resolve_exports=resolve_exports, - resolve_exports_depth=resolve_exports_depth, + resolve_imports=resolve_imports, + resolve_imports_depth=resolve_imports_depth, unmount_before_update=unmount_before_update, ) _STRING_WEB_MODULE_CACHE[key] = module - return _vdom_from_web_module(module, export_names, fallback, allow_children) + return _vdom_from_web_module(module, import_names, fallback, allow_children) def module_from_url( @@ -194,14 +194,14 @@ def module_from_url( unmount_before_update: bool = False, ) -> WebModule: # pragma: no cover warn( - "module_from_url is deprecated, use import_js_from_url instead", + "module_from_url is deprecated, use reactjs_component_from_url instead", DeprecationWarning, ) return _module_from_url( url, fallback=fallback, - resolve_exports=resolve_exports, - resolve_exports_depth=resolve_exports_depth, + resolve_imports=resolve_exports, + resolve_imports_depth=resolve_exports_depth, unmount_before_update=unmount_before_update, ) @@ -216,15 +216,15 @@ def module_from_file( symlink: bool = False, ) -> WebModule: # pragma: no cover warn( - "module_from_file is deprecated, use import_js_from_file instead", + "module_from_file is deprecated, use reactjs_component_from_file instead", DeprecationWarning, ) return _module_from_file( name, file, fallback=fallback, - resolve_exports=resolve_exports, - resolve_exports_depth=resolve_exports_depth, + resolve_imports=resolve_exports, + resolve_imports_depth=resolve_exports_depth, unmount_before_update=unmount_before_update, symlink=symlink, ) @@ -239,15 +239,15 @@ def module_from_string( unmount_before_update: bool = False, ) -> WebModule: # pragma: no cover warn( - "module_from_string is deprecated, use import_js_from_string instead", + "module_from_string is deprecated, use reactjs_component_from_string instead", DeprecationWarning, ) return _module_from_string( name, content, fallback=fallback, - resolve_exports=resolve_exports, - resolve_exports_depth=resolve_exports_depth, + resolve_imports=resolve_exports, + resolve_imports_depth=resolve_exports_depth, unmount_before_update=unmount_before_update, ) @@ -255,38 +255,20 @@ def module_from_string( def _module_from_url( url: str, fallback: Any | None = None, - resolve_exports: bool | None = None, - resolve_exports_depth: int = 5, + resolve_imports: bool | None = None, + resolve_imports_depth: int = 5, unmount_before_update: bool = False, ) -> WebModule: - """Load a :class:`WebModule` from a :data:`URL_SOURCE` - - Parameters: - url: - Where the javascript module will be loaded from which conforms to the - interface for :ref:`Custom Javascript Components` - fallback: - What to temporarily display while the module is being loaded. - resolve_imports: - Whether to try and find all the named exports of this module. - resolve_exports_depth: - How deeply to search for those exports. - unmount_before_update: - Cause the component to be unmounted before each update. This option should - only be used if the imported package fails to re-render when props change. - Using this option has negative performance consequences since all DOM - elements must be changed on each render. See :issue:`461` for more info. - """ return WebModule( source=url, source_type=URL_SOURCE, default_fallback=fallback, file=None, export_names=( - resolve_module_exports_from_url(url, resolve_exports_depth) + resolve_module_exports_from_url(url, resolve_imports_depth) if ( - resolve_exports - if resolve_exports is not None + resolve_imports + if resolve_imports is not None else REACTPY_DEBUG.current ) else None @@ -299,32 +281,11 @@ def _module_from_file( name: str, file: str | Path, fallback: Any | None = None, - resolve_exports: bool | None = None, - resolve_exports_depth: int = 5, + resolve_imports: bool | None = None, + resolve_imports_depth: int = 5, unmount_before_update: bool = False, symlink: bool = False, ) -> WebModule: - """Load a :class:`WebModule` from a given ``file`` - - Parameters: - name: - The name of the package - file: - The file from which the content of the web module will be created. - fallback: - What to temporarily display while the module is being loaded. - resolve_imports: - Whether to try and find all the named exports of this module. - resolve_exports_depth: - How deeply to search for those exports. - unmount_before_update: - Cause the component to be unmounted before each update. This option should - only be used if the imported package fails to re-render when props change. - Using this option has negative performance consequences since all DOM - elements must be changed on each render. See :issue:`461` for more info. - symlink: - Whether the web module should be saved as a symlink to the given ``file``. - """ name += module_name_suffix(name) source_file = Path(file).resolve() @@ -349,10 +310,10 @@ def _module_from_file( default_fallback=fallback, file=target_file, export_names=( - resolve_module_exports_from_file(source_file, resolve_exports_depth) + resolve_module_exports_from_file(source_file, resolve_imports_depth) if ( - resolve_exports - if resolve_exports is not None + resolve_imports + if resolve_imports is not None else REACTPY_DEBUG.current ) else None @@ -381,29 +342,10 @@ def _module_from_string( name: str, content: str, fallback: Any | None = None, - resolve_exports: bool | None = None, - resolve_exports_depth: int = 5, + resolve_imports: bool | None = None, + resolve_imports_depth: int = 5, unmount_before_update: bool = False, ) -> WebModule: - """Load a :class:`WebModule` whose ``content`` comes from a string. - - Parameters: - name: - The name of the package - content: - The contents of the web module - fallback: - What to temporarily display while the module is being loaded. - resolve_imports: - Whether to try and find all the named exports of this module. - resolve_exports_depth: - How deeply to search for those exports. - unmount_before_update: - Cause the component to be unmounted before each update. This option should - only be used if the imported package fails to re-render when props change. - Using this option has negative performance consequences since all DOM - elements must be changed on each render. See :issue:`461` for more info. - """ name += module_name_suffix(name) target_file = _web_module_path(name) @@ -424,10 +366,10 @@ def _module_from_string( default_fallback=fallback, file=target_file, export_names=( - resolve_module_exports_from_file(target_file, resolve_exports_depth) + resolve_module_exports_from_file(target_file, resolve_imports_depth) if ( - resolve_exports - if resolve_exports is not None + resolve_imports + if resolve_imports is not None else REACTPY_DEBUG.current ) else None @@ -471,7 +413,7 @@ def export( allow_children: bool = True, ) -> VdomConstructor | list[VdomConstructor]: # pragma: no cover warn( - "export is deprecated, use import_js_from_* functions instead", + "export is deprecated, use reactjs_component_from_* functions instead", DeprecationWarning, ) return _vdom_from_web_module(web_module, export_names, fallback, allow_children) diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index a28b8f4e9..a0083bc6a 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -55,7 +55,7 @@ def ShowCurrentComponent(): async def test_module_from_url(browser): SimpleButton = reactpy.web.module._vdom_from_web_module( reactpy.web.module._module_from_url( - "/static/simple-button.js", resolve_exports=False + "/static/simple-button.js", resolve_imports=False ), "SimpleButton", ) @@ -420,18 +420,20 @@ def App(): assert await my_div.inner_text() == "PREFIX TEXT: TEST 123" -def test_import_js_from_string(): - reactpy.web.import_js_from_string("temp", "old", "Component", resolve_exports=False) +def test_reactjs_component_from_string(): + reactpy.web.reactjs_component_from_string( + "temp", "old", "Component", resolve_imports=False + ) reactpy.web.module._STRING_WEB_MODULE_CACHE.clear() with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): - reactpy.web.import_js_from_string( - "temp", "new", "Component", resolve_exports=False + reactpy.web.reactjs_component_from_string( + "temp", "new", "Component", resolve_imports=False ) -async def test_import_js_from_url(browser): - SimpleButton = reactpy.web.import_js_from_url( - "/static/simple-button.js", "SimpleButton", resolve_exports=False +async def test_reactjs_component_from_url(browser): + SimpleButton = reactpy.web.reactjs_component_from_url( + "/static/simple-button.js", "SimpleButton", resolve_imports=False ) @reactpy.component @@ -448,8 +450,8 @@ def ShowSimpleButton(): await display.page.wait_for_selector("#my-button") -async def test_import_js_from_file(display: DisplayFixture): - SimpleButton = reactpy.web.import_js_from_file( +async def test_reactjs_component_from_file(display: DisplayFixture): + SimpleButton = reactpy.web.reactjs_component_from_file( "simple-button", JS_FIXTURES_DIR / "simple-button.js", "SimpleButton" ) @@ -468,12 +470,12 @@ def ShowSimpleButton(): await poll(lambda: is_clicked.current).until_is(True) -def test_import_js_from_url_caching(): +def test_reactjs_component_from_url_caching(): url = "https://example.com/module.js" reactpy.web.module._URL_WEB_MODULE_CACHE.clear() # First import - reactpy.web.import_js_from_url(url, "Component", resolve_exports=False) + reactpy.web.reactjs_component_from_url(url, "Component", resolve_imports=False) # Find the key that contains the 'url' substring key = next(x for x in reactpy.web.module._URL_WEB_MODULE_CACHE.keys() if url in x) module1 = reactpy.web.module._URL_WEB_MODULE_CACHE[key] @@ -481,32 +483,32 @@ def test_import_js_from_url_caching(): initial_length = len(reactpy.web.module._URL_WEB_MODULE_CACHE) # Second import - reactpy.web.import_js_from_url(url, "Component", resolve_exports=False) + reactpy.web.reactjs_component_from_url(url, "Component", resolve_imports=False) assert len(reactpy.web.module._URL_WEB_MODULE_CACHE) == initial_length -def test_import_js_from_file_caching(tmp_path): +def test_reactjs_component_from_file_caching(tmp_path): file = tmp_path / "test.js" file.write_text("export function Component() {}") name = "test-file-module" reactpy.web.module._FILE_WEB_MODULE_CACHE.clear() - reactpy.web.import_js_from_file(name, file, "Component") + reactpy.web.reactjs_component_from_file(name, file, "Component") key = next(x for x in reactpy.web.module._FILE_WEB_MODULE_CACHE.keys() if name in x) module1 = reactpy.web.module._FILE_WEB_MODULE_CACHE[key] assert module1 initial_length = len(reactpy.web.module._FILE_WEB_MODULE_CACHE) - reactpy.web.import_js_from_file(name, file, "Component") + reactpy.web.reactjs_component_from_file(name, file, "Component") assert len(reactpy.web.module._FILE_WEB_MODULE_CACHE) == initial_length -def test_import_js_from_string_caching(): +def test_reactjs_component_from_string_caching(): name = "test-string-module" content = "export function Component() {}" reactpy.web.module._STRING_WEB_MODULE_CACHE.clear() - reactpy.web.import_js_from_string(name, content, "Component") + reactpy.web.reactjs_component_from_string(name, content, "Component") key = next( x for x in reactpy.web.module._STRING_WEB_MODULE_CACHE.keys() if name in x ) @@ -514,5 +516,5 @@ def test_import_js_from_string_caching(): assert module1 initial_length = len(reactpy.web.module._STRING_WEB_MODULE_CACHE) - reactpy.web.import_js_from_string(name, content, "Component") + reactpy.web.reactjs_component_from_string(name, content, "Component") assert len(reactpy.web.module._STRING_WEB_MODULE_CACHE) == initial_length