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
**/*.pyc
assets/external/*
dist/*
examples/
.idea

View File

@ -21,6 +21,8 @@ class Dirs(SimpleNamespace):
WEB = ".web"
# The name of the assets directory.
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.
UTILS = "utils"
# The name of the output static directory.

View File

@ -15,10 +15,15 @@ from types import SimpleNamespace
from platformdirs import PlatformDirs
IS_WINDOWS = platform.system() == "Windows"
IS_WINDOWS_BUN_SUPPORTED_MACHINE = IS_WINDOWS and platform.machine() in [
"AMD64",
"x86_64",
]
class Dirs(SimpleNamespace):
WEB = ".web"
APP_ASSETS = "assets"
EXTERNAL_APP_ASSETS = "external"
UTILS = "utils"
STATIC = "_static"
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 . import hooks as hooks
from .assets import asset as asset
from .client_state import ClientStateVar as ClientStateVar
from .layout import layout as layout
from .misc import run_in_thread as run_in_thread
@ -17,6 +18,7 @@ warn(
)
_x = SimpleNamespace(
asset=asset,
client_state=ClientStateVar.create,
hooks=hooks,
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 sys
from typing import Any, Callable, Optional, Type
from typing import Any, Callable, Optional, Type, Union
from reflex import constants
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.
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."""
from typing import Optional, Union
from reflex.utils.imports import ImportVar
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:
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.
Args:
@ -31,7 +33,7 @@ def const(name, value) -> Var | None:
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.
Args:
@ -49,7 +51,7 @@ def useCallback(func, deps) -> Var | None:
return v
def useContext(context) -> Var | None:
def useContext(context) -> Optional[Var]:
"""Create a useContext hook with a context.
Args:
@ -63,7 +65,7 @@ def useContext(context) -> Var | None:
return v
def useRef(default) -> Var | None:
def useRef(default) -> Optional[Var]:
"""Create a useRef hook with a default value.
Args:
@ -77,7 +79,7 @@ def useRef(default) -> Var | None:
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.
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()