Skip to content

Commit 3f26e07

Browse files
committed
more tests + check js module exports
1 parent 6def076 commit 3f26e07

File tree

15 files changed

+138
-64
lines changed

15 files changed

+138
-64
lines changed

docs/source/examples/material_ui_slider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import idom
44

55

6-
material_ui = idom.Module("@material-ui/core")
7-
MaterialSlider = material_ui.Import("Slider", fallback="loading...")
6+
material_ui = idom.Module("@material-ui/core", fallback="loading...")
7+
MaterialSlider = material_ui.Import("Slider")
88

99

1010
@idom.element

docs/source/examples/victory_chart.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import idom
22

3-
victory = idom.Module("victory")
3+
victory = idom.Module("victory", fallback="loading...")
44

5-
VictoryBar = victory.Import("VictoryBar", fallback="loading...")
5+
VictoryBar = victory.Import("VictoryBar")
66

77
idom.run(
88
idom.element(

idom/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from .client.module import Module, Import
2525
from .client.protocol import client_implementation as client
2626

27-
from .server.utils import run
27+
from .server.prefab import run
2828

2929
from . import widgets
3030

idom/cli/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def restore():
4747
@show.command()
4848
def build_config() -> None:
4949
"""Show the state of IDOM's build config"""
50-
typer.echo(json.dumps(manage_client.build_config_file().to_dicts(), indent=2))
50+
typer.echo(json.dumps(manage_client.build_config().config, indent=2))
5151
return None
5252

5353

idom/client/build_config.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,16 @@ def update_config_items(self, config_items: Iterable[BuildConfigItem]) -> None:
9191
self.config["by_source"][conf["source_name"]] = conf
9292

9393
@_modified_by_transaction
94-
def get_js_dependency_alias(self, source_name: str, dependency_name: str) -> str:
94+
def get_js_dependency_alias(
95+
self,
96+
source_name: str,
97+
dependency_name: str,
98+
) -> Optional[str]:
9599
aliases_by_src = self._derived_properties["js_dependency_aliases_by_source"]
96-
return aliases_by_src[source_name][dependency_name]
100+
try:
101+
return aliases_by_src[source_name][dependency_name]
102+
except KeyError:
103+
return None
97104

98105
@_modified_by_transaction
99106
def all_aliased_js_dependencies(self) -> List[str]:
@@ -132,13 +139,6 @@ def __repr__(self) -> str:
132139
return f"{type(self).__name__}({self.config})"
133140

134141

135-
def find_build_config_item_in_python_file(
136-
module_name: str, path: Path
137-
) -> Optional[BuildConfigItem]:
138-
with path.open() as f:
139-
return find_build_config_item_in_python_source(module_name, f.read())
140-
141-
142142
def find_python_packages_build_config_items(
143143
paths: Optional[List[str]] = None,
144144
) -> Tuple[List[BuildConfigItem], List[Exception]]:
@@ -173,6 +173,13 @@ def find_python_packages_build_config_items(
173173
return build_configs, failures
174174

175175

176+
def find_build_config_item_in_python_file(
177+
module_name: str, path: Path
178+
) -> Optional[BuildConfigItem]:
179+
with path.open() as f:
180+
return find_build_config_item_in_python_source(module_name, f.read())
181+
182+
176183
def find_build_config_item_in_python_source(
177184
module_name: str, module_src: str
178185
) -> Optional[BuildConfigItem]:

idom/client/manage.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,51 +9,58 @@
99
BuildConfigItem,
1010
find_python_packages_build_config_items,
1111
)
12-
from .utils import open_modifiable_json
12+
from .utils import open_modifiable_json, find_js_module_exports
1313

1414
from idom.cli import console
1515

1616

1717
APP_DIR = Path(__file__).parent / "app"
1818
BUILD_DIR = APP_DIR / "build"
19+
_BUILD_CONFIG: Optional[BuildConfig] = None
1920

2021

2122
def build_config() -> BuildConfig:
22-
return BuildConfig(BUILD_DIR)
23+
global _BUILD_CONFIG
24+
if _BUILD_CONFIG is None:
25+
_BUILD_CONFIG = BuildConfig(BUILD_DIR)
26+
return _BUILD_CONFIG
2327

2428

25-
def find_path(url_path: str) -> Optional[Path]:
26-
url_path = url_path.strip("/")
27-
28-
builtin_path = BUILD_DIR.joinpath(*url_path.split("/"))
29-
if builtin_path.exists():
30-
return builtin_path
31-
else:
32-
return None
29+
def web_module_exports(source_name: str, package_name: str) -> List[str]:
30+
dep_alias = build_config().get_js_dependency_alias(source_name, package_name)
31+
if dep_alias is None:
32+
return []
33+
module_file = find_client_build_path(f"web_modules/{dep_alias}.js")
34+
if module_file is None:
35+
return []
36+
return find_js_module_exports(module_file)
3337

3438

3539
def web_module_url(source_name: str, package_name: str) -> Optional[str]:
36-
config = build_config().configs.get(source_name)
37-
if config is None:
38-
return None
39-
if package_name not in config.js_dependency_aliases:
40+
dep_alias = build_config().get_js_dependency_alias(source_name, package_name)
41+
if dep_alias is None:
4042
return None
41-
alias = config.js_dependency_aliases[package_name]
42-
if find_path(f"web_modules/{alias}.js") is None:
43+
if find_client_build_path(f"web_modules/{dep_alias}.js") is None:
4344
return None
4445
# need to go back a level since the JS that import this is in `core_components`
45-
return f"../web_modules/{alias}.js"
46+
return f"../web_modules/{dep_alias}.js"
47+
48+
49+
def find_client_build_path(rel_path: str) -> Optional[Path]:
50+
if rel_path.startswith("/"):
51+
raise ValueError(f"{rel_path!r} is not a relative path")
52+
builtin_path = BUILD_DIR.joinpath(*rel_path.split("/"))
53+
if builtin_path.exists():
54+
return builtin_path
55+
else:
56+
return None
4657

4758

48-
def build(
49-
config_items: Optional[Iterable[BuildConfigItem]] = None,
50-
output_dir: Path = BUILD_DIR,
51-
) -> None:
59+
def build(config_items: Iterable[BuildConfigItem] = ()) -> None:
5260
config = build_config()
5361

5462
with config.transaction():
55-
if config_items is not None:
56-
config.update_config_items(config_items)
63+
config.update_config_items(config_items)
5764

5865
with console.spinner("Discovering dependencies"):
5966
configs, errors = find_python_packages_build_config_items()
@@ -88,10 +95,10 @@ def build(
8895
with console.spinner("Building client"):
8996
_npm_run_build(temp_app_dir)
9097

91-
if output_dir.exists():
92-
shutil.rmtree(output_dir)
98+
if BUILD_DIR.exists():
99+
shutil.rmtree(BUILD_DIR)
93100

94-
shutil.copytree(temp_build_dir, output_dir, symlinks=True)
101+
shutil.copytree(temp_build_dir, BUILD_DIR, symlinks=True)
95102

96103

97104
def restore() -> None:

idom/client/module.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
class Module:
11-
"""A Javascript module
11+
"""An importable client-side module
1212
1313
Parameters:
1414
url_or_name:
@@ -35,12 +35,19 @@ class Module:
3535
files or loading modules that have been installed by some other means:
3636
"""
3737

38-
__slots__ = "url", "installed"
38+
__slots__ = "url", "installed", "fallback", "exports"
3939

40-
def __init__(self, url_or_name: str, source_name: Optional[str] = None) -> None:
40+
def __init__(
41+
self,
42+
url_or_name: str,
43+
source_name: Optional[str] = None,
44+
fallback: Optional[str] = None,
45+
check_exports: bool = True,
46+
) -> None:
4147
if _is_url(url_or_name):
4248
self.url = url_or_name
4349
self.installed = False
50+
self.exports = []
4451
else:
4552
if source_name is None:
4653
module_name: str = inspect.currentframe().f_back.f_globals["__name__"]
@@ -52,6 +59,14 @@ def __init__(self, url_or_name: str, source_name: Optional[str] = None) -> None:
5259
)
5360
self.url = url
5461
self.installed = True
62+
if check_exports:
63+
self.exports = client.current.web_module_exports(
64+
source_name,
65+
url_or_name,
66+
)
67+
else:
68+
self.exports = []
69+
self.fallback = fallback
5570

5671
def Import(self, name: str, *args: Any, **kwargs: Any) -> "Import":
5772
"""Return an :class:`Import` for the given :class:`Module` and ``name``
@@ -65,6 +80,17 @@ def Import(self, name: str, *args: Any, **kwargs: Any) -> "Import":
6580
Where ``name`` is the given name, and ``module`` is the :attr:`Module.url` of
6681
this :class:`Module` instance.
6782
"""
83+
if (
84+
self.installed
85+
and self.exports
86+
# if 'default' is exported there's not much we can infer
87+
and "default" not in self.exports
88+
):
89+
if name not in self.exports:
90+
raise ValueError(
91+
f"{self} does not export {name!r}, available options are {self.exports}"
92+
)
93+
kwargs.setdefault("fallback", self.fallback)
6894
return Import(self.url, name, *args, **kwargs)
6995

7096
def __repr__(self) -> str: # pragma: no cover

idom/client/utils.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import re
12
import json
23
from pathlib import Path
34
from contextlib import contextmanager
4-
from typing import Iterator, Any
5+
from typing import Iterator, Any, List
56

67

78
@contextmanager
@@ -13,3 +14,18 @@ def open_modifiable_json(path: Path) -> Iterator[Any]:
1314

1415
with path.open("w") as f:
1516
json.dump(data, f)
17+
18+
19+
_JS_MODULE_EXPORT_PATTERN = re.compile(r";export{(.*)};")
20+
21+
22+
def find_js_module_exports(path: Path) -> List[str]:
23+
names: List[str] = []
24+
if path.suffix != ".js":
25+
# we only know how to do this for javascript modules
26+
return []
27+
with path.open() as f:
28+
for match in _JS_MODULE_EXPORT_PATTERN.findall(f.read()):
29+
for export in match.split(","):
30+
names.append(export.split(" as ", 1)[1].strip())
31+
return names

idom/server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .base import AbstractRenderServer
2-
from .utils import run, multiview_server, hotswap_server
2+
from .prefab import run, multiview_server, hotswap_server
33

44
__all__ = ["run", "multiview_server", "hotswap_server", "AbstractRenderServer"]

idom/server/utils.py renamed to idom/server/prefab.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from socket import socket
21
from importlib import import_module
2+
from socket import socket
33
from typing import Any, Dict, Optional, Tuple, Type, TypeVar, cast
44

55
from idom.core.element import ElementConstructor
@@ -53,7 +53,7 @@ def run(
5353
if server_type is None: # pragma: no cover
5454
raise ValueError("No default server available.")
5555
if port is None: # pragma: no cover
56-
port = find_available_port(host)
56+
port = _find_available_port(host)
5757

5858
server = server_type(element, server_options)
5959

@@ -152,7 +152,7 @@ def hotswap_server(
152152
return mount, server
153153

154154

155-
def find_available_port(host: str) -> int:
155+
def _find_available_port(host: str) -> int:
156156
"""Get a port that's available for the given host"""
157157
sock = socket()
158158
sock.bind((host, 0))

0 commit comments

Comments
 (0)