diff --git a/reflex/experimental/assets.py b/reflex/experimental/assets.py index 197f7031d..c0f9706d7 100644 --- a/reflex/experimental/assets.py +++ b/reflex/experimental/assets.py @@ -9,7 +9,7 @@ from reflex import constants def asset( - filename: str, + path: str, subfolder: Optional[str] = None, shared: Optional[bool] = None, ) -> str: @@ -29,7 +29,7 @@ def asset( ``` Args: - filename: The relative filename of the asset. + path: The relative path of the asset. subfolder: The directory to place the asset in. shared: Whether to expose the asset to other apps. None means auto-detect. @@ -44,16 +44,13 @@ def asset( 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 cwd = Path.cwd() assets = constants.Dirs.APP_ASSETS external = constants.Dirs.EXTERNAL_APP_ASSETS - src_file_shared = Path(calling_file).parent / filename - src_file_local = cwd / assets / subfolder / filename + src_file_shared = Path(calling_file).parent / path + src_file_local = cwd / assets / path shared_exists = src_file_shared.exists() local_exists = src_file_local.exists() @@ -62,7 +59,7 @@ def asset( if shared is None: if shared_exists and local_exists: raise ValueError( - f"Both shared and local assets exist for {filename}. " + f"Both shared and local assets exist for {path}. " + "Please explicitly set shared=True or shared=False." ) if not shared_exists and not local_exists: @@ -73,26 +70,31 @@ def asset( # Local asset handling if not shared: + if subfolder is not None: + raise ValueError("Subfolder is not supported for local assets.") if not local_exists: raise FileNotFoundError(f"File not found: {src_file_local}") - return f"/{filename}" + return f"/{path}" # Shared asset handling if not 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 os.environ.get(constants.ENV_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 / filename + 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) - asset_url = f"/{external}/{subfolder}/{filename}" + asset_url = f"/{external}/{subfolder}/{path}" return asset_url diff --git a/tests/experimental/test_assets.py b/tests/experimental/test_assets.py index 8037bcc75..2a905961c 100644 --- a/tests/experimental/test_assets.py +++ b/tests/experimental/test_assets.py @@ -1,14 +1,15 @@ import shutil +from collections.abc import Generator from pathlib import Path import pytest import reflex as rx +import reflex.constants as constants -def test_asset(): - # Test the asset function. - +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("custom_script.js", "subfolder") assert asset == "/external/test_assets/subfolder/custom_script.js" @@ -34,3 +35,65 @@ def test_asset(): # Nothing is done to assets when file does not exist. assert not Path(Path.cwd() / "assets/external").exists() + + +@pytest.mark.parametrize( + "path,shared", + [ + pytest.param("non_existing_file", True), + pytest.param("non_existing_file", False), + ], +) +def test_invalid_assets(path: str, shared: bool) -> None: + """Test that asset raises an error when the file does not exist. + + Args: + path: The path to the asset. + shared: Whether the asset should be shared. + """ + with pytest.raises(FileNotFoundError): + _ = rx._x.asset(path, shared=shared) + + +@pytest.fixture +def custom_script_in_asset_dir() -> Generator[Path, None, None]: + """Create a custom_script.js file in the app's assets directory. + + Yields: + The path to the custom_script.js file. + """ + asset_dir = Path.cwd() / constants.Dirs.APP_ASSETS + asset_dir.mkdir(exist_ok=True) + path = asset_dir / "custom_script.js" + path.touch() + yield path + path.unlink() + + +def test_both_existing_implicit(custom_script_in_asset_dir: Path) -> None: + """Test that asset raises an error if shared is not set and both files exist. + + Args: + custom_script_in_asset_dir: Fixture that creates a custom_script.js file in the app's assets directory. + + """ + with pytest.raises(ValueError) as e: + _ = rx._x.asset("custom_script.js") + assert ( + str(e.value) + == "Both shared and local assets exist for custom_script.js. Please explicitly set shared=True or shared=False." + ) + + +def test_both_existing_explicit(custom_script_in_asset_dir: Path) -> None: + """Test that no error is raised if shared is set and both files exist. + + Args: + 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=True) + assert asset == "/external/test_assets/custom_script.js" + + asset = rx._x.asset("custom_script.js", shared=False) + assert asset == "/custom_script.js"