[ENG-3867] Garden Variety Pickle (#4054)

* Use regular `pickle` module from stdlib

* Avoid recreating the rx.State tree for every `get_state`

* Remove dill dependency

* relock deps
This commit is contained in:
Masen Furer 2024-10-03 19:19:06 -07:00 committed by GitHub
parent fafdeb892e
commit d77b900bd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 103 additions and 85 deletions

96
poetry.lock generated
View File

@ -516,21 +516,6 @@ files = [
{file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"},
] ]
[[package]]
name = "dill"
version = "0.3.8"
description = "serialize all of Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"},
{file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"},
]
[package.extras]
graph = ["objgraph (>=1.7.2)"]
profile = ["gprof2dot (>=2022.7.29)"]
[[package]] [[package]]
name = "distlib" name = "distlib"
version = "0.3.8" version = "0.3.8"
@ -719,13 +704,13 @@ files = [
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "1.0.5" version = "1.0.6"
description = "A minimal low-level HTTP client." description = "A minimal low-level HTTP client."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"},
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"},
] ]
[package.dependencies] [package.dependencies]
@ -736,7 +721,7 @@ h11 = ">=0.13,<0.15"
asyncio = ["anyio (>=4.0,<5.0)"] asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"] http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"] socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<0.26.0)"] trio = ["trio (>=0.22.0,<1.0)"]
[[package]] [[package]]
name = "httpx" name = "httpx"
@ -863,21 +848,25 @@ test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-c
[[package]] [[package]]
name = "jaraco-functools" name = "jaraco-functools"
version = "4.0.2" version = "4.1.0"
description = "Functools like those found in stdlib" description = "Functools like those found in stdlib"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "jaraco.functools-4.0.2-py3-none-any.whl", hash = "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3"}, {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"},
{file = "jaraco_functools-4.0.2.tar.gz", hash = "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5"}, {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"},
] ]
[package.dependencies] [package.dependencies]
more-itertools = "*" more-itertools = "*"
[package.extras] [package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
test = ["jaraco.classes", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] enabler = ["pytest-enabler (>=2.2)"]
test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"]
type = ["pytest-mypy"]
[[package]] [[package]]
name = "jeepney" name = "jeepney"
@ -1788,13 +1777,13 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "pyproject-hooks" name = "pyproject-hooks"
version = "1.1.0" version = "1.2.0"
description = "Wrappers to call pyproject.toml-based build backend hooks." description = "Wrappers to call pyproject.toml-based build backend hooks."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2"}, {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"},
{file = "pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965"}, {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"},
] ]
[[package]] [[package]]
@ -1992,13 +1981,13 @@ docs = ["sphinx"]
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.10" version = "0.0.12"
description = "A streaming multipart parser for Python" description = "A streaming multipart parser for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "python_multipart-0.0.10-py3-none-any.whl", hash = "sha256:2b06ad9e8d50c7a8db80e3b56dab590137b323410605af2be20d62a5f1ba1dc8"}, {file = "python_multipart-0.0.12-py3-none-any.whl", hash = "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"},
{file = "python_multipart-0.0.10.tar.gz", hash = "sha256:46eb3c6ce6fdda5fb1a03c7e11d490e407c6930a2703fe7aef4da71c374688fa"}, {file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"},
] ]
[[package]] [[package]]
@ -2143,31 +2132,31 @@ md = ["cmarkgfm (>=0.8.0)"]
[[package]] [[package]]
name = "redis" name = "redis"
version = "5.0.8" version = "5.1.0"
description = "Python client for Redis database and key-value store" description = "Python client for Redis database and key-value store"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, {file = "redis-5.1.0-py3-none-any.whl", hash = "sha256:fd4fccba0d7f6aa48c58a78d76ddb4afc698f5da4a2c1d03d916e4fd7ab88cdd"},
{file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, {file = "redis-5.1.0.tar.gz", hash = "sha256:b756df1e4a3858fcc0ef861f3fc53623a96c41e2b1f5304e09e0fe758d333d40"},
] ]
[package.dependencies] [package.dependencies]
async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""}
[package.extras] [package.extras]
hiredis = ["hiredis (>1.0.0)"] hiredis = ["hiredis (>=3.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"]
[[package]] [[package]]
name = "reflex-chakra" name = "reflex-chakra"
version = "0.6.0" version = "0.6.1"
description = "reflex using chakra components" description = "reflex using chakra components"
optional = false optional = false
python-versions = "<4.0,>=3.8" python-versions = "<4.0,>=3.8"
files = [ files = [
{file = "reflex_chakra-0.6.0-py3-none-any.whl", hash = "sha256:eca1593fca67289e05591dd21fbcc8632c119d64a08bdc41fd995055a114cc91"}, {file = "reflex_chakra-0.6.1-py3-none-any.whl", hash = "sha256:824d461264b6d2c836ba4a2a430e677a890b82e83da149672accfc58786442fa"},
{file = "reflex_chakra-0.6.0.tar.gz", hash = "sha256:db1c7b48f1ba547bf91e5af103fce6fc7191d7225b414ebfbada7d983e33dd87"}, {file = "reflex_chakra-0.6.1.tar.gz", hash = "sha256:4b9b3c8bada19cbb4d1b8d8bc4ab0460ec008a91f380010c34d416d5b613dc07"},
] ]
[package.dependencies] [package.dependencies]
@ -2247,18 +2236,19 @@ idna2008 = ["idna"]
[[package]] [[package]]
name = "rich" name = "rich"
version = "13.8.1" version = "13.9.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false optional = false
python-versions = ">=3.7.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, {file = "rich-13.9.1-py3-none-any.whl", hash = "sha256:b340e739f30aa58921dc477b8adaa9ecdb7cecc217be01d93730ee1bc8aa83be"},
{file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, {file = "rich-13.9.1.tar.gz", hash = "sha256:097cffdf85db1babe30cc7deba5ab3a29e1b9885047dab24c57e9a7f8a9c1466"},
] ]
[package.dependencies] [package.dependencies]
markdown-it-py = ">=2.2.0" markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0" pygments = ">=2.13.0,<3.0.0"
typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
[package.extras] [package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"] jupyter = ["ipywidgets (>=7.5.1,<9)"]
@ -2595,13 +2585,13 @@ files = [
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.2"
description = "A lil' TOML parser" description = "A lil' TOML parser"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
] ]
[[package]] [[package]]
@ -2734,13 +2724,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.30.6" version = "0.31.0"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, {file = "uvicorn-0.31.0-py3-none-any.whl", hash = "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced"},
{file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, {file = "uvicorn-0.31.0.tar.gz", hash = "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906"},
] ]
[package.dependencies] [package.dependencies]
@ -2753,13 +2743,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.26.5" version = "20.26.6"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"},
{file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"},
] ]
[package.dependencies] [package.dependencies]
@ -3011,4 +3001,4 @@ type = ["pytest-mypy"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "adccd071775567aeefe219261aeb9e222906c865745f03edb1e770edc79c44ac" content-hash = "e4b462ebfae90550ba7fa49b360d7110c0d344ee616c23989c22d866ef8f6f31"

View File

@ -27,7 +27,6 @@ packages = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.9"
dill = ">=0.3.8,<0.4"
fastapi = ">=0.96.0,!=0.111.0,!=0.111.1" fastapi = ">=0.96.0,!=0.111.0,!=0.111.1"
gunicorn = ">=20.1.0,<24.0" gunicorn = ">=20.1.0,<24.0"
jinja2 = ">=3.1.2,<4.0" jinja2 = ">=3.1.2,<4.0"

View File

@ -9,6 +9,7 @@ import dataclasses
import functools import functools
import inspect import inspect
import os import os
import pickle
import uuid import uuid
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict from collections import defaultdict
@ -19,6 +20,7 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
AsyncIterator, AsyncIterator,
BinaryIO,
Callable, Callable,
ClassVar, ClassVar,
Dict, Dict,
@ -33,7 +35,6 @@ from typing import (
get_type_hints, get_type_hints,
) )
import dill
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
from typing_extensions import Self from typing_extensions import Self
@ -76,6 +77,7 @@ from reflex.utils.exceptions import (
ImmutableStateError, ImmutableStateError,
LockExpiredError, LockExpiredError,
SetUndefinedStateVarError, SetUndefinedStateVarError,
StateSchemaMismatchError,
) )
from reflex.utils.exec import is_testing_env from reflex.utils.exec import is_testing_env
from reflex.utils.serializers import serializer from reflex.utils.serializers import serializer
@ -1914,7 +1916,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
def __getstate__(self): def __getstate__(self):
"""Get the state for redis serialization. """Get the state for redis serialization.
This method is called by cloudpickle to serialize the object. This method is called by pickle to serialize the object.
It explicitly removes parent_state and substates because those are serialized separately It explicitly removes parent_state and substates because those are serialized separately
by the StateManagerRedis to allow for better horizontal scaling as state size increases. by the StateManagerRedis to allow for better horizontal scaling as state size increases.
@ -1930,6 +1932,43 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
state["__dict__"].pop("_was_touched", None) state["__dict__"].pop("_was_touched", None)
return state return state
def _serialize(self) -> bytes:
"""Serialize the state for redis.
Returns:
The serialized state.
"""
return pickle.dumps((state_to_schema(self), self))
@classmethod
def _deserialize(
cls, data: bytes | None = None, fp: BinaryIO | None = None
) -> BaseState:
"""Deserialize the state from redis/disk.
data and fp are mutually exclusive, but one must be provided.
Args:
data: The serialized state data.
fp: The file pointer to the serialized state data.
Returns:
The deserialized state.
Raises:
ValueError: If both data and fp are provided, or neither are provided.
StateSchemaMismatchError: If the state schema does not match the expected schema.
"""
if data is not None and fp is None:
(substate_schema, state) = pickle.loads(data)
elif fp is not None and data is None:
(substate_schema, state) = pickle.load(fp)
else:
raise ValueError("Only one of `data` or `fp` must be provided")
if substate_schema != state_to_schema(state):
raise StateSchemaMismatchError()
return state
class State(BaseState): class State(BaseState):
"""The app Base State.""" """The app Base State."""
@ -2086,7 +2125,11 @@ class ComponentState(State, mixin=True):
""" """
cls._per_component_state_instance_count += 1 cls._per_component_state_instance_count += 1
state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}" state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}"
component_state = type(state_cls_name, (cls, State), {}, mixin=False) component_state = type(
state_cls_name, (cls, State), {"__module__": __name__}, mixin=False
)
# Save a reference to the dynamic state for pickle/unpickle.
globals()[state_cls_name] = component_state
component = component_state.get_component(*children, **props) component = component_state.get_component(*children, **props)
component.State = component_state component.State = component_state
return component return component
@ -2552,7 +2595,7 @@ def is_serializable(value: Any) -> bool:
Whether the value is serializable. Whether the value is serializable.
""" """
try: try:
return bool(dill.dumps(value)) return bool(pickle.dumps(value))
except Exception: except Exception:
return False return False
@ -2688,8 +2731,7 @@ class StateManagerDisk(StateManager):
if token_path.exists(): if token_path.exists():
try: try:
with token_path.open(mode="rb") as file: with token_path.open(mode="rb") as file:
(substate_schema, substate) = dill.load(file) substate = BaseState._deserialize(fp=file)
if substate_schema == state_to_schema(substate):
await self.populate_substates(client_token, substate, root_state) await self.populate_substates(client_token, substate, root_state)
return substate return substate
except Exception: except Exception:
@ -2731,10 +2773,12 @@ class StateManagerDisk(StateManager):
client_token, substate_address = _split_substate_key(token) client_token, substate_address = _split_substate_key(token)
root_state_token = _substate_key(client_token, substate_address.split(".")[0]) root_state_token = _substate_key(client_token, substate_address.split(".")[0])
root_state = self.states.get(root_state_token)
if root_state is None:
# Create a new root state which will be persisted in the next set_state call.
root_state = self.state(_reflex_internal_init=True)
return await self.load_state( return await self.load_state(root_state_token, root_state)
root_state_token, self.state(_reflex_internal_init=True)
)
async def set_state_for_substate(self, client_token: str, substate: BaseState): async def set_state_for_substate(self, client_token: str, substate: BaseState):
"""Set the state for a substate. """Set the state for a substate.
@ -2747,7 +2791,7 @@ class StateManagerDisk(StateManager):
self.states[substate_token] = substate self.states[substate_token] = substate
state_dilled = dill.dumps((state_to_schema(substate), substate)) state_dilled = substate._serialize()
if not self.states_directory.exists(): if not self.states_directory.exists():
self.states_directory.mkdir(parents=True, exist_ok=True) self.states_directory.mkdir(parents=True, exist_ok=True)
self.token_path(substate_token).write_bytes(state_dilled) self.token_path(substate_token).write_bytes(state_dilled)
@ -2790,25 +2834,6 @@ class StateManagerDisk(StateManager):
await self.set_state(token, state) await self.set_state(token, state)
# Workaround https://github.com/cloudpipe/cloudpickle/issues/408 for dynamic pydantic classes
if not isinstance(State.validate.__func__, FunctionType):
cython_function_or_method = type(State.validate.__func__)
@dill.register(cython_function_or_method)
def _dill_reduce_cython_function_or_method(pickler, obj):
# Ignore cython function when pickling.
pass
@dill.register(type(State))
def _dill_reduce_state(pickler, obj):
if obj is not State and issubclass(obj, State):
# Avoid serializing subclasses of State, instead get them by reference from the State class.
pickler.save_reduce(State.get_class_substate, (obj.get_full_name(),), obj=obj)
else:
dill.Pickler.dispatch[type](pickler, obj)
def _default_lock_expiration() -> int: def _default_lock_expiration() -> int:
"""Get the default lock expiration time. """Get the default lock expiration time.
@ -2948,7 +2973,7 @@ class StateManagerRedis(StateManager):
if redis_state is not None: if redis_state is not None:
# Deserialize the substate. # Deserialize the substate.
state = dill.loads(redis_state) state = BaseState._deserialize(data=redis_state)
# Populate parent state if missing and requested. # Populate parent state if missing and requested.
if parent_state is None: if parent_state is None:
@ -3060,7 +3085,7 @@ class StateManagerRedis(StateManager):
) )
# Persist only the given state (parents or substates are excluded by BaseState.__getstate__). # Persist only the given state (parents or substates are excluded by BaseState.__getstate__).
if state._get_was_touched(): if state._get_was_touched():
pickle_state = dill.dumps(state, byref=True) pickle_state = state._serialize()
self._warn_if_too_large(state, len(pickle_state)) self._warn_if_too_large(state, len(pickle_state))
await self.redis.set( await self.redis.set(
_substate_key(client_token, state), _substate_key(client_token, state),

View File

@ -123,3 +123,7 @@ class DynamicComponentMissingLibrary(ReflexError, ValueError):
class SetUndefinedStateVarError(ReflexError, AttributeError): class SetUndefinedStateVarError(ReflexError, AttributeError):
"""Raised when setting the value of a var without first declaring it.""" """Raised when setting the value of a var without first declaring it."""
class StateSchemaMismatchError(ReflexError, TypeError):
"""Raised when the serialized schema of a state class does not match the current schema."""