External assets (#3220)
This commit is contained in:
parent
7c2056e960
commit
6c6eaaa55f
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
**/*.pyc
|
**/*.pyc
|
||||||
|
assets/external/*
|
||||||
dist/*
|
dist/*
|
||||||
examples/
|
examples/
|
||||||
.idea
|
.idea
|
||||||
|
@ -21,6 +21,8 @@ class Dirs(SimpleNamespace):
|
|||||||
WEB = ".web"
|
WEB = ".web"
|
||||||
# The name of the assets directory.
|
# The name of the assets directory.
|
||||||
APP_ASSETS = "assets"
|
APP_ASSETS = "assets"
|
||||||
|
# The name of the assets directory for external ressource (a subfolder of APP_ASSETS).
|
||||||
|
EXTERNAL_APP_ASSETS = "external"
|
||||||
# The name of the utils file.
|
# The name of the utils file.
|
||||||
UTILS = "utils"
|
UTILS = "utils"
|
||||||
# The name of the output static directory.
|
# The name of the output static directory.
|
||||||
|
@ -15,10 +15,15 @@ from types import SimpleNamespace
|
|||||||
from platformdirs import PlatformDirs
|
from platformdirs import PlatformDirs
|
||||||
|
|
||||||
IS_WINDOWS = platform.system() == "Windows"
|
IS_WINDOWS = platform.system() == "Windows"
|
||||||
|
IS_WINDOWS_BUN_SUPPORTED_MACHINE = IS_WINDOWS and platform.machine() in [
|
||||||
|
"AMD64",
|
||||||
|
"x86_64",
|
||||||
|
]
|
||||||
|
|
||||||
class Dirs(SimpleNamespace):
|
class Dirs(SimpleNamespace):
|
||||||
WEB = ".web"
|
WEB = ".web"
|
||||||
APP_ASSETS = "assets"
|
APP_ASSETS = "assets"
|
||||||
|
EXTERNAL_APP_ASSETS = "external"
|
||||||
UTILS = "utils"
|
UTILS = "utils"
|
||||||
STATIC = "_static"
|
STATIC = "_static"
|
||||||
STATE_PATH = "/".join([UTILS, "state"])
|
STATE_PATH = "/".join([UTILS, "state"])
|
||||||
|
@ -8,6 +8,7 @@ from reflex.components.sonner.toast import toast as toast
|
|||||||
|
|
||||||
from ..utils.console import warn
|
from ..utils.console import warn
|
||||||
from . import hooks as hooks
|
from . import hooks as hooks
|
||||||
|
from .assets import asset as asset
|
||||||
from .client_state import ClientStateVar as ClientStateVar
|
from .client_state import ClientStateVar as ClientStateVar
|
||||||
from .layout import layout as layout
|
from .layout import layout as layout
|
||||||
from .misc import run_in_thread as run_in_thread
|
from .misc import run_in_thread as run_in_thread
|
||||||
@ -17,6 +18,7 @@ warn(
|
|||||||
)
|
)
|
||||||
|
|
||||||
_x = SimpleNamespace(
|
_x = SimpleNamespace(
|
||||||
|
asset=asset,
|
||||||
client_state=ClientStateVar.create,
|
client_state=ClientStateVar.create,
|
||||||
hooks=hooks,
|
hooks=hooks,
|
||||||
layout=layout,
|
layout=layout,
|
||||||
|
56
reflex/experimental/assets.py
Normal file
56
reflex/experimental/assets.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"""Helper functions for adding assets to the app."""
|
||||||
|
import inspect
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from reflex import constants
|
||||||
|
|
||||||
|
|
||||||
|
def asset(relative_filename: str, subfolder: Optional[str] = None) -> str:
|
||||||
|
"""Add an asset to the app.
|
||||||
|
Place the file next to your including python file.
|
||||||
|
Copies the file to the app's external assets directory.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
rx.script(src=rx._x.asset("my_custom_javascript.js"))
|
||||||
|
rx.image(src=rx._x.asset("test_image.png","subfolder"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Args:
|
||||||
|
relative_filename: The relative filename of the asset.
|
||||||
|
subfolder: The directory to place the asset in.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the file does not exist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The relative URL to the copied asset.
|
||||||
|
"""
|
||||||
|
# Determine the file by which the asset is exposed.
|
||||||
|
calling_file = inspect.stack()[1].filename
|
||||||
|
module = inspect.getmodule(inspect.stack()[1][0])
|
||||||
|
assert module is not None
|
||||||
|
caller_module_path = module.__name__.replace(".", "/")
|
||||||
|
|
||||||
|
subfolder = f"{caller_module_path}/{subfolder}" if subfolder else caller_module_path
|
||||||
|
|
||||||
|
src_file = Path(calling_file).parent / relative_filename
|
||||||
|
|
||||||
|
assets = constants.Dirs.APP_ASSETS
|
||||||
|
external = constants.Dirs.EXTERNAL_APP_ASSETS
|
||||||
|
|
||||||
|
if not src_file.exists():
|
||||||
|
raise FileNotFoundError(f"File not found: {src_file}")
|
||||||
|
|
||||||
|
# Create the asset folder in the currently compiling app.
|
||||||
|
asset_folder = Path.cwd() / assets / external / subfolder
|
||||||
|
asset_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
dst_file = asset_folder / relative_filename
|
||||||
|
|
||||||
|
if not dst_file.exists():
|
||||||
|
dst_file.symlink_to(src_file)
|
||||||
|
|
||||||
|
asset_url = f"/{external}/{subfolder}/{relative_filename}"
|
||||||
|
return asset_url
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Callable, Optional, Type
|
from typing import Any, Callable, Optional, Type, Union
|
||||||
|
|
||||||
from reflex import constants
|
from reflex import constants
|
||||||
from reflex.event import EventChain, EventHandler, EventSpec, call_script
|
from reflex.event import EventChain, EventHandler, EventSpec, call_script
|
||||||
@ -171,7 +171,9 @@ class ClientStateVar(Var):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def retrieve(self, callback: EventHandler | Callable | None = None) -> EventSpec:
|
def retrieve(
|
||||||
|
self, callback: Union[EventHandler, Callable, None] = None
|
||||||
|
) -> EventSpec:
|
||||||
"""Pass the value of the client state variable to a backend EventHandler.
|
"""Pass the value of the client state variable to a backend EventHandler.
|
||||||
|
|
||||||
The event handler must `yield` or `return` the EventSpec to trigger the event.
|
The event handler must `yield` or `return` the EventSpec to trigger the event.
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
"""Add standard Hooks wrapper for React."""
|
"""Add standard Hooks wrapper for React."""
|
||||||
|
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
from reflex.utils.imports import ImportVar
|
from reflex.utils.imports import ImportVar
|
||||||
from reflex.vars import Var, VarData
|
from reflex.vars import Var, VarData
|
||||||
|
|
||||||
|
|
||||||
def _add_react_import(v: Var | None, tags: str | list):
|
def _add_react_import(v: Optional[Var], tags: Union[str, list]):
|
||||||
if v is None:
|
if v is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -16,7 +18,7 @@ def _add_react_import(v: Var | None, tags: str | list):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def const(name, value) -> Var | None:
|
def const(name, value) -> Optional[Var]:
|
||||||
"""Create a constant Var.
|
"""Create a constant Var.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -31,7 +33,7 @@ def const(name, value) -> Var | None:
|
|||||||
return Var.create(f"const {name} = {value}")
|
return Var.create(f"const {name} = {value}")
|
||||||
|
|
||||||
|
|
||||||
def useCallback(func, deps) -> Var | None:
|
def useCallback(func, deps) -> Optional[Var]:
|
||||||
"""Create a useCallback hook with a function and dependencies.
|
"""Create a useCallback hook with a function and dependencies.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -49,7 +51,7 @@ def useCallback(func, deps) -> Var | None:
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
def useContext(context) -> Var | None:
|
def useContext(context) -> Optional[Var]:
|
||||||
"""Create a useContext hook with a context.
|
"""Create a useContext hook with a context.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -63,7 +65,7 @@ def useContext(context) -> Var | None:
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
def useRef(default) -> Var | None:
|
def useRef(default) -> Optional[Var]:
|
||||||
"""Create a useRef hook with a default value.
|
"""Create a useRef hook with a default value.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -77,7 +79,7 @@ def useRef(default) -> Var | None:
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
def useState(var_name, default=None) -> Var | None:
|
def useState(var_name, default=None) -> Optional[Var]:
|
||||||
"""Create a useState hook with a variable name and setter name.
|
"""Create a useState hook with a variable name and setter name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
1
tests/experimental/custom_script.js
Normal file
1
tests/experimental/custom_script.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
const test = "inside custom_script.js";
|
36
tests/experimental/test_assets.py
Normal file
36
tests/experimental/test_assets.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import reflex as rx
|
||||||
|
|
||||||
|
|
||||||
|
def test_asset():
|
||||||
|
# Test the asset function.
|
||||||
|
|
||||||
|
# The asset function copies a file to the app's external assets directory.
|
||||||
|
asset = rx._x.asset("custom_script.js", "subfolder")
|
||||||
|
assert asset == "/external/test_assets/subfolder/custom_script.js"
|
||||||
|
result_file = Path(
|
||||||
|
Path.cwd(), "assets/external/test_assets/subfolder/custom_script.js"
|
||||||
|
)
|
||||||
|
assert result_file.exists()
|
||||||
|
|
||||||
|
# Running a second time should not raise an error.
|
||||||
|
asset = rx._x.asset("custom_script.js", "subfolder")
|
||||||
|
|
||||||
|
# Test the asset function without a subfolder.
|
||||||
|
asset = rx._x.asset("custom_script.js")
|
||||||
|
assert asset == "/external/test_assets/custom_script.js"
|
||||||
|
result_file = Path(Path.cwd(), "assets/external/test_assets/custom_script.js")
|
||||||
|
assert result_file.exists()
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
shutil.rmtree(Path.cwd() / "assets/external")
|
||||||
|
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
asset = rx._x.asset("non_existent_file.js")
|
||||||
|
|
||||||
|
# Nothing is done to assets when file does not exist.
|
||||||
|
assert not Path(Path.cwd() / "assets/external").exists()
|
Loading…
Reference in New Issue
Block a user