eptm_dashboard/.venv/lib/python3.12/site-packages/reflex/assets.py

215 lines
7.6 KiB
Python

"""Helper functions for adding assets to the app."""
import inspect
from pathlib import Path
from typing import TYPE_CHECKING, overload
from reflex_base import constants
from reflex_base.config import get_config
from reflex_base.environment import EnvironmentVariables
if TYPE_CHECKING:
from typing_extensions import Buffer
class AssetPathStr(str):
"""The relative URL to an asset, with a build-time importable variant.
Returned by :func:`asset`. The string value is the asset URL with the
configured ``frontend_path`` prepended; :attr:`importable_path` is the
same asset prefixed with ``$/public`` so the asset can be referenced by
a component ``library`` or module import at build time.
The constructor signature mirrors :class:`str`: the input is interpreted
as the unprefixed asset path and both forms are derived from it at
construction time.
"""
__slots__ = ("importable_path",)
importable_path: str
@overload
def __new__(cls, object: object = "") -> "AssetPathStr": ...
@overload
def __new__(
cls,
object: "Buffer",
encoding: str = "utf-8",
errors: str = "strict",
) -> "AssetPathStr": ...
def __new__(
cls,
object: object = "",
encoding: str | None = None,
errors: str | None = None,
) -> "AssetPathStr":
"""Construct from an unprefixed, leading-slash asset path.
Args/semantics mirror :class:`str`. The resulting string is interpreted
as the asset path (e.g. ``"/external/mod/file.js"``); the
frontend-prefixed URL is stored as the ``AssetPathStr`` value and
``$/public`` + ``relative_path`` as :attr:`importable_path`.
Args:
object: The object to stringify (str, bytes, or any object).
encoding: Encoding to decode ``object`` with when it is bytes-like.
errors: Error handler for decoding.
Returns:
A new ``AssetPathStr`` instance.
"""
if encoding is None and errors is None:
relative_path = str.__new__(str, object)
else:
relative_path = str.__new__(
str,
object, # pyright: ignore[reportArgumentType]
"utf-8" if encoding is None else encoding,
"strict" if errors is None else errors,
)
instance = super().__new__(
cls, get_config().prepend_frontend_path(relative_path)
)
instance.importable_path = f"$/public{relative_path}"
return instance
def __getnewargs__(self) -> tuple[str]:
"""Return the unprefixed path for pickle/copy reconstruction.
Python's default ``str`` pickle path would feed the frontend-prefixed
value back into :meth:`__new__`, double-applying the prefix and
losing the :attr:`importable_path` slot. Returning the raw path
(recovered by stripping the ``$/public`` prefix) lets ``__new__``
rebuild both forms correctly.
Returns:
A one-tuple containing the unprefixed asset path.
"""
return (self.importable_path[len("$/public") :],)
def remove_stale_external_asset_symlinks():
"""Remove broken symlinks and empty directories in assets/external/.
When a Python module directory that uses rx.asset(shared=True) is renamed
or deleted, stale symlinks remain in assets/external/ pointing to the old
path. This cleanup prevents issues with file watchers detecting symlink
re-creation during import.
"""
external_dir = (
Path.cwd() / constants.Dirs.APP_ASSETS / constants.Dirs.EXTERNAL_APP_ASSETS
)
if not external_dir.exists():
return
# Remove broken symlinks.
broken = [
p
for p in external_dir.rglob("*")
if p.is_symlink() and not p.resolve().exists()
]
for path in broken:
path.unlink()
# Remove empty directories left behind (deepest first).
for dirpath in sorted(external_dir.rglob("*"), reverse=True):
if dirpath.is_dir() and not dirpath.is_symlink() and not any(dirpath.iterdir()):
dirpath.rmdir()
def asset(
path: str,
shared: bool = False,
subfolder: str | None = None,
_stack_level: int = 1,
) -> AssetPathStr:
"""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.
Returns:
The relative URL to the asset, with an ``importable_path`` property
for use as a build-time module reference.
Raises:
FileNotFoundError: If the file does not exist.
ValueError: If subfolder is provided for local assets.
"""
assets = constants.Dirs.APP_ASSETS
backend_only = EnvironmentVariables.REFLEX_BACKEND_ONLY.get()
# Local asset handling
if not shared:
cwd = Path.cwd()
src_file_local = cwd / assets / path
if subfolder is not None:
msg = "Subfolder is not supported for local assets."
raise ValueError(msg)
if not backend_only and not src_file_local.exists():
msg = f"File not found: {src_file_local}"
raise FileNotFoundError(msg)
return AssetPathStr(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():
msg = f"File not found: {src_file_shared}"
raise FileNotFoundError(msg)
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()
):
try:
dst_file.symlink_to(src_file_shared)
except FileExistsError:
# This happens when Simon builds the app on a bind mount in a docker container.
dst_file.unlink()
dst_file.symlink_to(src_file_shared)
return AssetPathStr(f"/{external}/{subfolder}/{path}")