Skip to content

Commit 8f82227

Browse files
committed
create well defined client implementation layer
now the internals of IDOM use idom.client.protocol.client_implementation where possible (e.g. idom.Module) in order to allow different client implementations to be patched in.
1 parent 7eabcbd commit 8f82227

12 files changed

+128
-90
lines changed

idom/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
DistributionNotFound as _DistributionNotFound,
44
)
55

6+
from .utils import Ref, html_to_vdom
7+
68
from .core.element import element, Element
79
from .core.events import event, Events
810
from .core.layout import Layout
911
from .core.vdom import vdom, VdomDict
1012
from .core import hooks
1113

1214
from .widgets.html import html
13-
from .widgets.module import Module, Import
1415
from .widgets.utils import hotswap, multiview
1516

16-
from .utils import Ref, html_to_vdom
17+
from .client.module import Module, Import
18+
from .client.protocol import client_implementation as client
1719

1820
from . import server
1921
from . import widgets
@@ -59,4 +61,5 @@
5961
"html_to_vdom",
6062
"VdomDict",
6163
"widgets",
64+
"client",
6265
]

idom/client/__init__.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
1-
from .manage import (
2-
web_module_url,
3-
find_path,
4-
delete_web_modules,
5-
register_web_module,
6-
web_module_exists,
7-
install,
8-
installed,
9-
restore,
10-
)
1+
from .protocol import client_implementation
2+
from .module import Module, Import
113

12-
__all__ = [
13-
"web_module_url",
14-
"find_path",
15-
"delete_web_modules",
16-
"register_web_module",
17-
"web_module_exists",
18-
"web_module_path",
19-
"install",
20-
"installed",
21-
"restore",
22-
]
4+
5+
__all__ = ["client_implementation", "Module", "Import"]

idom/widgets/module.py renamed to idom/client/module.py

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,60 @@
22
from typing import Any, Optional, Union
33
from urllib.parse import urlparse
44

5-
from idom import client
65
from idom.core.vdom import VdomDict, ImportSourceDict, make_vdom_constructor
76

7+
from .protocol import client_implementation as client
8+
89

910
class Module:
1011
"""A Javascript module
1112
1213
Parameters:
13-
path:
14+
url_or_name:
1415
The URL to an ECMAScript module which exports React components
1516
(*with* a ``.js`` file extension) or name of a module installed in the
1617
built-in client application (*without* a ``.js`` file extension).
1718
source_file:
18-
Only applicable if running on the built-in client app. Dynamically install
19-
the code in the give file as a single-file module. The built-in client will
20-
inject this module adjacent to other installed modules which means they can
21-
be imported via a relative path (e.g. ``./some-other-installed-module.js``).
22-
23-
.. warning::
24-
25-
Do not use the ``source_file`` parameter if not running with the client app
26-
distributed with ``idom``.
27-
28-
Examples:
29-
.. testcode::
30-
31-
import idom
32-
19+
Only applicable if running on a client app which supports this feature.
20+
Dynamically install the code in the give file as a single-file module. The
21+
built-in client will inject this module adjacent to other installed modules
22+
which means they can be imported via a relative path like
23+
``./some-other-installed-module.js``.
24+
25+
Attributes:
26+
installed:
27+
Whether or not this module has been installed into the built-in client app.
28+
url:
29+
The URL this module will be imported from.
30+
31+
Notes:
32+
To allow for other client implementations, you can set the current client
33+
implementation
34+
following private methods to support serving dynamically registered source
35+
files or loading modules that have been installed by some other means:
3336
"""
3437

35-
__slots__ = "_module", "_installed"
38+
__slots__ = "url", "installed"
3639

3740
def __init__(
3841
self,
39-
path: str,
42+
url_or_name: str,
4043
source_file: Optional[Union[str, Path]] = None,
4144
) -> None:
42-
self._installed = False
45+
self.installed = False
4346
if source_file is not None:
44-
self._module = client.register_web_module(path, source_file)
45-
self._installed = True
46-
elif client.web_module_exists(path):
47-
self._module = client.web_module_url(path)
48-
self._installed = True
49-
elif not _is_url(path):
47+
self.url = client.current.register_web_module(url_or_name, source_file)
48+
self.installed = True
49+
elif client.current.web_module_exists(url_or_name):
50+
self.url = client.current.web_module_url(url_or_name)
51+
self.installed = True
52+
elif _is_url(url_or_name):
53+
self.url = url_or_name
54+
else:
5055
raise ValueError(
51-
f"{path!r} is not installed - "
56+
f"{url_or_name!r} is not installed - "
5257
"only installed modules can omit a file extension."
5358
)
54-
else:
55-
self._module = path
56-
57-
@property
58-
def installed(self) -> bool:
59-
"""Whether or not this module has been installed into the built-in client app."""
60-
return self._installed
61-
62-
@property
63-
def url(self) -> str:
64-
"""The path this module will be imported from"""
65-
return self._module
6659

6760
def Import(self, name: str, *args: Any, **kwargs: Any) -> "Import":
6861
"""Return an :class:`Import` for the given :class:`Module` and ``name``
@@ -76,10 +69,10 @@ def Import(self, name: str, *args: Any, **kwargs: Any) -> "Import":
7669
Where ``name`` is the given name, and ``module`` is the :attr:`Module.url` of
7770
this :class:`Module` instance.
7871
"""
79-
return Import(self._module, name, *args, **kwargs)
72+
return Import(self.url, name, *args, **kwargs)
8073

8174
def __repr__(self) -> str: # pragma: no cover
82-
return f"{type(self).__name__}({self._module!r})"
75+
return f"{type(self).__name__}({self.url!r})"
8376

8477

8578
class Import:

idom/client/protocol.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from pathlib import Path
2+
from typing import Union
3+
4+
from typing_extensions import Protocol
5+
6+
from idom import Ref
7+
8+
from . import manage
9+
10+
11+
class ClientImplementation(Protocol):
12+
"""A minimal set of functions required to use :class:`idom.widget.module.Module`"""
13+
14+
def register_web_module(self, name: str, source: Union[str, Path]) -> None:
15+
"""Add a module with the given ``name`` to the client using the given ``source``"""
16+
17+
def web_module_url(self, name: str) -> str:
18+
"""Return the URL to import the module with the given name."""
19+
20+
def web_module_exists(self, name: str) -> bool:
21+
"""Check if a module with the given name is installed"""
22+
23+
24+
client_implementation: Ref[ClientImplementation] = Ref(manage)
25+
"""The current client implementation used by :class:`idom.widgets.module.Module`"""

idom/widgets/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
from .utils import hotswap, multiview
2-
from .module import Module, Import
32
from .html import html, image, Input
43

54
__all__ = [
65
"node",
76
"hotswap",
87
"multiview",
98
"html",
10-
"Module",
11-
"Import",
129
"image",
1310
"Input",
1411
]

scripts/__init__.py

Whitespace-only changes.

scripts/all_examples.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
import idom
66
from idom.server.sanic import PerClientStateServer
77

8+
from scripts.install_doc_js_modules import install_doc_js_modules
9+
10+
11+
install_doc_js_modules()
12+
813
here = Path(__file__).parent
914
examples_dir = here.parent / "docs" / "source" / "examples"
1015
sys.path.insert(0, str(examples_dir))

scripts/install_doc_js_modules.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from idom.client.manage import install, installed
2+
3+
4+
def install_doc_js_modules(show_spinner=True):
5+
to_install = {
6+
"@material-ui/core",
7+
"victory",
8+
"semantic-ui-react",
9+
}.difference(installed())
10+
if to_install:
11+
install(to_install, [], show_spinner=show_spinner)
12+
13+
14+
if __name__ == "__main__":
15+
install_doc_js_modules()

scripts/local-docs.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
set -e
2-
sphinx-build -b html docs/source docs/build
2+
python scripts/install_doc_js_modules.py
3+
sphinx-build -E -b html docs/source docs/build
34
cd docs
45
python -c "import main; main.local()"
56
cd ../

scripts/one_example.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
from idom.widgets.utils import hotswap
55
from idom.server.sanic import PerClientStateServer
66

7+
from scripts.install_doc_js_modules import install_doc_js_modules
8+
9+
10+
install_doc_js_modules()
11+
712
here = Path(__file__).parent
813
examples_dir = here.parent / "docs" / "source" / "examples"
914
sys.path.insert(0, str(examples_dir))

tests/test_client/test_manage.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,50 @@
44

55
import pytest
66

7-
from idom import client
7+
from idom.client.manage import (
8+
install,
9+
installed,
10+
delete_web_modules,
11+
register_web_module,
12+
web_module_exists,
13+
web_module_url,
14+
)
815

916

1017
@pytest.mark.slow
1118
def test_install():
12-
client.delete_web_modules(["jquery"], skip_missing=True)
13-
client.install("jquery")
19+
delete_web_modules(["jquery"], skip_missing=True)
20+
install("jquery")
1421

15-
assert client.web_module_exists("jquery")
16-
assert client.web_module_exists("/jquery") # works with a leading slash too
17-
assert "jquery" in client.installed()
22+
assert web_module_exists("jquery")
23+
assert web_module_exists("/jquery") # works with a leading slash too
24+
assert "jquery" in installed()
1825

1926
with pytest.raises(ValueError, match="already exists"):
2027
# can't register a module with the same name
21-
client.register_web_module("jquery", Path() / "some-module.js")
28+
register_web_module("jquery", Path() / "some-module.js")
2229

23-
client.delete_web_modules("jquery")
24-
assert not client.web_module_exists("jquery")
25-
assert "jquery" not in client.installed()
30+
delete_web_modules("jquery")
31+
assert not web_module_exists("jquery")
32+
assert "jquery" not in installed()
2633

2734

2835
@pytest.mark.slow
2936
def test_install_namespace_package():
30-
client.install("@material-ui/core")
31-
assert client.web_module_exists("@material-ui/core")
37+
install("@material-ui/core")
38+
assert web_module_exists("@material-ui/core")
3239
expected = "../web_modules/@material-ui/core.js"
33-
assert client.web_module_url("@material-ui/core") == expected
40+
assert web_module_url("@material-ui/core") == expected
3441

3542

3643
def test_error_on_delete_not_exists():
3744
with pytest.raises(ValueError, match=r"Module .*? does not exist"):
38-
client.delete_web_modules("module/that/does/not/exist")
45+
delete_web_modules("module/that/does/not/exist")
3946

4047

4148
def test_raise_on_missing_import_path():
4249
with pytest.raises(ValueError, match="does not exist"):
43-
client.web_module_url("module/that/does/not/exist")
50+
web_module_url("module/that/does/not/exist")
4451

4552

4653
called_process_error = CalledProcessError(1, "failing-cmd")
@@ -50,13 +57,13 @@ def test_raise_on_missing_import_path():
5057
@mock.patch("subprocess.run", side_effect=called_process_error)
5158
def test_bad_subprocess_call(subprocess_run, caplog):
5259
with pytest.raises(CalledProcessError):
53-
client.install(["victory"])
60+
install(["victory"])
5461
assert "an error occured" in caplog.text
5562

5663

5764
def test_cannot_register_module_from_non_existant_source():
5865
with pytest.raises(ValueError, match="does not exist"):
59-
client.register_web_module("my-module", Path() / "a-non-existant-file.js")
66+
register_web_module("my-module", Path() / "a-non-existant-file.js")
6067

6168
with pytest.raises(ValueError, match="is not a file"):
62-
client.register_web_module("my-module", Path("/"))
69+
register_web_module("my-module", Path("/"))

tests/test_widgets/test_module.py renamed to tests/test_client/test_module.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import pytest
44
import idom
5-
from idom import client
6-
from idom import Module
5+
from idom.client.manage import install, delete_web_modules
6+
from idom import Module, client
77

88

99
HERE = Path(__file__).parent
@@ -12,7 +12,7 @@
1212
@pytest.fixture
1313
def victory():
1414
if "victory" not in client.installed():
15-
client.install(["victory"], [])
15+
install(["victory"], [])
1616
return Module("victory")
1717

1818

@@ -51,7 +51,7 @@ def test_custom_module(driver, display, victory):
5151

5252
driver.find_element_by_class_name("VictoryContainer")
5353

54-
client.delete_web_modules("my/chart")
54+
delete_web_modules("my/chart")
5555
assert not client.web_module_exists("my/chart")
5656

5757

@@ -60,3 +60,7 @@ def test_module_from_url():
6060
jquery = idom.Module(url)
6161
assert jquery.url == url
6262
assert not jquery.installed
63+
64+
65+
def test_module_uses_current_client_implementation():
66+
assert False

0 commit comments

Comments
 (0)