Skip to content

Expose .importable_path on the value returned from rx.asset#6348

Merged
adhami3310 merged 2 commits intomainfrom
masenf/asset-path-importable
Apr 20, 2026
Merged

Expose .importable_path on the value returned from rx.asset#6348
adhami3310 merged 2 commits intomainfrom
masenf/asset-path-importable

Conversation

@masenf
Copy link
Copy Markdown
Collaborator

@masenf masenf commented Apr 20, 2026

This allows components to reference the internal path in the .web directory for use with JS imports at compile time.

This allows components to reference the internal path in the `.web` directory
for use with JS imports at compile time.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 20, 2026

Merging this PR will not alter performance

✅ 9 untouched benchmarks


Comparing masenf/asset-path-importable (358ef70) with main (b9f3570)

Open in CodSpeed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR exposes an importable_path attribute on the value returned from rx.asset() by introducing AssetPathStr, a str subclass whose string value carries the frontend_path-prefixed URL while importable_path holds the $/public-prefixed path for use in component library / JS module imports at compile time. The change is backward-compatible since AssetPathStr is a str, and the new tests cover local, shared, and custom-frontend_path scenarios.

Confidence Score: 5/5

Safe to merge; the pickle edge case is unlikely to affect production use given the compile-time nature of AssetPathStr.

The only finding is a P2 pickle/reconstruction issue that affects an immutable str subclass used exclusively as a compile-time constant — it is not stored in state, serialized, or passed across process boundaries in normal Reflex usage. All changed paths are well-tested and the change is backward-compatible.

reflex/assets.py — AssetPathStr.__new__ lacks __getnewargs__, causing double-prefix on pickle round-trip.

Important Files Changed

Filename Overview
reflex/assets.py Introduces AssetPathStr, a str subclass that carries a $/public-prefixed importable_path slot; asset() return type updated accordingly. Pickling/unpickling will double-apply frontend_path and lose the slot value.
tests/units/assets/test_assets.py Three new tests cover local asset, shared asset, and custom frontend_path variants of importable_path; correctly monkeypatches get_config in the module namespace.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["rx.asset(path, shared=...)"] --> B{shared?}
    B -- No --> C["AssetPathStr('/{path}')"]
    B -- Yes --> D["AssetPathStr('/{external}/{subfolder}/{path}')"]
    C --> E["__new__(relative_path)"]
    D --> E
    E --> F["str value = prepend_frontend_path(relative_path)"]
    E --> G["importable_path = '$/public' + relative_path"]
    F --> H["Used as URL in HTML/CSS"]
    G --> I["Used in JS library/module imports at build time"]
Loading

Reviews (1): Last reviewed commit: "Expose `.importable_path` on the value r..." | Re-trigger Greptile

Comment thread reflex/assets.py
Comment on lines +15 to +76
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Pickle/copy will double-apply frontend_path

When Python pickles a str subclass it calls cls.__new__(cls, str(self)) on reconstruction — passing the already-prefixed string value back into __new__, which calls prepend_frontend_path a second time. The result is a corrupted double-prefixed path (e.g. /my-app/my-app/external/…) and the importable_path slot is silently lost.

This can be fixed by overriding __getnewargs__ to return the raw relative path (recoverable from importable_path):

def __getnewargs__(self) -> tuple[str]:
    # Return the unprefixed path so __new__ reconstructs correctly.
    return (self.importable_path[len("$/public"):],)

If AssetPathStr is never intended to be serialized/pickled, document that constraint explicitly in the class docstring.

adhami3310
adhami3310 previously approved these changes Apr 20, 2026
@adhami3310 adhami3310 merged commit 0d9f0c5 into main Apr 20, 2026
47 of 48 checks passed
@adhami3310 adhami3310 deleted the masenf/asset-path-importable branch April 20, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants