From 5d56f32ca8d2d21e122e086dfc0b31f38d928e3b Mon Sep 17 00:00:00 2001 From: Casey Riebe Date: Fri, 7 Nov 2025 10:26:42 -0600 Subject: [PATCH 1/5] Improve handling of `Var`s when using `rx.download()` --- reflex/event.py | 9 ++++++++- reflex/vars/function.py | 5 +++++ reflex/vars/sequence.py | 13 +++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/reflex/event.py b/reflex/event.py index b6b49636c3a..06ffef668dd 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -45,6 +45,8 @@ from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var from reflex.vars.function import ( + BASE64_ENCODE, + CREATE_OBJECT_URL, ArgsFunctionOperation, ArgsFunctionOperationBuilder, BuilderFunctionVar, @@ -54,6 +56,7 @@ VarOperationCall, ) from reflex.vars.object import ObjectVar +from reflex.vars.sequence import ArrayVar @dataclasses.dataclass( @@ -1285,7 +1288,11 @@ def download( url = cond( is_data_url, data.to(str), - f"data:{mime_type}," + data.to_string(), + ( + CREATE_OBJECT_URL.call(data.to_blob(mime_type=mime_type)) + if isinstance(data, ArrayVar) + else f"data:{mime_type};base64," + BASE64_ENCODE.call(data).to(str) + ), ) elif isinstance(data, bytes): if mime_type is None: diff --git a/reflex/vars/function.py b/reflex/vars/function.py index 6836e9d3b49..f6340ba3498 100644 --- a/reflex/vars/function.py +++ b/reflex/vars/function.py @@ -469,3 +469,8 @@ def create( "((__to_string) => __to_string.toString())", _var_type=ReflexCallable[[Any], str], ) +BASE64_ENCODE = FunctionStringVar.create("btoa", _var_type=ReflexCallable[[Any], str]) +CREATE_OBJECT_URL = FunctionStringVar.create( + "window.URL.createObjectURL", + _var_type=ReflexCallable[[Any], str], +) diff --git a/reflex/vars/sequence.py b/reflex/vars/sequence.py index 3824ba8760b..757ef492521 100644 --- a/reflex/vars/sequence.py +++ b/reflex/vars/sequence.py @@ -469,6 +469,19 @@ def foreach(self, fn: Any): return map_array_operation(self, function_var) + def to_blob(self, mime_type: str | Var[str]): + """Convert the array to a Blob object. + + Args: + mime_type: The MIME type for the Blob. + + Returns: + A Blob object created from the array data. + """ + return var_operation_return( + js_expression=f"new Blob([new Uint8Array({self})], {{ type: '{mime_type}' }})", + ) + @dataclasses.dataclass( eq=False, From dfab25f3586ef7df76ec0e514bd045fab635c813 Mon Sep 17 00:00:00 2001 From: Casey Riebe Date: Fri, 7 Nov 2025 10:50:32 -0600 Subject: [PATCH 2/5] Preserve formatting if already a string, otherwise stringify. --- reflex/event.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 06ffef668dd..cb626a6b385 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -56,7 +56,7 @@ VarOperationCall, ) from reflex.vars.object import ObjectVar -from reflex.vars.sequence import ArrayVar +from reflex.vars.sequence import ArrayVar, StringVar @dataclasses.dataclass( @@ -1291,7 +1291,12 @@ def download( ( CREATE_OBJECT_URL.call(data.to_blob(mime_type=mime_type)) if isinstance(data, ArrayVar) - else f"data:{mime_type};base64," + BASE64_ENCODE.call(data).to(str) + else f"data:{mime_type};base64," + + BASE64_ENCODE.call( + data.to(str) + if isinstance(data, StringVar) + else data.to_string(), + ).to(str) ), ) elif isinstance(data, bytes): From 24f6c05c42aa6adc2ab85d2f0eb1db3f76f1a52f Mon Sep 17 00:00:00 2001 From: Casey Riebe Date: Mon, 10 Nov 2025 08:51:37 -0600 Subject: [PATCH 3/5] Creating `BytesVar` and `BlobVar` --- pyproject.toml | 2 +- reflex/event.py | 14 +--- reflex/vars/__init__.py | 7 ++ reflex/vars/base.py | 47 ++++++++++- reflex/vars/blob.py | 118 ++++++++++++++++++++++++++++ reflex/vars/function.py | 5 -- reflex/vars/sequence.py | 167 ++++++++++++++++++++++++++++++++++++---- uv.lock | 10 +++ 8 files changed, 337 insertions(+), 33 deletions(-) create mode 100644 reflex/vars/blob.py diff --git a/pyproject.toml b/pyproject.toml index a561cace774..5999754440b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,6 @@ reportIncompatibleMethodOverride = false target-version = "py310" output-format = "concise" lint.isort.split-on-trailing-comma = false -preview = true lint.select = ["ALL"] lint.ignore = [ "A", @@ -168,6 +167,7 @@ lint.flake8-bugbear.extend-immutable-calls = [ "reflex.utils.types.Unset", "reflex.vars.base.Var.create", ] +preview = true [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] diff --git a/reflex/event.py b/reflex/event.py index cb626a6b385..b6b49636c3a 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -45,8 +45,6 @@ from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var from reflex.vars.function import ( - BASE64_ENCODE, - CREATE_OBJECT_URL, ArgsFunctionOperation, ArgsFunctionOperationBuilder, BuilderFunctionVar, @@ -56,7 +54,6 @@ VarOperationCall, ) from reflex.vars.object import ObjectVar -from reflex.vars.sequence import ArrayVar, StringVar @dataclasses.dataclass( @@ -1288,16 +1285,7 @@ def download( url = cond( is_data_url, data.to(str), - ( - CREATE_OBJECT_URL.call(data.to_blob(mime_type=mime_type)) - if isinstance(data, ArrayVar) - else f"data:{mime_type};base64," - + BASE64_ENCODE.call( - data.to(str) - if isinstance(data, StringVar) - else data.to_string(), - ).to(str) - ), + f"data:{mime_type}," + data.to_string(), ) elif isinstance(data, bytes): if mime_type is None: diff --git a/reflex/vars/__init__.py b/reflex/vars/__init__.py index c81e9a9bff3..5b3e3cbbbf8 100644 --- a/reflex/vars/__init__.py +++ b/reflex/vars/__init__.py @@ -13,6 +13,7 @@ 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 @@ -20,8 +21,10 @@ from .object import LiteralObjectVar, ObjectVar from .sequence import ( ArrayVar, + BytesVar, ConcatVarOperation, LiteralArrayVar, + LiteralBytesVar, LiteralStringVar, StringVar, ) @@ -29,7 +32,9 @@ __all__ = [ "ArrayVar", "BaseStateMeta", + "BlobVar", "BooleanVar", + "BytesVar", "ColorVar", "ConcatVarOperation", "DateTimeVar", @@ -38,7 +43,9 @@ "FunctionStringVar", "FunctionVar", "LiteralArrayVar", + "LiteralBlobVar", "LiteralBooleanVar", + "LiteralBytesVar", "LiteralColorVar", "LiteralNumberVar", "LiteralObjectVar", diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 412c0e41c70..7d69992b3a8 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -76,10 +76,18 @@ from reflex.constants.colors import Color from reflex.state import BaseState + from .blob import Blob, BlobVar 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) @@ -655,6 +663,14 @@ 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] @@ -845,6 +861,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: ... @@ -1686,6 +1705,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) @@ -2306,6 +2339,13 @@ def __get__( owner: type, ) -> StringVar: ... + @overload + def __get__( + self: ComputedVar[bytes], + instance: None, + owner: type, + ) -> BytesVar: ... + @overload def __get__( self: ComputedVar[MAPPING_TYPE], @@ -3402,6 +3442,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]] diff --git a/reflex/vars/blob.py b/reflex/vars/blob.py new file mode 100644 index 00000000000..ef087318bcb --- /dev/null +++ b/reflex/vars/blob.py @@ -0,0 +1,118 @@ +"""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.""" + + +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 create( + cls, + value: str | bytes | Var, + mime_type: str | Var, + _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. + """ + if not isinstance(value, Var): + value = LiteralVar.create(value) + if not isinstance(mime_type, Var): + mime_type = LiteralVar.create(mime_type) + elif type(value).__qualname__.endswith("BytesCastedVar"): + value = f"new Uint8Array({value})" + 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, + mime_type: str, + _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. + """ + return cls( + _js_expr=( + f"new Blob([new Uint8Array({list(value)})], {{ type: '{mime_type}' }})" + if isinstance(value, bytes) + else f"new Blob([{value}], {{ type: '{mime_type}' }})" + ), + _var_type=bytes, + _var_data=_var_data, + ) diff --git a/reflex/vars/function.py b/reflex/vars/function.py index f6340ba3498..6836e9d3b49 100644 --- a/reflex/vars/function.py +++ b/reflex/vars/function.py @@ -469,8 +469,3 @@ def create( "((__to_string) => __to_string.toString())", _var_type=ReflexCallable[[Any], str], ) -BASE64_ENCODE = FunctionStringVar.create("btoa", _var_type=ReflexCallable[[Any], str]) -CREATE_OBJECT_URL = FunctionStringVar.create( - "window.URL.createObjectURL", - _var_type=ReflexCallable[[Any], str], -) diff --git a/reflex/vars/sequence.py b/reflex/vars/sequence.py index 757ef492521..736e9cef798 100644 --- a/reflex/vars/sequence.py +++ b/reflex/vars/sequence.py @@ -33,6 +33,7 @@ var_operation, var_operation_return, ) +from .blob import Blob from .number import ( BooleanVar, LiteralNumberVar, @@ -469,19 +470,6 @@ def foreach(self, fn: Any): return map_array_operation(self, function_var) - def to_blob(self, mime_type: str | Var[str]): - """Convert the array to a Blob object. - - Args: - mime_type: The MIME type for the Blob. - - Returns: - A Blob object created from the array data. - """ - return var_operation_return( - js_expression=f"new Blob([new Uint8Array({self})], {{ type: '{mime_type}' }})", - ) - @dataclasses.dataclass( eq=False, @@ -873,6 +861,45 @@ def replace(self, search_value: Any, new_value: Any) -> StringVar: # pyright: i return string_replace_operation(self, search_value, new_value) + def encode(self, encoding: StringVar | str = "utf-8"): + """Encode the string to bytes using the specified encoding. + + Args: + encoding: The character encoding to use. Defaults to "utf-8". + + Returns: + The encoded bytes. + """ + return string_encode_operation(self, encoding) + + def to_blob(self, mime_type: StringVar | str = "text/plain"): + """Convert the string to a Blob object. + + Args: + mime_type: The MIME type of the blob. Defaults to "text/plain". + + Returns: + A Blob object containing the string data. + """ + return blob_object_create_operation(self, mime_type=mime_type) + + +@var_operation +def string_encode_operation(value: StringVar[Any] | str, encoding: StringVar | str): + """Encode a string to bytes using the specified encoding. + + Args: + value: The string to encode. + encoding: The character encoding to use. + + Returns: + The encoded bytes. + """ + return var_operation_return( + f"(new TextEncoder({encoding})).encode({value})", + var_type=bytes, + ) + @var_operation def string_lt_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): @@ -1300,6 +1327,120 @@ def json(self) -> str: return json.dumps(self._var_value) +class BytesVar(Var[bytes], python_types=bytes): + """A variable that represents Python bytes as JavaScript Uint8Array.""" + + @classmethod + def create( + cls, value: str | bytes, _var_data: VarData | None = None + ) -> LiteralBytesVar: + """Create a BytesVar from a string or bytes value. + + Args: + value: The string or bytes value to create the var from. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + A LiteralBytesVar representing the bytes value. + """ + if isinstance(value, str): + value = value.encode() + return LiteralVar.create(value, _var_data=_var_data) + + def decode(self, encoding: StringVar | str = "utf-8"): + """Decode bytes to a string using the specified encoding. + + Args: + encoding: The character encoding to use for decoding. Defaults to "utf-8". + + Returns: + A StringVar containing the decoded string. + """ + return bytes_decode_operation(self, encoding) + + def to_blob(self, mime_type: StringVar | str = "application/octet-stream"): + """Convert the bytes to a Blob object. + + Args: + mime_type: The MIME type of the blob. Defaults to "application/octet-stream". + + Returns: + A Blob object containing the bytes data. + """ + return blob_object_create_operation(self, mime_type=mime_type) + + +@var_operation +def bytes_decode_operation(value: BytesVar, encoding: StringVar | str): + """Decode bytes to a string using the specified encoding. + + Args: + value: The BytesVar to decode. + encoding: The character encoding to use for decoding. + + Returns: + A StringVar containing the decoded string. + """ + return var_operation_return( + f"(new TextDecoder({encoding})).decode(new Uint8Array({value}))", + var_type=str, + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralBytesVar(LiteralVar, BytesVar): + """A literal version of BytesVar.""" + + _var_value: bytes = dataclasses.field(default=b"") + + @classmethod + def create(cls, value: bytes, _var_data: VarData | None = None) -> BytesVar: + """Create a LiteralBytesVar from a bytes value. + + Args: + value: The bytes value to create the variable from. + _var_data: Additional variable data, by default None. + + Returns: + A literal bytes variable representing the given bytes value. + """ + return cls( + _js_expr=f"new Uint8Array({list(value)})", + _var_type=bytes, + _var_data=_var_data, + _var_value=value, + ) + + +@var_operation +def blob_object_create_operation( + value: StringVar[Any] | BytesVar, + mime_type: StringVar[Any] | str, +) -> CustomVarOperationReturn[Blob]: + """Create a Blob object from string or bytes data. + + Args: + value: The string or bytes data to convert to a Blob. + mime_type: The MIME type of the blob. + + Returns: + A Blob object containing the data. + """ + if isinstance(value, BytesVar): + return var_operation_return( + js_expression=f"new Blob([new Uint8Array({value})], {{ type: {mime_type} }})", + var_type=Blob, + ) + return var_operation_return( + js_expression=f"new Blob([{value}], {{ type: {mime_type} }})", + var_type=Blob, + ) + + @dataclasses.dataclass( eq=False, frozen=True, diff --git a/uv.lock b/uv.lock index bd234f09a68..ce06746f8f5 100644 --- a/uv.lock +++ b/uv.lock @@ -589,6 +589,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, @@ -598,6 +600,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, @@ -607,6 +611,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, @@ -616,6 +622,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, @@ -623,6 +631,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] From ea9f14ba122b528d23beb8a71e8d790c0a0c432c Mon Sep 17 00:00:00 2001 From: Casey Riebe Date: Tue, 11 Nov 2025 06:08:15 -0600 Subject: [PATCH 4/5] Using _var_type and Blob dataclass --- reflex/vars/base.py | 10 ++++++- reflex/vars/blob.py | 71 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 7d69992b3a8..12907b73218 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -76,7 +76,7 @@ from reflex.constants.colors import Color from reflex.state import BaseState - from .blob import Blob, BlobVar + from .blob import Blob, BlobVar, LiteralBlobVar from .color import LiteralColorVar from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar from .object import LiteralObjectVar, ObjectVar @@ -671,6 +671,14 @@ def create( # pyright: ignore [reportOverlappingOverload] _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] diff --git a/reflex/vars/blob.py b/reflex/vars/blob.py index ef087318bcb..287d32c0c67 100644 --- a/reflex/vars/blob.py +++ b/reflex/vars/blob.py @@ -19,6 +19,9 @@ class Blob: """Represents a JavaScript Blob object.""" + data: str | bytes = "" + mime_type: str = "" + BLOB_T = TypeVar("BLOB_T", bound=bytes | str, covariant=True) @@ -26,11 +29,35 @@ class Blob: 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 + @classmethod def create( cls, - value: str | bytes | Var, - mime_type: str | Var, + 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. @@ -43,12 +70,21 @@ def create( Returns: A BlobVar instance representing the JavaScript Blob object. """ - if not isinstance(value, Var): - value = LiteralVar.create(value) + if mime_type is None: + mime_type = cls._determine_mime_type(value) + if not isinstance(mime_type, Var): mime_type = LiteralVar.create(mime_type) - elif type(value).__qualname__.endswith("BytesCastedVar"): + + 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})" + return cls( _js_expr=f"new Blob([{value}], {{ type: {mime_type} }})", _var_type=Blob, @@ -93,8 +129,8 @@ class LiteralBlobVar(LiteralVar, BlobVar): @classmethod def create( cls, - value: bytes | str, - mime_type: str, + value: bytes | str | Blob, + mime_type: str | None = None, _var_data: VarData | None = None, ) -> BlobVar: """Create a literal Blob variable from bytes or string data. @@ -107,12 +143,21 @@ def create( 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([new Uint8Array({list(value)})], {{ type: '{mime_type}' }})" - if isinstance(value, bytes) - else f"new Blob([{value}], {{ type: '{mime_type}' }})" - ), - _var_type=bytes, + _js_expr=f"new Blob([{value}], {{ type: '{mime_type}' }})", + _var_type=var_type, _var_data=_var_data, ) From 08097dd473393321654293b51f07613821b794ad Mon Sep 17 00:00:00 2001 From: Casey Riebe Date: Tue, 18 Nov 2025 08:17:43 -0600 Subject: [PATCH 5/5] Removing silly change --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 682a4cad815..effed9dbac6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,6 +123,7 @@ reportIncompatibleMethodOverride = false target-version = "py310" output-format = "concise" lint.isort.split-on-trailing-comma = false +preview = true lint.select = ["ALL"] lint.ignore = [ "A", @@ -167,7 +168,6 @@ lint.flake8-bugbear.extend-immutable-calls = [ "reflex.utils.types.Unset", "reflex.vars.base.Var.create", ] -preview = true [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"]