reactpy 2.0.0b6__py3-none-any.whl → 2.0.0b8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. reactpy/__init__.py +2 -2
  2. reactpy/config.py +1 -1
  3. reactpy/core/_thread_local.py +1 -1
  4. reactpy/core/hooks.py +34 -37
  5. reactpy/executors/asgi/pyscript.py +4 -1
  6. reactpy/executors/asgi/standalone.py +1 -1
  7. reactpy/{pyscript → executors/pyscript}/component_template.py +1 -1
  8. reactpy/{pyscript → executors/pyscript}/components.py +1 -1
  9. reactpy/executors/utils.py +32 -7
  10. reactpy/reactjs/__init__.py +5 -7
  11. reactpy/reactjs/module.py +106 -42
  12. reactpy/reactjs/utils.py +49 -20
  13. reactpy/static/{index-sbddj6ms.js → index-64wy0fss.js} +4 -4
  14. reactpy/static/{index-sbddj6ms.js.map → index-64wy0fss.js.map} +1 -1
  15. reactpy/static/index-beq660xy.js +5 -0
  16. reactpy/static/index-beq660xy.js.map +12 -0
  17. reactpy/static/index.js +2 -2
  18. reactpy/static/index.js.map +6 -5
  19. reactpy/static/preact-dom.js +4 -0
  20. reactpy/static/{react-dom.js.map → preact-dom.js.map} +3 -3
  21. reactpy/static/preact-jsx-runtime.js +4 -0
  22. reactpy/static/{react-jsx-runtime.js.map → preact-jsx-runtime.js.map} +1 -1
  23. reactpy/static/preact.js +4 -0
  24. reactpy/static/{react.js.map → preact.js.map} +3 -3
  25. reactpy/templatetags/jinja.py +4 -1
  26. reactpy/testing/__init__.py +2 -7
  27. reactpy/testing/backend.py +20 -8
  28. reactpy/testing/common.py +1 -9
  29. reactpy/testing/display.py +68 -32
  30. {reactpy-2.0.0b6.dist-info → reactpy-2.0.0b8.dist-info}/METADATA +1 -1
  31. {reactpy-2.0.0b6.dist-info → reactpy-2.0.0b8.dist-info}/RECORD +37 -40
  32. reactpy/static/index-h31022cd.js +0 -5
  33. reactpy/static/index-h31022cd.js.map +0 -11
  34. reactpy/static/index-y71bxs88.js +0 -5
  35. reactpy/static/index-y71bxs88.js.map +0 -10
  36. reactpy/static/react-dom.js +0 -4
  37. reactpy/static/react-jsx-runtime.js +0 -4
  38. reactpy/static/react.js +0 -4
  39. reactpy/testing/utils.py +0 -27
  40. /reactpy/{pyscript → executors/pyscript}/__init__.py +0 -0
  41. /reactpy/{pyscript → executors/pyscript}/layout_handler.py +0 -0
  42. /reactpy/{pyscript → executors/pyscript}/utils.py +0 -0
  43. {reactpy-2.0.0b6.dist-info → reactpy-2.0.0b8.dist-info}/WHEEL +0 -0
  44. {reactpy-2.0.0b6.dist-info → reactpy-2.0.0b8.dist-info}/entry_points.txt +0 -0
  45. {reactpy-2.0.0b6.dist-info → reactpy-2.0.0b8.dist-info}/licenses/LICENSE +0 -0
reactpy/__init__.py CHANGED
@@ -19,11 +19,11 @@ from reactpy.core.hooks import (
19
19
  use_state,
20
20
  )
21
21
  from reactpy.core.vdom import Vdom
22
- from reactpy.pyscript.components import pyscript_component
22
+ from reactpy.executors.pyscript.components import pyscript_component
23
23
  from reactpy.utils import Ref, reactpy_to_string, string_to_reactpy
24
24
 
25
25
  __author__ = "The Reactive Python Team"
26
- __version__ = "2.0.0b6"
26
+ __version__ = "2.0.0b8"
27
27
 
28
28
  __all__ = [
29
29
  "Ref",
reactpy/config.py CHANGED
@@ -77,7 +77,7 @@ set of publicly available APIs for working with the client.
77
77
 
78
78
  REACTPY_TESTS_DEFAULT_TIMEOUT = Option(
79
79
  "REACTPY_TESTS_DEFAULT_TIMEOUT",
80
- 10.0,
80
+ 15.0,
81
81
  mutable=False,
82
82
  validator=float,
83
83
  )
@@ -8,7 +8,7 @@ _StateType = TypeVar("_StateType")
8
8
 
9
9
  class ThreadLocal(Generic[_StateType]): # nocov
10
10
  """Utility for managing per-thread state information. This is only used in
11
- environments where ContextVars are not available, such as the `pyodide`
11
+ environments where ContextVars are not available, such as the `pyscript`
12
12
  executor."""
13
13
 
14
14
  def __init__(self, default: Callable[[], _StateType]):
reactpy/core/hooks.py CHANGED
@@ -84,11 +84,7 @@ class _CurrentState(Generic[_Type]):
84
84
  self,
85
85
  initial_value: _Type | Callable[[], _Type],
86
86
  ) -> None:
87
- if callable(initial_value):
88
- self.value = initial_value()
89
- else:
90
- self.value = initial_value
91
-
87
+ self.value = initial_value() if callable(initial_value) else initial_value
92
88
  hook = HOOK_STACK.current_hook()
93
89
 
94
90
  def dispatch(new: _Type | Callable[[_Type], _Type]) -> None:
@@ -186,7 +182,6 @@ def use_effect(
186
182
  def use_async_effect(
187
183
  function: None = None,
188
184
  dependencies: Sequence[Any] | ellipsis | None = ...,
189
- shutdown_timeout: float = 0.1,
190
185
  ) -> Callable[[_EffectApplyFunc], None]: ...
191
186
 
192
187
 
@@ -194,14 +189,12 @@ def use_async_effect(
194
189
  def use_async_effect(
195
190
  function: _AsyncEffectFunc,
196
191
  dependencies: Sequence[Any] | ellipsis | None = ...,
197
- shutdown_timeout: float = 0.1,
198
192
  ) -> None: ...
199
193
 
200
194
 
201
195
  def use_async_effect(
202
196
  function: _AsyncEffectFunc | None = None,
203
197
  dependencies: Sequence[Any] | ellipsis | None = ...,
204
- shutdown_timeout: float = 0.1,
205
198
  ) -> Callable[[_AsyncEffectFunc], None] | None:
206
199
  """
207
200
  A hook that manages an asynchronous side effect in a React-like component.
@@ -218,9 +211,6 @@ def use_async_effect(
218
211
  of any value in the given sequence changes (i.e. their :func:`id` is
219
212
  different). By default these are inferred based on local variables that are
220
213
  referenced by the given function.
221
- shutdown_timeout:
222
- The amount of time (in seconds) to wait for the effect to complete before
223
- forcing a shutdown.
224
214
 
225
215
  Returns:
226
216
  If not function is provided, a decorator. Otherwise ``None``.
@@ -236,26 +226,37 @@ def use_async_effect(
236
226
  # always clean up the previous effect's resources
237
227
  run_effect_cleanup(cleanup_func)
238
228
 
239
- # Execute the effect in a background task
229
+ # Execute the effect and store the clean-up function.
230
+ # We run this in a task so it can be cancelled if the stop signal
231
+ # is set before the effect completes.
240
232
  task = asyncio.create_task(func())
241
233
 
242
- # Wait until we get the signal to stop this effect
243
- await stop.wait()
234
+ # Wait for either the effect to complete or the stop signal
235
+ stop_task = asyncio.create_task(stop.wait())
236
+ done, _ = await asyncio.wait(
237
+ [task, stop_task],
238
+ return_when=asyncio.FIRST_COMPLETED,
239
+ )
244
240
 
245
- # If renders are queued back-to-back, the effect might not have
246
- # completed. So, we give the task a small amount of time to finish.
247
- # If it manages to finish, we can obtain a clean-up function.
248
- results, _ = await asyncio.wait([task], timeout=shutdown_timeout)
249
- if results:
250
- cleanup_func.current = results.pop().result()
241
+ # If the effect completed first, store the cleanup function
242
+ if task in done:
243
+ cleanup_func.current = task.result()
244
+ # Cancel the stop task since we don't need it anymore
245
+ stop_task.cancel()
246
+ with contextlib.suppress(asyncio.CancelledError):
247
+ await stop_task
248
+ # Now wait for the stop signal to run cleanup
249
+ await stop.wait()
250
+ else:
251
+ # Stop signal came first - cancel the effect task
252
+ task.cancel()
253
+ with contextlib.suppress(asyncio.CancelledError):
254
+ await task
251
255
 
252
256
  # Run the clean-up function when the effect is stopped,
253
257
  # if it hasn't been run already by a new effect
254
258
  run_effect_cleanup(cleanup_func)
255
259
 
256
- # Cancel the task if it's still running
257
- task.cancel()
258
-
259
260
  return memoize(lambda: hook.add_effect(effect))
260
261
 
261
262
  # Handle decorator usage
@@ -434,10 +435,7 @@ def use_callback(
434
435
  def setup(function: _CallbackFunc) -> _CallbackFunc:
435
436
  return memoize(lambda: function)
436
437
 
437
- if function is not None:
438
- return setup(function)
439
- else:
440
- return setup
438
+ return setup(function) if function is not None else setup
441
439
 
442
440
 
443
441
  class _LambdaCaller(Protocol):
@@ -553,17 +551,16 @@ def _try_to_infer_closure_values(
553
551
  func: Callable[..., Any] | None,
554
552
  values: Sequence[Any] | ellipsis | None,
555
553
  ) -> Sequence[Any] | None:
556
- if values is ...:
557
- if isinstance(func, FunctionType):
558
- return (
559
- [cell.cell_contents for cell in func.__closure__]
560
- if func.__closure__
561
- else []
562
- )
563
- else:
564
- return None
565
- else:
554
+ if values is not ...:
566
555
  return values
556
+ if isinstance(func, FunctionType):
557
+ return (
558
+ [cell.cell_contents for cell in func.__closure__]
559
+ if func.__closure__
560
+ else []
561
+ )
562
+ else:
563
+ return None
567
564
 
568
565
 
569
566
  def strictly_equal(x: Any, y: Any) -> bool:
@@ -13,8 +13,11 @@ from reactpy import html
13
13
  from reactpy.executors.asgi.middleware import ReactPyMiddleware
14
14
  from reactpy.executors.asgi.standalone import ReactPy, ReactPyApp
15
15
  from reactpy.executors.asgi.types import AsgiWebsocketScope
16
+ from reactpy.executors.pyscript.utils import (
17
+ pyscript_component_html,
18
+ pyscript_setup_html,
19
+ )
16
20
  from reactpy.executors.utils import vdom_head_to_html
17
- from reactpy.pyscript.utils import pyscript_component_html, pyscript_setup_html
18
21
  from reactpy.types import ReactPyConfig, VdomDict
19
22
 
20
23
 
@@ -23,8 +23,8 @@ from reactpy.executors.asgi.types import (
23
23
  AsgiV3WebsocketApp,
24
24
  AsgiWebsocketScope,
25
25
  )
26
+ from reactpy.executors.pyscript.utils import pyscript_setup_html
26
27
  from reactpy.executors.utils import server_side_component_html, vdom_head_to_html
27
- from reactpy.pyscript.utils import pyscript_setup_html
28
28
  from reactpy.types import (
29
29
  PyScriptOptions,
30
30
  ReactPyConfig,
@@ -2,7 +2,7 @@
2
2
  # type: ignore
3
3
  import asyncio
4
4
 
5
- from reactpy.pyscript.layout_handler import ReactPyLayoutHandler
5
+ from reactpy.executors.pyscript.layout_handler import ReactPyLayoutHandler
6
6
 
7
7
 
8
8
  # User component is inserted below by regex replacement
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  from reactpy import component, hooks
7
- from reactpy.pyscript.utils import pyscript_component_html
7
+ from reactpy.executors.pyscript.utils import pyscript_component_html
8
8
  from reactpy.types import Component, Key
9
9
  from reactpy.utils import string_to_reactpy
10
10
 
@@ -64,16 +64,41 @@ def server_side_component_html(
64
64
  ) -> str:
65
65
  return (
66
66
  f'<div id="{element_id}" class="{class_}"></div>'
67
+ "<script>"
68
+ 'if (!document.querySelector("#reactpy-importmap")) {'
69
+ " console.debug("
70
+ ' "ReactPy did not detect a suitable JavaScript import map. Falling back to ReactPy\'s internal framework (Preact)."'
71
+ " );"
72
+ ' const im = document.createElement("script");'
73
+ ' im.type = "importmap";'
74
+ f" im.textContent = '{default_import_map()}';"
75
+ ' im.id = "reactpy-importmap";'
76
+ " document.head.appendChild(im);"
77
+ " delete im;"
78
+ "}"
79
+ "</script>"
67
80
  '<script type="module" crossorigin="anonymous">'
68
81
  f'import {{ mountReactPy }} from "{REACTPY_PATH_PREFIX.current}static/index.js";'
69
82
  "mountReactPy({"
70
- f' mountElement: document.getElementById("{element_id}"),'
71
- f' pathPrefix: "{REACTPY_PATH_PREFIX.current}",'
72
- f' componentPath: "{component_path}",'
73
- f" reconnectInterval: {REACTPY_RECONNECT_INTERVAL.current},"
74
- f" reconnectMaxInterval: {REACTPY_RECONNECT_MAX_INTERVAL.current},"
75
- f" reconnectMaxRetries: {REACTPY_RECONNECT_MAX_RETRIES.current},"
76
- f" reconnectBackoffMultiplier: {REACTPY_RECONNECT_BACKOFF_MULTIPLIER.current},"
83
+ f' mountElement: document.getElementById("{element_id}"),'
84
+ f' pathPrefix: "{REACTPY_PATH_PREFIX.current}",'
85
+ f' componentPath: "{component_path}",'
86
+ f" reconnectInterval: {REACTPY_RECONNECT_INTERVAL.current},"
87
+ f" reconnectMaxInterval: {REACTPY_RECONNECT_MAX_INTERVAL.current},"
88
+ f" reconnectMaxRetries: {REACTPY_RECONNECT_MAX_RETRIES.current},"
89
+ f" reconnectBackoffMultiplier: {REACTPY_RECONNECT_BACKOFF_MULTIPLIER.current},"
77
90
  "});"
78
91
  "</script>"
79
92
  )
93
+
94
+
95
+ def default_import_map() -> str:
96
+ path_prefix = REACTPY_PATH_PREFIX.current.strip("/")
97
+ return f"""{{
98
+ "imports": {{
99
+ "react": "/{path_prefix}/static/preact.js",
100
+ "react-dom": "/{path_prefix}/static/preact-dom.js",
101
+ "react-dom/client": "/{path_prefix}/static/preact-dom.js",
102
+ "react/jsx-runtime": "/{path_prefix}/static/preact-jsx-runtime.js"
103
+ }}
104
+ }}""".replace("\n", "").replace(" ", "")
@@ -110,7 +110,7 @@ def component_from_npm(
110
110
  resolve_imports: bool = ...,
111
111
  resolve_imports_depth: int = ...,
112
112
  version: str = "latest",
113
- cdn: str = "https://esm.sh",
113
+ cdn: str = "https://esm.sh/v135",
114
114
  fallback: Any | None = ...,
115
115
  unmount_before_update: bool = ...,
116
116
  allow_children: bool = ...,
@@ -124,7 +124,7 @@ def component_from_npm(
124
124
  resolve_imports: bool = ...,
125
125
  resolve_imports_depth: int = ...,
126
126
  version: str = "latest",
127
- cdn: str = "https://esm.sh",
127
+ cdn: str = "https://esm.sh/v135",
128
128
  fallback: Any | None = ...,
129
129
  unmount_before_update: bool = ...,
130
130
  allow_children: bool = ...,
@@ -137,7 +137,7 @@ def component_from_npm(
137
137
  resolve_imports: bool = False,
138
138
  resolve_imports_depth: int = 5,
139
139
  version: str = "latest",
140
- cdn: str = "https://esm.sh",
140
+ cdn: str = "https://esm.sh/v135",
141
141
  fallback: Any | None = None,
142
142
  unmount_before_update: bool = False,
143
143
  allow_children: bool = True,
@@ -175,10 +175,8 @@ def component_from_npm(
175
175
  url = f"{cdn}/{package}@{version}"
176
176
 
177
177
  if "esm.sh" in cdn:
178
- if "?" in url:
179
- url += "&external=react,react-dom"
180
- else:
181
- url += "?external=react,react-dom"
178
+ url += "&" if "?" in url else "?"
179
+ url += "external=react,react-dom,react/jsx-runtime&bundle&target=es2020"
182
180
 
183
181
  return component_from_url(
184
182
  url,
reactpy/reactjs/module.py CHANGED
@@ -1,21 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from pathlib import Path
5
- from typing import Any
4
+ from pathlib import Path, PurePosixPath
5
+ from typing import Any, Literal
6
6
 
7
- from reactpy import config
8
- from reactpy.config import REACTPY_WEB_MODULES_DIR
7
+ from reactpy.config import REACTPY_DEBUG, REACTPY_WEB_MODULES_DIR
9
8
  from reactpy.core.vdom import Vdom
10
9
  from reactpy.reactjs.types import NAME_SOURCE, URL_SOURCE
11
10
  from reactpy.reactjs.utils import (
12
11
  are_files_identical,
13
12
  copy_file,
14
- module_name_suffix,
15
- resolve_from_module_file,
16
- resolve_from_module_url,
13
+ file_lock,
14
+ resolve_names_from_file,
15
+ resolve_names_from_url,
17
16
  )
18
- from reactpy.types import ImportSourceDict, JavaScriptModule, VdomConstructor
17
+ from reactpy.types import ImportSourceDict, JavaScriptModule, VdomConstructor, VdomDict
19
18
 
20
19
  logger = logging.getLogger(__name__)
21
20
 
@@ -33,7 +32,7 @@ def url_to_module(
33
32
  default_fallback=fallback,
34
33
  file=None,
35
34
  import_names=(
36
- resolve_from_module_url(url, resolve_imports_depth)
35
+ resolve_names_from_url(url, resolve_imports_depth)
37
36
  if resolve_imports
38
37
  else None
39
38
  ),
@@ -54,19 +53,20 @@ def file_to_module(
54
53
 
55
54
  source_file = Path(file).resolve()
56
55
  target_file = get_module_path(name)
57
- if not source_file.exists():
58
- msg = f"Source file does not exist: {source_file}"
59
- raise FileNotFoundError(msg)
60
56
 
61
- if not target_file.exists():
62
- copy_file(target_file, source_file, symlink)
63
- elif not are_files_identical(source_file, target_file):
64
- logger.info(
65
- f"Existing web module {name!r} will "
66
- f"be replaced with {target_file.resolve()}"
67
- )
68
- target_file.unlink()
69
- copy_file(target_file, source_file, symlink)
57
+ with file_lock(target_file.with_name(f"{target_file.name}.lock")):
58
+ if not source_file.exists():
59
+ msg = f"Source file does not exist: {source_file}"
60
+ raise FileNotFoundError(msg)
61
+
62
+ if not target_file.exists():
63
+ copy_file(target_file, source_file, symlink)
64
+ elif not are_files_identical(source_file, target_file):
65
+ logger.info(
66
+ f"Existing web module {name!r} will "
67
+ f"be replaced with {target_file.resolve()}"
68
+ )
69
+ copy_file(target_file, source_file, symlink)
70
70
 
71
71
  return JavaScriptModule(
72
72
  source=name,
@@ -74,7 +74,7 @@ def file_to_module(
74
74
  default_fallback=fallback,
75
75
  file=target_file,
76
76
  import_names=(
77
- resolve_from_module_file(source_file, resolve_imports_depth)
77
+ resolve_names_from_file(source_file, resolve_imports_depth)
78
78
  if resolve_imports
79
79
  else None
80
80
  ),
@@ -110,7 +110,7 @@ def string_to_module(
110
110
  default_fallback=fallback,
111
111
  file=target_file,
112
112
  import_names=(
113
- resolve_from_module_file(target_file, resolve_imports_depth)
113
+ resolve_names_from_file(target_file, resolve_imports_depth)
114
114
  if resolve_imports
115
115
  else None
116
116
  ),
@@ -178,26 +178,90 @@ def make_module(
178
178
  )
179
179
 
180
180
 
181
+ def import_reactjs(
182
+ framework: Literal["preact", "react"] | None = None,
183
+ version: str | None = None,
184
+ use_local: bool = False,
185
+ ) -> VdomDict:
186
+ """
187
+ Return an import map script tag for ReactJS or Preact.
188
+ Parameters:
189
+ framework:
190
+ The framework to use, either "preact" or "react". Defaults to "preact" for
191
+ performance reasons. Set this to `react` if you are experiencing compatibility
192
+ issues with your component library.
193
+ version:
194
+ The version of the framework to use. Example values include "18", "10.2.4",
195
+ or "latest". If left as `None`, a default version will be used depending on the
196
+ selected framework.
197
+ use_local:
198
+ Whether to use the local framework ReactPy is bundled with (Preact).
199
+ Raises:
200
+ ValueError:
201
+ If both `framework` and `react_url_prefix` are provided, or if
202
+ `framework` is not one of "preact" or "react".
203
+ Returns:
204
+ A VDOM script tag containing the import map.
205
+ """
206
+ from reactpy import html
207
+ from reactpy.executors.utils import default_import_map
208
+
209
+ if use_local and (framework or version): # nocov
210
+ raise ValueError("use_local cannot be used with framework or version")
211
+
212
+ framework = framework or "preact"
213
+ if framework and framework not in {"preact", "react"}: # nocov
214
+ raise ValueError("framework must be 'preact' or 'react'")
215
+
216
+ # Import map for ReactPy's local framework (re-exported/bundled/minified version of Preact)
217
+ if use_local:
218
+ return html.script(
219
+ {"type": "importmap", "id": "reactpy-importmap"},
220
+ default_import_map(),
221
+ )
222
+
223
+ # Import map for ReactJS from esm.sh
224
+ if framework == "react":
225
+ version = version or "19"
226
+ postfix = "?dev" if REACTPY_DEBUG.current else ""
227
+ return html.script(
228
+ {"type": "importmap", "id": "reactpy-importmap"},
229
+ f"""{{
230
+ "imports": {{
231
+ "react": "https://esm.sh/v135/react@{version}{postfix}",
232
+ "react-dom": "https://esm.sh/v135/react-dom@{version}{postfix}",
233
+ "react-dom/client": "https://esm.sh/v135/react-dom@{version}/client{postfix}",
234
+ "react/jsx-runtime": "https://esm.sh/v135/react@{version}/jsx-runtime{postfix}"
235
+ }}
236
+ }}""".replace("\n", "").replace(" ", ""),
237
+ )
238
+
239
+ # Import map for Preact from esm.sh
240
+ if framework == "preact":
241
+ version = version or "10"
242
+ postfix = "?dev" if REACTPY_DEBUG.current else ""
243
+ return html.script(
244
+ {"type": "importmap", "id": "reactpy-importmap"},
245
+ f"""{{
246
+ "imports": {{
247
+ "react": "https://esm.sh/v135/preact@{version}/compat{postfix}",
248
+ "react-dom": "https://esm.sh/v135/preact@{version}/compat{postfix}",
249
+ "react-dom/client": "https://esm.sh/v135/preact@{version}/compat/client{postfix}",
250
+ "react/jsx-runtime": "https://esm.sh/v135/preact@{version}/compat/jsx-runtime{postfix}"
251
+ }}
252
+ }}""".replace("\n", "").replace(" ", ""),
253
+ )
254
+
255
+
256
+ def module_name_suffix(name: str) -> str:
257
+ if name.startswith("@"):
258
+ name = name[1:]
259
+ head, _, tail = name.partition("@") # handle version identifier
260
+ _, _, tail = tail.partition("/") # get section after version
261
+ return PurePosixPath(tail or head).suffix or ".js"
262
+
263
+
181
264
  def get_module_path(name: str) -> Path:
182
265
  directory = REACTPY_WEB_MODULES_DIR.current
183
266
  path = directory.joinpath(*name.split("/"))
184
267
  return path.with_suffix(path.suffix)
185
-
186
-
187
- def import_reactjs():
188
- from reactpy import html
189
-
190
- base_url = config.REACTPY_PATH_PREFIX.current.strip("/")
191
- return html.script(
192
- {"type": "importmap"},
193
- f"""
194
- {{
195
- "imports": {{
196
- "react": "/{base_url}/static/react.js",
197
- "react-dom": "/{base_url}/static/react-dom.js",
198
- "react-dom/client": "/{base_url}/static/react-dom.js",
199
- "react/jsx-runtime": "/{base_url}/static/react-jsx-runtime.js"
200
- }}
201
- }}
202
- """,
203
- )
reactpy/reactjs/utils.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import filecmp
2
2
  import logging
3
+ import os
3
4
  import re
4
5
  import shutil
5
- from pathlib import Path, PurePosixPath
6
+ import time
7
+ from contextlib import contextmanager, suppress
8
+ from pathlib import Path
6
9
  from urllib.parse import urlparse, urlunparse
7
10
 
8
11
  import requests
@@ -10,15 +13,7 @@ import requests
10
13
  logger = logging.getLogger(__name__)
11
14
 
12
15
 
13
- def module_name_suffix(name: str) -> str:
14
- if name.startswith("@"):
15
- name = name[1:]
16
- head, _, tail = name.partition("@") # handle version identifier
17
- _, _, tail = tail.partition("/") # get section after version
18
- return PurePosixPath(tail or head).suffix or ".js"
19
-
20
-
21
- def resolve_from_module_file(
16
+ def resolve_names_from_file(
22
17
  file: Path,
23
18
  max_depth: int,
24
19
  is_regex_import: bool = False,
@@ -30,25 +25,25 @@ def resolve_from_module_file(
30
25
  logger.warning(f"Did not resolve imports for unknown file {file}")
31
26
  return set()
32
27
 
33
- names, references = resolve_from_module_source(
28
+ names, references = resolve_names_from_source(
34
29
  file.read_text(encoding="utf-8"), exclude_default=is_regex_import
35
30
  )
36
31
 
37
32
  for ref in references:
38
33
  if urlparse(ref).scheme: # is an absolute URL
39
34
  names.update(
40
- resolve_from_module_url(ref, max_depth - 1, is_regex_import=True)
35
+ resolve_names_from_url(ref, max_depth - 1, is_regex_import=True)
41
36
  )
42
37
  else:
43
38
  path = file.parent.joinpath(*ref.split("/"))
44
39
  names.update(
45
- resolve_from_module_file(path, max_depth - 1, is_regex_import=True)
40
+ resolve_names_from_file(path, max_depth - 1, is_regex_import=True)
46
41
  )
47
42
 
48
43
  return names
49
44
 
50
45
 
51
- def resolve_from_module_url(
46
+ def resolve_names_from_url(
52
47
  url: str,
53
48
  max_depth: int,
54
49
  is_regex_import: bool = False,
@@ -64,18 +59,16 @@ def resolve_from_module_url(
64
59
  logger.warning(f"Did not resolve imports for url {url} {reason}")
65
60
  return set()
66
61
 
67
- names, references = resolve_from_module_source(
68
- text, exclude_default=is_regex_import
69
- )
62
+ names, references = resolve_names_from_source(text, exclude_default=is_regex_import)
70
63
 
71
64
  for ref in references:
72
65
  url = normalize_url_path(url, ref)
73
- names.update(resolve_from_module_url(url, max_depth - 1, is_regex_import=True))
66
+ names.update(resolve_names_from_url(url, max_depth - 1, is_regex_import=True))
74
67
 
75
68
  return names
76
69
 
77
70
 
78
- def resolve_from_module_source(
71
+ def resolve_names_from_source(
79
72
  content: str, exclude_default: bool
80
73
  ) -> tuple[set[str], set[str]]:
81
74
  """Find names exported by the given JavaScript module content to assist with ReactPy import resolution.
@@ -167,9 +160,26 @@ def are_files_identical(f1: Path, f2: Path) -> bool:
167
160
  def copy_file(target: Path, source: Path, symlink: bool) -> None:
168
161
  target.parent.mkdir(parents=True, exist_ok=True)
169
162
  if symlink:
163
+ if target.exists():
164
+ target.unlink()
170
165
  target.symlink_to(source)
171
166
  else:
172
- shutil.copy(source, target)
167
+ temp_target = target.with_suffix(f"{target.suffix}.tmp")
168
+ shutil.copy(source, temp_target)
169
+ try:
170
+ temp_target.replace(target)
171
+ except OSError:
172
+ # On Windows, replace might fail if the file is open
173
+ # Retry once after a short delay
174
+ time.sleep(0.1)
175
+ try:
176
+ temp_target.replace(target)
177
+ except OSError:
178
+ # If it still fails, try to unlink and rename
179
+ # This is not atomic, but it's a fallback
180
+ if target.exists():
181
+ target.unlink()
182
+ temp_target.rename(target)
173
183
 
174
184
 
175
185
  _JS_DEFAULT_EXPORT_PATTERN = re.compile(
@@ -181,3 +191,22 @@ _JS_FUNC_OR_CLS_EXPORT_PATTERN = re.compile(
181
191
  _JS_GENERAL_EXPORT_PATTERN = re.compile(
182
192
  r"(?:^|;|})\s*export(?=\s+|{)(.*?)(?=;|$)", re.MULTILINE
183
193
  )
194
+
195
+
196
+ @contextmanager
197
+ def file_lock(lock_file: Path, timeout: float = 10.0):
198
+ start_time = time.time()
199
+ while True:
200
+ try:
201
+ fd = os.open(lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR)
202
+ os.close(fd)
203
+ break
204
+ except OSError as e:
205
+ if time.time() - start_time > timeout:
206
+ raise TimeoutError(f"Could not acquire lock {lock_file}") from e
207
+ time.sleep(0.1)
208
+ try:
209
+ yield
210
+ finally:
211
+ with suppress(OSError):
212
+ os.unlink(lock_file)
@@ -1,5 +1,5 @@
1
- import{V as B,Y as D}from"./index-y71bxs88.js";var G=/["&<]/;function C(b){if(b.length===0||G.test(b)===!1)return b;for(var h=0,k=0,y="",q="";k<b.length;k++){switch(b.charCodeAt(k)){case 34:q="&quot;";break;case 38:q="&amp;";break;case 60:q="&lt;";break;default:continue}k!==h&&(y+=b.slice(h,k)),y+=q,h=k+1}return k!==h&&(y+=b.slice(h,k)),y}var H=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,I=0,J=Array.isArray;function K(b,h,k,y,q,A){h||(h={});var z,v,w=h;if("ref"in w)for(v in w={},h)v=="ref"?z=h[v]:w[v]=h[v];var E={type:b,props:w,key:k,ref:z,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--I,__i:-1,__u:0,__source:q,__self:A};if(typeof b=="function"&&(z=b.defaultProps))for(v in z)w[v]===void 0&&(w[v]=z[v]);return B.vnode&&B.vnode(E),E}function Q(b){var h=K(D,{tpl:b,exprs:[].slice.call(arguments,1)});return h.key=h.__v,h}var F={},N=/[A-Z]/g;function S(b,h){if(B.attr){var k=B.attr(b,h);if(typeof k=="string")return k}if(h=function(w){return w!==null&&typeof w=="object"&&typeof w.valueOf=="function"?w.valueOf():w}(h),b==="ref"||b==="key")return"";if(b==="style"&&typeof h=="object"){var y="";for(var q in h){var A=h[q];if(A!=null&&A!==""){var z=q[0]=="-"?q:F[q]||(F[q]=q.replace(N,"-$&").toLowerCase()),v=";";typeof A!="number"||z.startsWith("--")||H.test(z)||(v="px;"),y=y+z+":"+A+v}}return b+'="'+C(y)+'"'}return h==null||h===!1||typeof h=="function"||typeof h=="object"?"":h===!0?b:b+'="'+C(""+h)+'"'}function O(b){if(b==null||typeof b=="boolean"||typeof b=="function")return null;if(typeof b=="object"){if(b.constructor===void 0)return b;if(J(b)){for(var h=0;h<b.length;h++)b[h]=O(b[h]);return b}}return C(""+b)}
2
- export{K as P,Q,S as R,O as S};
1
+ import{h as B,k as D}from"./index-beq660xy.js";var G=/["&<]/;function C(b){if(b.length===0||G.test(b)===!1)return b;for(var h=0,k=0,y="",q="";k<b.length;k++){switch(b.charCodeAt(k)){case 34:q="&quot;";break;case 38:q="&amp;";break;case 60:q="&lt;";break;default:continue}k!==h&&(y+=b.slice(h,k)),y+=q,h=k+1}return k!==h&&(y+=b.slice(h,k)),y}var H=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,I=0,J=Array.isArray;function K(b,h,k,y,q,A){h||(h={});var z,v,w=h;if("ref"in w)for(v in w={},h)v=="ref"?z=h[v]:w[v]=h[v];var E={type:b,props:w,key:k,ref:z,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--I,__i:-1,__u:0,__source:q,__self:A};if(typeof b=="function"&&(z=b.defaultProps))for(v in z)w[v]===void 0&&(w[v]=z[v]);return B.vnode&&B.vnode(E),E}function Q(b){var h=K(D,{tpl:b,exprs:[].slice.call(arguments,1)});return h.key=h.__v,h}var F={},N=/[A-Z]/g;function S(b,h){if(B.attr){var k=B.attr(b,h);if(typeof k=="string")return k}if(h=function(w){return w!==null&&typeof w=="object"&&typeof w.valueOf=="function"?w.valueOf():w}(h),b==="ref"||b==="key")return"";if(b==="style"&&typeof h=="object"){var y="";for(var q in h){var A=h[q];if(A!=null&&A!==""){var z=q[0]=="-"?q:F[q]||(F[q]=q.replace(N,"-$&").toLowerCase()),v=";";typeof A!="number"||z.startsWith("--")||H.test(z)||(v="px;"),y=y+z+":"+A+v}}return b+'="'+C(y)+'"'}return h==null||h===!1||typeof h=="function"||typeof h=="object"?"":h===!0?b:b+'="'+C(""+h)+'"'}function O(b){if(b==null||typeof b=="boolean"||typeof b=="function")return null;if(typeof b=="object"){if(b.constructor===void 0)return b;if(J(b)){for(var h=0;h<b.length;h++)b[h]=O(b[h]);return b}}return C(""+b)}
2
+ export{K as a,Q as b,S as c,O as d};
3
3
 
4
- //# debugId=2B835D6D3E73856C64756E2164756E21
5
- //# sourceMappingURL=index-sbddj6ms.js.map
4
+ //# debugId=87FF749D2D1F353A64756E2164756E21
5
+ //# sourceMappingURL=index-64wy0fss.js.map
@@ -5,6 +5,6 @@
5
5
  "import{options as r,Fragment as e}from\"preact\";export{Fragment}from\"preact\";var t=/[\"&<]/;function n(r){if(0===r.length||!1===t.test(r))return r;for(var e=0,n=0,o=\"\",f=\"\";n<r.length;n++){switch(r.charCodeAt(n)){case 34:f=\"&quot;\";break;case 38:f=\"&amp;\";break;case 60:f=\"&lt;\";break;default:continue}n!==e&&(o+=r.slice(e,n)),o+=f,e=n+1}return n!==e&&(o+=r.slice(e,n)),o}var o=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,f=0,i=Array.isArray;function u(e,t,n,o,i,u){t||(t={});var a,c,p=t;if(\"ref\"in p)for(c in p={},t)\"ref\"==c?a=t[c]:p[c]=t[c];var l={type:e,props:p,key:n,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--f,__i:-1,__u:0,__source:i,__self:u};if(\"function\"==typeof e&&(a=e.defaultProps))for(c in a)void 0===p[c]&&(p[c]=a[c]);return r.vnode&&r.vnode(l),l}function a(r){var t=u(e,{tpl:r,exprs:[].slice.call(arguments,1)});return t.key=t.__v,t}var c={},p=/[A-Z]/g;function l(e,t){if(r.attr){var f=r.attr(e,t);if(\"string\"==typeof f)return f}if(t=function(r){return null!==r&&\"object\"==typeof r&&\"function\"==typeof r.valueOf?r.valueOf():r}(t),\"ref\"===e||\"key\"===e)return\"\";if(\"style\"===e&&\"object\"==typeof t){var i=\"\";for(var u in t){var a=t[u];if(null!=a&&\"\"!==a){var l=\"-\"==u[0]?u:c[u]||(c[u]=u.replace(p,\"-$&\").toLowerCase()),s=\";\";\"number\"!=typeof a||l.startsWith(\"--\")||o.test(l)||(s=\"px;\"),i=i+l+\":\"+a+s}}return e+'=\"'+n(i)+'\"'}return null==t||!1===t||\"function\"==typeof t||\"object\"==typeof t?\"\":!0===t?e:e+'=\"'+n(\"\"+t)+'\"'}function s(r){if(null==r||\"boolean\"==typeof r||\"function\"==typeof r)return null;if(\"object\"==typeof r){if(void 0===r.constructor)return r;if(i(r)){for(var e=0;e<r.length;e++)r[e]=s(r[e]);return r}}return n(\"\"+r)}export{u as jsx,l as jsxAttr,u as jsxDEV,s as jsxEscape,a as jsxTemplate,u as jsxs};\n//# sourceMappingURL=jsxRuntime.module.js.map\n"
6
6
  ],
7
7
  "mappings": "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",
8
- "debugId": "2B835D6D3E73856C64756E2164756E21",
8
+ "debugId": "87FF749D2D1F353A64756E2164756E21",
9
9
  "names": []
10
10
  }