11from __future__ import annotations
22
3+ import filecmp
34import shutil
4- from dataclasses import dataclass
5+ from dataclasses import dataclass , replace
56from functools import partial
67from pathlib import Path
78from string import Template
9+ from tempfile import NamedTemporaryFile
810from typing import Any , List , NewType , Optional , Set , Tuple , Union , overload
911from urllib .parse import urlparse
1012
@@ -74,6 +76,9 @@ def module_from_url(
7476 )
7577
7678
79+ _FROM_TEMPLATE_DIR = "__from_template__"
80+
81+
7782def 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
166178def 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+
231257class _VdomDictConstructor (Protocol ):
232258 def __call__ (
233259 self ,
@@ -326,10 +352,8 @@ def _make_export(
326352 )
327353
328354
329- def _web_module_path (name : str , prefix : str = "" ) -> Path :
355+ def _web_module_path (name : str ) -> Path :
330356 name += module_name_suffix (name )
331357 directory = IDOM_WED_MODULES_DIR .current
332- if prefix :
333- directory /= prefix
334358 path = directory .joinpath (* name .split ("/" ))
335359 return path .with_suffix (path .suffix )
0 commit comments