Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions reflex/vars/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,28 @@
var_operation,
var_operation_return,
)
from .blob import BlobVar, LiteralBlobVar
from .color import ColorVar, LiteralColorVar
from .datetime import DateTimeVar
from .function import FunctionStringVar, FunctionVar, VarOperationCall
from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar
from .object import LiteralObjectVar, ObjectVar
from .sequence import (
ArrayVar,
BytesVar,
ConcatVarOperation,
LiteralArrayVar,
LiteralBytesVar,
LiteralStringVar,
StringVar,
)

__all__ = [
"ArrayVar",
"BaseStateMeta",
"BlobVar",
"BooleanVar",
"BytesVar",
"ColorVar",
"ConcatVarOperation",
"DateTimeVar",
Expand All @@ -38,7 +43,9 @@
"FunctionStringVar",
"FunctionVar",
"LiteralArrayVar",
"LiteralBlobVar",
"LiteralBooleanVar",
"LiteralBytesVar",
"LiteralColorVar",
"LiteralNumberVar",
"LiteralObjectVar",
Expand Down
55 changes: 54 additions & 1 deletion reflex/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,18 @@
from reflex.constants.colors import Color
from reflex.state import BaseState

from .blob import Blob, BlobVar, LiteralBlobVar
from .color import LiteralColorVar
from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar
from .object import LiteralObjectVar, ObjectVar
from .sequence import ArrayVar, LiteralArrayVar, LiteralStringVar, StringVar
from .sequence import (
ArrayVar,
BytesVar,
LiteralArrayVar,
LiteralBytesVar,
LiteralStringVar,
StringVar,
)


VAR_TYPE = TypeVar("VAR_TYPE", covariant=True)
Expand Down Expand Up @@ -655,6 +663,22 @@ def create( # pyright: ignore [reportOverlappingOverload]
_var_data: VarData | None = None,
) -> LiteralStringVar: ...

@overload
@classmethod
def create( # pyright: ignore [reportOverlappingOverload]
cls,
value: bytes,
_var_data: VarData | None = None,
) -> LiteralBytesVar: ...

@overload
@classmethod
def create( # pyright: ignore [reportOverlappingOverload]
cls,
value: Blob,
_var_data: VarData | None = None,
) -> LiteralBlobVar: ...

@overload
@classmethod
def create( # pyright: ignore [reportOverlappingOverload]
Expand Down Expand Up @@ -845,6 +869,9 @@ def guess_type(self: Var[NoReturn]) -> Var[Any]: ... # pyright: ignore [reportO
@overload
def guess_type(self: Var[str]) -> StringVar: ...

@overload
def guess_type(self: Var[bytes | Sequence[bytes]]) -> BytesVar: ...

@overload
def guess_type(self: Var[bool]) -> BooleanVar: ...

Expand Down Expand Up @@ -1686,6 +1713,20 @@ def var_operation(
) -> Callable[P, StringVar]: ...


@overload
def var_operation(
func: Callable[P, CustomVarOperationReturn[bytes]]
| Callable[P, CustomVarOperationReturn[bytes | None]],
) -> Callable[P, BytesVar]: ...


@overload
def var_operation(
func: Callable[P, CustomVarOperationReturn[Blob]]
| Callable[P, CustomVarOperationReturn[Blob | None]],
) -> Callable[P, BlobVar]: ...


LIST_T = TypeVar("LIST_T", bound=Sequence)


Expand Down Expand Up @@ -2306,6 +2347,13 @@ def __get__(
owner: type,
) -> StringVar: ...

@overload
def __get__(
self: ComputedVar[bytes],
instance: None,
owner: type,
) -> BytesVar: ...

@overload
def __get__(
self: ComputedVar[MAPPING_TYPE],
Expand Down Expand Up @@ -3402,6 +3450,11 @@ def __get__(
self: Field[str] | Field[str | None], instance: None, owner: Any
) -> StringVar: ...

@overload
def __get__(
self: Field[bytes] | Field[bytes | None], instance: None, owner: Any
) -> BytesVar: ...

@overload
def __get__(
self: Field[list[V]]
Expand Down
163 changes: 163 additions & 0 deletions reflex/vars/blob.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""Blob variable types for representing JavaScript Blob objects in Reflex."""

import dataclasses
from typing import TYPE_CHECKING, TypeVar

from reflex.vars.base import (
LiteralVar,
Var,
VarData,
var_operation,
var_operation_return,
)

if TYPE_CHECKING:
from reflex.vars import Var


@dataclasses.dataclass
class Blob:
"""Represents a JavaScript Blob object."""

data: str | bytes = ""
mime_type: str = ""


BLOB_T = TypeVar("BLOB_T", bound=bytes | str, covariant=True)


class BlobVar(Var[BLOB_T], python_types=Blob):
"""A variable representing a JavaScript Blob object."""

@classmethod
def _determine_mime_type(cls, value: str | bytes | Blob | Var) -> str:
mime_type = ""
if isinstance(value, str | bytes | Blob):
match value:
case str():
mime_type = "text/plain"
case bytes():
mime_type = "application/octet-stream"
case Blob():
mime_type = value.mime_type

elif isinstance(value, Var):
if isinstance(value._var_type, str):
mime_type = "text/plain"
if isinstance(value._var_type, bytes):
mime_type = "application/octet-stream"

if not mime_type:
msg = "Unable to determine mime type for blob creation."
raise ValueError(msg)

return mime_type
Comment on lines +32 to +54
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggest moving this logic into a create function for the Blob dataclass


@classmethod
def create(
cls,
value: str | bytes | Blob | Var,
mime_type: str | Var | None = None,
_var_data: VarData | None = None,
):
"""Create a BlobVar from the given value and MIME type.

Args:
value: The data to create the Blob from (string, bytes, or Var).
mime_type: The MIME type of the Blob (string or Var).
_var_data: Optional variable data.

Returns:
A BlobVar instance representing the JavaScript Blob object.
"""
Comment on lines +56 to +72
Copy link
Collaborator

Choose a reason for hiding this comment

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

Combine this with LiteralBlobVar.create and remove the definition in the non-literal BlobVar

if mime_type is None:
mime_type = cls._determine_mime_type(value)

if not isinstance(mime_type, Var):
mime_type = LiteralVar.create(mime_type)

if isinstance(value, str | bytes):
value = LiteralVar.create(value)

elif isinstance(value, Blob):
value = LiteralVar.create(value.data)

if isinstance(value._var_type, bytes):
value = f"new Uint8Array({value})"
Comment on lines +85 to +86
Copy link
Collaborator

Choose a reason for hiding this comment

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

if the value is a BytesVar, then you don't need this additional conversion, because the BytesVar already includes the array cast in its _js_expr, so when you format it into the expression, it will already appear like this

str(rx.Var(f"{rx.Var.create(b"foo")}")) -> 'new Uint8Array([102, 111, 111])'


return cls(
_js_expr=f"new Blob([{value}], {{ type: {mime_type} }})",
_var_type=Blob,
_var_data=_var_data,
)

def create_object_url(self):
"""Create a URL from this Blob object using window.URL.createObjectURL.

Returns:
A URL string representing the Blob object.
"""
return create_url_from_blob_operation(self)


@var_operation
def create_url_from_blob_operation(value: BlobVar):
"""Create a URL from a Blob variable using window.URL.createObjectURL.

Args:
value: The Blob variable to create a URL from.

Returns:
A URL string representing the Blob object.
"""
return var_operation_return(
js_expression=f"window.URL.createObjectURL({value})",
var_type=str,
)


@dataclasses.dataclass(
eq=False,
frozen=True,
slots=True,
)
class LiteralBlobVar(LiteralVar, BlobVar):
"""A literal version of a Blob variable."""

_var_value: Blob = dataclasses.field(default_factory=Blob)

@classmethod
def create(
cls,
value: bytes | str | Blob,
mime_type: str | None = None,
Comment on lines +132 to +133
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think this method should just take a Blob object to simplify the API.

it also needs to assign that Blob object to _var_value so it can be pulled out on the python side later.

_var_data: VarData | None = None,
) -> BlobVar:
"""Create a literal Blob variable from bytes or string data.

Args:
value: The data to create the Blob from (bytes or string).
mime_type: The MIME type of the Blob.
_var_data: Optional variable data.

Returns:
A BlobVar instance representing the Blob.
"""
if not mime_type:
mime_type = cls._determine_mime_type(value)

if isinstance(value, Blob):
value = value.data

var_type = type(value)

if isinstance(value, bytes):
value = f"new Uint8Array({list(value)})"
else:
value = f"'{value}'"

return cls(
_js_expr=f"new Blob([{value}], {{ type: '{mime_type}' }})",
_var_type=var_type,
_var_data=_var_data,
)
Loading