Skip to content

Commit 88f0891

Browse files
committed
clean up web module from file and template logic
1 parent 0bf5877 commit 88f0891

File tree

3 files changed

+81
-41
lines changed

3 files changed

+81
-41
lines changed

docs/source/escape-hatches/_examples/super_simple_chart/app.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,7 @@
44

55

66
file = Path(__file__).parent / "super-simple-chart.js"
7-
ssc = web.module_from_file(
8-
"super-simple-chart",
9-
file,
10-
fallback="⌛",
11-
# normally this option is not required
12-
replace_existing=True,
13-
)
7+
ssc = web.module_from_file("super-simple-chart", file, fallback="⌛", symlink=True)
148
SuperSimpleChart = web.export(ssc, "SuperSimpleChart")
159

1610

src/idom/web/module.py

+57-31
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22

3+
import filecmp
34
import shutil
4-
from dataclasses import dataclass
5+
from dataclasses import dataclass, replace
56
from functools import partial
67
from pathlib import Path
78
from string import Template
9+
from tempfile import NamedTemporaryFile
810
from typing import Any, List, NewType, Optional, Set, Tuple, Union, overload
911
from urllib.parse import urlparse
1012

@@ -74,6 +76,9 @@ def module_from_url(
7476
)
7577

7678

79+
_FROM_TEMPLATE_DIR = "__from_template__"
80+
81+
7782
def module_from_template(
7883
template: str,
7984
package: str,
@@ -83,6 +88,7 @@ def module_from_template(
8388
resolve_exports: bool = IDOM_DEBUG_MODE.current,
8489
resolve_exports_depth: int = 5,
8590
unmount_before_update: bool = False,
91+
replace_existing: bool = False,
8692
) -> WebModule:
8793
"""Create a :class:`WebModule` from a framework template
8894
@@ -121,6 +127,9 @@ def module_from_template(
121127
only be used if the imported package failes to re-render when props change.
122128
Using this option has negative performance consequences since all DOM
123129
elements must be changed on each render. See :issue:`461` for more info.
130+
replace_existing:
131+
Whether to replace the source for a module with the same name if it already
132+
exists and has different content. Otherwise raise an error.
124133
"""
125134
# We do this since the package may be any valid URL path. Thus we may need to strip
126135
# object parameters or query information so we save the resulting template under the
@@ -140,27 +149,30 @@ def module_from_template(
140149
if not template_file.exists():
141150
raise ValueError(f"No template for {template_file_name!r} exists")
142151

143-
target_file = _web_module_path(package_name, "from-template")
144-
if not target_file.exists():
145-
target_file.parent.mkdir(parents=True, exist_ok=True)
146-
target_file.write_text(
147-
Template(template_file.read_text()).substitute(
148-
{"PACKAGE": package, "CDN": cdn}
149-
)
152+
variables = {"PACKAGE": package, "CDN": cdn}
153+
content = Template(template_file.read_text()).substitute(variables)
154+
155+
with NamedTemporaryFile(mode="r+") as file:
156+
file.write(content)
157+
file.seek(0) # set the cursor back to begining of file
158+
159+
module = module_from_file(
160+
(
161+
_FROM_TEMPLATE_DIR
162+
+ "/"
163+
+ package_name
164+
+ module_name_suffix(package_name)
165+
),
166+
file.name,
167+
fallback,
168+
resolve_exports,
169+
resolve_exports_depth,
170+
symlink=False,
171+
unmount_before_update=unmount_before_update,
172+
replace_existing=replace_existing,
150173
)
151174

152-
return WebModule(
153-
source="from-template/" + package_name + module_name_suffix(package_name),
154-
source_type=NAME_SOURCE,
155-
default_fallback=fallback,
156-
file=target_file,
157-
export_names=(
158-
resolve_module_exports_from_file(target_file, resolve_exports_depth)
159-
if resolve_exports
160-
else None
161-
),
162-
unmount_before_update=unmount_before_update,
163-
)
175+
return replace(module, file=None)
164176

165177

166178
def module_from_file(
@@ -196,23 +208,29 @@ def module_from_file(
196208
elements must be changed on each render. See :issue:`461` for more info.
197209
replace_existing:
198210
Whether to replace the source for a module with the same name if it already
199-
exists. Otherwise raise an error.
211+
exists and has different content. Otherwise raise an error.
200212
"""
201213
source_file = Path(file)
202214
target_file = _web_module_path(name)
203215
if not source_file.exists():
204216
raise FileNotFoundError(f"Source file does not exist: {source_file}")
205-
elif target_file.exists() or target_file.is_symlink():
206-
if not replace_existing:
207-
raise FileExistsError(f"{name!r} already exists as {target_file.resolve()}")
208-
else:
209-
target_file.unlink()
210217

211-
target_file.parent.mkdir(parents=True, exist_ok=True)
212-
if symlink:
213-
target_file.symlink_to(source_file)
214-
else:
215-
shutil.copy(source_file, target_file)
218+
if not target_file.exists():
219+
_copy_file(target_file, source_file, symlink)
220+
elif not (
221+
symlink
222+
and target_file.is_symlink()
223+
and target_file.resolve() == source_file.resolve()
224+
):
225+
if replace_existing:
226+
target_file.unlink()
227+
_copy_file(target_file, source_file, symlink)
228+
elif not filecmp.cmp(
229+
str(source_file.resolve()),
230+
str(target_file.resolve()),
231+
shallow=False,
232+
):
233+
raise FileExistsError(f"{name!r} already exists as {target_file.resolve()}")
216234

217235
return WebModule(
218236
source=name + module_name_suffix(name),
@@ -228,6 +246,14 @@ def module_from_file(
228246
)
229247

230248

249+
def _copy_file(target: Path, source: Path, symlink: bool) -> None:
250+
target.parent.mkdir(parents=True, exist_ok=True)
251+
if symlink:
252+
target.symlink_to(source)
253+
else:
254+
shutil.copy(source, target)
255+
256+
231257
class _VdomDictConstructor(Protocol):
232258
def __call__(
233259
self,

tests/test_web/test_module.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,14 @@ def test_module_from_file_source_conflict(tmp_path):
126126
second_file = tmp_path / "second.js"
127127
second_file.touch()
128128

129+
# ok, same content
130+
idom.web.module_from_file("temp", second_file)
131+
132+
third_file = tmp_path / "third.js"
133+
third_file.write_text("something-different")
134+
129135
with pytest.raises(FileExistsError, match="already exists"):
130-
idom.web.module_from_file("temp", second_file)
136+
idom.web.module_from_file("temp", third_file)
131137

132138

133139
def test_web_module_from_file_symlink(tmp_path):
@@ -143,14 +149,28 @@ def test_web_module_from_file_symlink(tmp_path):
143149
assert module.file.resolve().read_text() == "hello world!"
144150

145151

152+
def test_web_module_from_file_symlink_twice(tmp_path):
153+
file_1 = tmp_path / "temp_1.js"
154+
file_1.touch()
155+
156+
idom.web.module_from_file("temp", file_1, symlink=True)
157+
idom.web.module_from_file("temp", file_1, symlink=True)
158+
159+
file_2 = tmp_path / "temp_2.js"
160+
file_2.write_text("something")
161+
162+
with pytest.raises(FileExistsError, match="already exists"):
163+
idom.web.module_from_file("temp", file_2, symlink=True)
164+
165+
146166
def test_web_module_from_file_replace_existing(tmp_path):
147167
file1 = tmp_path / "temp1.js"
148168
file1.touch()
149169

150170
idom.web.module_from_file("temp", file1)
151171

152172
file2 = tmp_path / "temp2.js"
153-
file2.touch()
173+
file2.write_text("something")
154174

155175
with pytest.raises(FileExistsError, match="already exists"):
156176
idom.web.module_from_file("temp", file2)
@@ -200,7 +220,7 @@ def test_imported_components_can_render_children(driver, display):
200220
)
201221

202222
parent = driver.find_element("id", "the-parent")
203-
children = parent.find_elements_by_tag_name("li")
223+
children = parent.find_elements("tag name", "li")
204224

205225
assert len(children) == 3
206226

0 commit comments

Comments
 (0)