Skip to content

Commit

Permalink
External assets (#3220)
Browse files Browse the repository at this point in the history
  • Loading branch information
abulvenz authored May 28, 2024
1 parent 7c2056e commit 6c6eaaa
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
**/.DS_Store
**/*.pyc
assets/external/*
dist/*
examples/
.idea
Expand Down
2 changes: 2 additions & 0 deletions reflex/constants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions reflex/constants/base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
2 changes: 2 additions & 0 deletions reflex/experimental/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

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
Expand All @@ -17,6 +18,7 @@
)

_x = SimpleNamespace(
asset=asset,
client_state=ClientStateVar.create,
hooks=hooks,
layout=layout,
Expand Down
56 changes: 56 additions & 0 deletions reflex/experimental/assets.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 4 additions & 2 deletions reflex/experimental/client_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -171,7 +171,9 @@ def set(self) -> 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.
Expand Down
14 changes: 8 additions & 6 deletions reflex/experimental/hooks.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions tests/experimental/custom_script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const test = "inside custom_script.js";
36 changes: 36 additions & 0 deletions tests/experimental/test_assets.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 6c6eaaa

Please sign in to comment.