Skip to content

Commit

Permalink
Move to rx.asset, retain old API in rx._x.asset
Browse files Browse the repository at this point in the history
  • Loading branch information
masenf committed Nov 18, 2024
1 parent a5b1f49 commit 47e77f4
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 79 deletions.
1 change: 1 addition & 0 deletions reflex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
"experimental": ["_x"],
"admin": ["AdminDash"],
"app": ["App", "UploadFile"],
"assets": ["asset"],
"base": ["Base"],
"components.component": [
"Component",
Expand Down
1 change: 1 addition & 0 deletions reflex/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ from . import vars as vars
from .admin import AdminDash as AdminDash
from .app import App as App
from .app import UploadFile as UploadFile
from .assets import asset as asset
from .base import Base as Base
from .components import el as el
from .components import lucide as lucide
Expand Down
95 changes: 95 additions & 0 deletions reflex/assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Helper functions for adding assets to the app."""

import inspect
from pathlib import Path
from typing import Optional

from reflex import constants
from reflex.utils.exec import is_backend_only


def asset(
path: str,
shared: bool = False,
subfolder: Optional[str] = None,
_stack_level: int = 1,
) -> str:
"""Add an asset to the app, either shared as a symlink or local.
Shared/External/Library assets:
Place the file next to your including python file.
Links the file to the app's external assets directory.
Example:
```python
# my_custom_javascript.js is a shared asset located next to the including python file.
rx.script(src=rx.asset(path="my_custom_javascript.js", shared=True))
rx.image(src=rx.asset(path="test_image.png", shared=True, subfolder="subfolder"))
```
Local/Internal assets:
Place the file in the app's assets/ directory.
Example:
```python
# local_image.png is an asset located in the app's assets/ directory. It cannot be shared when developing a library.
rx.image(src=rx.asset(path="local_image.png"))
```
Args:
path: The relative path of the asset.
subfolder: The directory to place the shared asset in.
shared: Whether to expose the asset to other apps.
_stack_level: The stack level to determine the calling file, defaults to
the immediate caller 1. When using rx.asset via a helper function,
increase this number for each helper function in the stack.
Raises:
FileNotFoundError: If the file does not exist.
ValueError: If subfolder is provided for local assets.
Returns:
The relative URL to the asset.
"""
assets = constants.Dirs.APP_ASSETS
backend_only = is_backend_only()

# Local asset handling
if not shared:
cwd = Path.cwd()
src_file_local = cwd / assets / path
if subfolder is not None:
raise ValueError("Subfolder is not supported for local assets.")
if not backend_only and not src_file_local.exists():
raise FileNotFoundError(f"File not found: {src_file_local}")
return f"/{path}"

# Shared asset handling
# Determine the file by which the asset is exposed.
frame = inspect.stack()[_stack_level]
calling_file = frame.filename
module = inspect.getmodule(frame[0])
assert module is not None

external = constants.Dirs.EXTERNAL_APP_ASSETS
src_file_shared = Path(calling_file).parent / path
if not src_file_shared.exists():
raise FileNotFoundError(f"File not found: {src_file_shared}")

caller_module_path = module.__name__.replace(".", "/")
subfolder = f"{caller_module_path}/{subfolder}" if subfolder else caller_module_path

# Symlink the asset to the app's external assets directory if running frontend.
if not backend_only:
# 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 / path

if not dst_file.exists() and (
not dst_file.is_symlink() or dst_file.resolve() != src_file_shared.resolve()
):
dst_file.symlink_to(src_file_shared)

return f"/{external}/{subfolder}/{path}"
93 changes: 20 additions & 73 deletions reflex/experimental/assets.py
Original file line number Diff line number Diff line change
@@ -1,90 +1,37 @@
"""Helper functions for adding assets to the app."""

import inspect
from pathlib import Path
from typing import Optional

from reflex import constants
from reflex.utils.exec import is_backend_only
from reflex import assets
from reflex.utils import console


def asset(
path: str,
shared: bool = False,
subfolder: Optional[str] = None,
) -> str:
"""Add an asset to the app, either shared as a symlink or local.
def asset(relative_filename: str, subfolder: Optional[str] = None) -> str:
"""DEPRECATED: use `rx.asset` with `shared=True` instead.
Shared/External/Library assets:
Add an asset to the app.
Place the file next to your including python file.
Links the file to the app's external assets directory.
Copies the file to the app's external assets directory.
Example:
```python
# my_custom_javascript.js is a shared asset located next to the including python file.
rx.script(src=rx._x.asset(path="my_custom_javascript.js", shared=True))
rx.image(src=rx._x.asset(path="test_image.png", shared=True, subfolder="subfolder"))
```
Local/Internal assets:
Place the file in the app's assets/ directory.
Example:
```python
# local_image.png is an asset located in the app's assets/ directory. It cannot be shared when developing a library.
rx.image(src=rx._x.asset(path="local_image.png"))
rx.script(src=rx._x.asset("my_custom_javascript.js"))
rx.image(src=rx._x.asset("test_image.png","subfolder"))
```
Args:
path: The relative path of the asset.
subfolder: The directory to place the shared asset in.
shared: Whether to expose the asset to other apps.
Raises:
FileNotFoundError: If the file does not exist.
ValueError: If subfolder is provided for local assets.
relative_filename: The relative filename of the asset.
subfolder: The directory to place the asset in.
Returns:
The relative URL to the asset.
The relative URL to the copied asset.
"""
assets = constants.Dirs.APP_ASSETS
backend_only = is_backend_only()

# Local asset handling
if not shared:
cwd = Path.cwd()
src_file_local = cwd / assets / path
if subfolder is not None:
raise ValueError("Subfolder is not supported for local assets.")
if not backend_only and not src_file_local.exists():
raise FileNotFoundError(f"File not found: {src_file_local}")
return f"/{path}"

# Shared asset handling
# 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

external = constants.Dirs.EXTERNAL_APP_ASSETS
src_file_shared = Path(calling_file).parent / path
if not src_file_shared.exists():
raise FileNotFoundError(f"File not found: {src_file_shared}")

caller_module_path = module.__name__.replace(".", "/")
subfolder = f"{caller_module_path}/{subfolder}" if subfolder else caller_module_path

# Symlink the asset to the app's external assets directory if running frontend.
if not backend_only:
# 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 / path

if not dst_file.exists() and (
not dst_file.is_symlink() or dst_file.resolve() != src_file_shared.resolve()
):
dst_file.symlink_to(src_file_shared)

return f"/{external}/{subfolder}/{path}"
console.deprecate(
feature_name="rx._x.asset",
reason="Use `rx.asset` with `shared=True` instead of `rx._x.asset`.",
deprecation_version="0.6.6",
removal_version="0.7.0",
)
return assets.asset(
relative_filename, shared=True, subfolder=subfolder, _stack_level=2
)
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@
def test_shared_asset() -> None:
"""Test shared assets."""
# The asset function copies a file to the app's external assets directory.
asset = rx._x.asset(path="custom_script.js", shared=True, subfolder="subfolder")
asset = rx.asset(path="custom_script.js", shared=True, subfolder="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(path="custom_script.js", shared=True, subfolder="subfolder")
asset = rx.asset(path="custom_script.js", shared=True, subfolder="subfolder")

# Test the asset function without a subfolder.
asset = rx._x.asset(path="custom_script.js", shared=True)
asset = rx.asset(path="custom_script.js", shared=True)
assert asset == "/external/test_assets/custom_script.js"
result_file = Path(Path.cwd(), "assets/external/test_assets/custom_script.js")
assert result_file.exists()
Expand All @@ -31,12 +31,25 @@ def test_shared_asset() -> None:
shutil.rmtree(Path.cwd() / "assets/external")

with pytest.raises(FileNotFoundError):
asset = rx._x.asset("non_existent_file.js")
asset = rx.asset("non_existent_file.js")

# Nothing is done to assets when file does not exist.
assert not Path(Path.cwd() / "assets/external").exists()


def test_deprecated_x_asset(capsys) -> None:
"""Test that the deprecated asset function raises a warning.
Args:
capsys: Pytest fixture that captures stdout and stderr.
"""
assert rx.asset("custom_script.js", shared=True) == rx._x.asset("custom_script.js")
assert (
"DeprecationWarning: rx._x.asset has been deprecated in version 0.6.6"
in capsys.readouterr().out
)


@pytest.mark.parametrize(
"path,shared",
[
Expand All @@ -52,7 +65,7 @@ def test_invalid_assets(path: str, shared: bool) -> None:
shared: Whether the asset should be shared.
"""
with pytest.raises(FileNotFoundError):
_ = rx._x.asset(path, shared=shared)
_ = rx.asset(path, shared=shared)


@pytest.fixture
Expand All @@ -77,5 +90,5 @@ def test_local_asset(custom_script_in_asset_dir: Path) -> None:
custom_script_in_asset_dir: Fixture that creates a custom_script.js file in the app's assets directory.
"""
asset = rx._x.asset("custom_script.js", shared=False)
asset = rx.asset("custom_script.js", shared=False)
assert asset == "/custom_script.js"

0 comments on commit 47e77f4

Please sign in to comment.