External assets (#3220)

This commit is contained in:
abulvenz 2024-05-28 16:39:25 +00:00 committed by GitHub
parent 7c2056e960
commit 6c6eaaa55f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 115 additions and 8 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
**/.DS_Store **/.DS_Store
**/*.pyc **/*.pyc
assets/external/*
dist/* dist/*
examples/ examples/
.idea .idea

View File

@ -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.

View File

@ -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"])

View File

@ -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,

View 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

View File

@ -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.

View File

@ -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:

View File

@ -0,0 +1 @@
const test = "inside custom_script.js";

View 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()