reflex 0.4.1a1__py3-none-any.whl → 0.4.2a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

Files changed (44) hide show
  1. reflex/.templates/jinja/web/pages/_app.js.jinja2 +2 -2
  2. reflex/.templates/jinja/web/utils/context.js.jinja2 +9 -2
  3. reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +5 -3
  4. reflex/.templates/web/utils/state.js +35 -8
  5. reflex/app.py +5 -4
  6. reflex/base.py +4 -0
  7. reflex/compiler/compiler.py +14 -4
  8. reflex/compiler/templates.py +1 -0
  9. reflex/components/base/bare.pyi +84 -0
  10. reflex/components/component.py +23 -4
  11. reflex/components/core/cond.py +2 -2
  12. reflex/components/core/debounce.py +1 -1
  13. reflex/components/core/upload.py +2 -2
  14. reflex/components/core/upload.pyi +2 -2
  15. reflex/components/radix/primitives/accordion.py +5 -2
  16. reflex/components/radix/primitives/accordion.pyi +1 -1
  17. reflex/components/radix/primitives/progress.py +40 -8
  18. reflex/components/radix/primitives/progress.pyi +71 -2
  19. reflex/components/radix/themes/base.py +9 -2
  20. reflex/components/radix/themes/base.pyi +3 -1
  21. reflex/components/radix/themes/color_mode.py +1 -1
  22. reflex/components/radix/themes/layout/stack.py +6 -5
  23. reflex/components/radix/themes/layout/stack.pyi +6 -5
  24. reflex/constants/base.pyi +94 -0
  25. reflex/constants/compiler.py +2 -0
  26. reflex/constants/event.pyi +59 -0
  27. reflex/constants/installer.py +2 -2
  28. reflex/constants/route.pyi +50 -0
  29. reflex/constants/style.pyi +20 -0
  30. reflex/event.py +10 -0
  31. reflex/middleware/hydrate_middleware.py +0 -8
  32. reflex/state.py +194 -13
  33. reflex/testing.py +1 -0
  34. reflex/utils/prerequisites.py +31 -4
  35. reflex/utils/processes.py +12 -1
  36. reflex/utils/serializers.py +18 -2
  37. reflex/utils/types.py +6 -1
  38. reflex/vars.py +20 -1
  39. reflex/vars.pyi +2 -0
  40. {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/METADATA +2 -1
  41. {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/RECORD +44 -39
  42. {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/WHEEL +1 -1
  43. {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/LICENSE +0 -0
  44. {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/entry_points.txt +0 -0
reflex/state.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Define the reflex state specification."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import asyncio
@@ -213,21 +214,29 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
213
214
  # The router data for the current page
214
215
  router: RouterData = RouterData()
215
216
 
216
- def __init__(self, *args, parent_state: BaseState | None = None, **kwargs):
217
+ def __init__(
218
+ self,
219
+ *args,
220
+ parent_state: BaseState | None = None,
221
+ init_substates: bool = True,
222
+ **kwargs,
223
+ ):
217
224
  """Initialize the state.
218
225
 
219
226
  Args:
220
227
  *args: The args to pass to the Pydantic init method.
221
228
  parent_state: The parent state.
229
+ init_substates: Whether to initialize the substates in this instance.
222
230
  **kwargs: The kwargs to pass to the Pydantic init method.
223
231
 
224
232
  """
225
233
  kwargs["parent_state"] = parent_state
226
234
  super().__init__(*args, **kwargs)
227
235
 
228
- # Setup the substates.
229
- for substate in self.get_substates():
230
- self.substates[substate.get_name()] = substate(parent_state=self)
236
+ # Setup the substates (for memory state manager only).
237
+ if init_substates:
238
+ for substate in self.get_substates():
239
+ self.substates[substate.get_name()] = substate(parent_state=self)
231
240
  # Convert the event handlers to functions.
232
241
  self._init_event_handlers()
233
242
 
@@ -1002,7 +1011,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1002
1011
  for substate in self.substates.values():
1003
1012
  substate._reset_client_storage()
1004
1013
 
1005
- def get_substate(self, path: Sequence[str]) -> BaseState | None:
1014
+ def get_substate(self, path: Sequence[str]) -> BaseState:
1006
1015
  """Get the substate.
1007
1016
 
1008
1017
  Args:
@@ -1257,6 +1266,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1257
1266
  # Recursively find the substate deltas.
1258
1267
  substates = self.substates
1259
1268
  for substate in self.dirty_substates.union(self._always_dirty_substates):
1269
+ if substate not in substates:
1270
+ continue # substate not loaded at this time, no delta
1260
1271
  delta.update(substates[substate].get_delta())
1261
1272
 
1262
1273
  # Format the delta.
@@ -1284,6 +1295,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1284
1295
  for var in self.dirty_vars:
1285
1296
  for substate_name in self._substate_var_dependencies[var]:
1286
1297
  self.dirty_substates.add(substate_name)
1298
+ if substate_name not in substates:
1299
+ continue
1287
1300
  substate = substates[substate_name]
1288
1301
  substate.dirty_vars.add(var)
1289
1302
  substate._mark_dirty()
@@ -1292,6 +1305,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1292
1305
  """Reset the dirty vars."""
1293
1306
  # Recursively clean the substates.
1294
1307
  for substate in self.dirty_substates:
1308
+ if substate not in self.substates:
1309
+ continue
1295
1310
  self.substates[substate]._clean()
1296
1311
 
1297
1312
  # Clean this state.
@@ -1377,6 +1392,24 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1377
1392
  """
1378
1393
  pass
1379
1394
 
1395
+ def __getstate__(self):
1396
+ """Get the state for redis serialization.
1397
+
1398
+ This method is called by cloudpickle to serialize the object.
1399
+
1400
+ It explicitly removes parent_state and substates because those are serialized separately
1401
+ by the StateManagerRedis to allow for better horizontal scaling as state size increases.
1402
+
1403
+ Returns:
1404
+ The state dict for serialization.
1405
+ """
1406
+ state = super().__getstate__()
1407
+ # Never serialize parent_state or substates
1408
+ state["__dict__"] = state["__dict__"].copy()
1409
+ state["__dict__"]["parent_state"] = None
1410
+ state["__dict__"]["substates"] = {}
1411
+ return state
1412
+
1380
1413
 
1381
1414
  class State(BaseState):
1382
1415
  """The app Base State."""
@@ -1405,6 +1438,23 @@ class State(BaseState):
1405
1438
  type(self).set_is_hydrated(True), # type: ignore
1406
1439
  ]
1407
1440
 
1441
+ def update_vars_internal(self, vars: dict[str, Any]) -> None:
1442
+ """Apply updates to fully qualified state vars.
1443
+
1444
+ The keys in `vars` should be in the form of `{state.get_full_name()}.{var_name}`,
1445
+ and each value will be set on the appropriate substate instance.
1446
+
1447
+ This function is primarily used to apply cookie and local storage
1448
+ updates from the frontend to the appropriate substate.
1449
+
1450
+ Args:
1451
+ vars: The fully qualified vars and values to update.
1452
+ """
1453
+ for var, value in vars.items():
1454
+ state_name, _, var_name = var.rpartition(".")
1455
+ var_state = self.get_substate(state_name.split("."))
1456
+ setattr(var_state, var_name, value)
1457
+
1408
1458
 
1409
1459
  class StateProxy(wrapt.ObjectProxy):
1410
1460
  """Proxy of a state instance to control mutability of vars for a background task.
@@ -1459,6 +1509,8 @@ class StateProxy(wrapt.ObjectProxy):
1459
1509
  """
1460
1510
  self._self_actx = self._self_app.modify_state(
1461
1511
  self.__wrapped__.router.session.client_token
1512
+ + "_"
1513
+ + ".".join(self._self_substate_path)
1462
1514
  )
1463
1515
  mutable_state = await self._self_actx.__aenter__()
1464
1516
  super().__setattr__(
@@ -1655,6 +1707,8 @@ class StateManagerMemory(StateManager):
1655
1707
  Returns:
1656
1708
  The state for the token.
1657
1709
  """
1710
+ # Memory state manager ignores the substate suffix and always returns the top-level state.
1711
+ token = token.partition("_")[0]
1658
1712
  if token not in self.states:
1659
1713
  self.states[token] = self.state()
1660
1714
  return self.states[token]
@@ -1678,6 +1732,8 @@ class StateManagerMemory(StateManager):
1678
1732
  Yields:
1679
1733
  The state for the token.
1680
1734
  """
1735
+ # Memory state manager ignores the substate suffix and always returns the top-level state.
1736
+ token = token.partition("_")[0]
1681
1737
  if token not in self._states_locks:
1682
1738
  async with self._state_manager_lock:
1683
1739
  if token not in self._states_locks:
@@ -1717,23 +1773,104 @@ class StateManagerRedis(StateManager):
1717
1773
  b"evicted",
1718
1774
  }
1719
1775
 
1720
- async def get_state(self, token: str) -> BaseState:
1776
+ async def get_state(
1777
+ self,
1778
+ token: str,
1779
+ top_level: bool = True,
1780
+ get_substates: bool = True,
1781
+ parent_state: BaseState | None = None,
1782
+ ) -> BaseState:
1721
1783
  """Get the state for a token.
1722
1784
 
1723
1785
  Args:
1724
1786
  token: The token to get the state for.
1787
+ top_level: If true, return an instance of the top-level state.
1788
+ get_substates: If true, also retrieve substates
1789
+ parent_state: If provided, use this parent_state instead of getting it from redis.
1725
1790
 
1726
1791
  Returns:
1727
1792
  The state for the token.
1793
+
1794
+ Raises:
1795
+ RuntimeError: when the state_cls is not specified in the token
1728
1796
  """
1797
+ # Split the actual token from the fully qualified substate name.
1798
+ client_token, _, state_path = token.partition("_")
1799
+ if state_path:
1800
+ # Get the State class associated with the given path.
1801
+ state_cls = self.state.get_class_substate(tuple(state_path.split(".")))
1802
+ else:
1803
+ raise RuntimeError(
1804
+ "StateManagerRedis requires token to be specified in the form of {token}_{state_full_name}"
1805
+ )
1806
+
1807
+ # Fetch the serialized substate from redis.
1729
1808
  redis_state = await self.redis.get(token)
1730
- if redis_state is None:
1731
- await self.set_state(token, self.state())
1732
- return await self.get_state(token)
1733
- return cloudpickle.loads(redis_state)
1809
+
1810
+ if redis_state is not None:
1811
+ # Deserialize the substate.
1812
+ state = cloudpickle.loads(redis_state)
1813
+
1814
+ # Populate parent and substates if requested.
1815
+ if parent_state is None:
1816
+ # Retrieve the parent state from redis.
1817
+ parent_state_name = state_path.rpartition(".")[0]
1818
+ if parent_state_name:
1819
+ parent_state_key = token.rpartition(".")[0]
1820
+ parent_state = await self.get_state(
1821
+ parent_state_key, top_level=False, get_substates=False
1822
+ )
1823
+ # Set up Bidirectional linkage between this state and its parent.
1824
+ if parent_state is not None:
1825
+ parent_state.substates[state.get_name()] = state
1826
+ state.parent_state = parent_state
1827
+ if get_substates:
1828
+ # Retrieve all substates from redis.
1829
+ for substate_cls in state_cls.get_substates():
1830
+ substate_name = substate_cls.get_name()
1831
+ substate_key = token + "." + substate_name
1832
+ state.substates[substate_name] = await self.get_state(
1833
+ substate_key, top_level=False, parent_state=state
1834
+ )
1835
+ # To retain compatibility with previous implementation, by default, we return
1836
+ # the top-level state by chasing `parent_state` pointers up the tree.
1837
+ if top_level:
1838
+ while type(state) != self.state and state.parent_state is not None:
1839
+ state = state.parent_state
1840
+ return state
1841
+
1842
+ # Key didn't exist so we have to create a new entry for this token.
1843
+ if parent_state is None:
1844
+ parent_state_name = state_path.rpartition(".")[0]
1845
+ if parent_state_name:
1846
+ # Retrieve the parent state to populate event handlers onto this substate.
1847
+ parent_state_key = client_token + "_" + parent_state_name
1848
+ parent_state = await self.get_state(
1849
+ parent_state_key, top_level=False, get_substates=False
1850
+ )
1851
+ # Persist the new state class to redis.
1852
+ await self.set_state(
1853
+ token,
1854
+ state_cls(
1855
+ parent_state=parent_state,
1856
+ init_substates=False,
1857
+ ),
1858
+ )
1859
+ # After creating the state key, recursively call `get_state` to populate substates.
1860
+ return await self.get_state(
1861
+ token,
1862
+ top_level=top_level,
1863
+ get_substates=get_substates,
1864
+ parent_state=parent_state,
1865
+ )
1734
1866
 
1735
1867
  async def set_state(
1736
- self, token: str, state: BaseState, lock_id: bytes | None = None
1868
+ self,
1869
+ token: str,
1870
+ state: BaseState,
1871
+ lock_id: bytes | None = None,
1872
+ set_substates: bool = True,
1873
+ set_parent_state: bool = True,
1737
1874
  ):
1738
1875
  """Set the state for a token.
1739
1876
 
@@ -1741,11 +1878,13 @@ class StateManagerRedis(StateManager):
1741
1878
  token: The token to set the state for.
1742
1879
  state: The state to set.
1743
1880
  lock_id: If provided, the lock_key must be set to this value to set the state.
1881
+ set_substates: If True, write substates to redis
1882
+ set_parent_state: If True, write parent state to redis
1744
1883
 
1745
1884
  Raises:
1746
1885
  LockExpiredError: If lock_id is provided and the lock for the token is not held by that ID.
1747
1886
  """
1748
- # check that we're holding the lock
1887
+ # Check that we're holding the lock.
1749
1888
  if (
1750
1889
  lock_id is not None
1751
1890
  and await self.redis.get(self._lock_key(token)) != lock_id
@@ -1755,6 +1894,27 @@ class StateManagerRedis(StateManager):
1755
1894
  f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
1756
1895
  "or use `@rx.background` decorator for long-running tasks."
1757
1896
  )
1897
+ # Find the substate associated with the token.
1898
+ state_path = token.partition("_")[2]
1899
+ if state_path and state.get_full_name() != state_path:
1900
+ state = state.get_substate(tuple(state_path.split(".")))
1901
+ # Persist the parent state separately, if requested.
1902
+ if state.parent_state is not None and set_parent_state:
1903
+ parent_state_key = token.rpartition(".")[0]
1904
+ await self.set_state(
1905
+ parent_state_key,
1906
+ state.parent_state,
1907
+ lock_id=lock_id,
1908
+ set_substates=False,
1909
+ )
1910
+ # Persist the substates separately, if requested.
1911
+ if set_substates:
1912
+ for substate_name, substate in state.substates.items():
1913
+ substate_key = token + "." + substate_name
1914
+ await self.set_state(
1915
+ substate_key, substate, lock_id=lock_id, set_parent_state=False
1916
+ )
1917
+ # Persist only the given state (parents or substates are excluded by BaseState.__getstate__).
1758
1918
  await self.redis.set(token, cloudpickle.dumps(state), ex=self.token_expiration)
1759
1919
 
1760
1920
  @contextlib.asynccontextmanager
@@ -1782,7 +1942,9 @@ class StateManagerRedis(StateManager):
1782
1942
  Returns:
1783
1943
  The redis lock key for the token.
1784
1944
  """
1785
- return f"{token}_lock".encode()
1945
+ # All substates share the same lock domain, so ignore any substate path suffix.
1946
+ client_token = token.partition("_")[0]
1947
+ return f"{client_token}_lock".encode()
1786
1948
 
1787
1949
  async def _try_get_lock(self, lock_key: bytes, lock_id: bytes) -> bool | None:
1788
1950
  """Try to get a redis lock for a token.
@@ -1949,6 +2111,7 @@ class LocalStorage(ClientStorageBase, str):
1949
2111
  """Represents a state Var that is stored in localStorage in the browser."""
1950
2112
 
1951
2113
  name: str | None
2114
+ sync: bool = False
1952
2115
 
1953
2116
  def __new__(
1954
2117
  cls,
@@ -1957,6 +2120,7 @@ class LocalStorage(ClientStorageBase, str):
1957
2120
  errors: str | None = None,
1958
2121
  /,
1959
2122
  name: str | None = None,
2123
+ sync: bool = False,
1960
2124
  ) -> "LocalStorage":
1961
2125
  """Create a client-side localStorage (str).
1962
2126
 
@@ -1965,6 +2129,7 @@ class LocalStorage(ClientStorageBase, str):
1965
2129
  encoding: The encoding to use.
1966
2130
  errors: The error handling scheme to use.
1967
2131
  name: The name of the storage key on the client side.
2132
+ sync: Whether changes should be propagated to other tabs.
1968
2133
 
1969
2134
  Returns:
1970
2135
  The client-side localStorage object.
@@ -1974,6 +2139,7 @@ class LocalStorage(ClientStorageBase, str):
1974
2139
  else:
1975
2140
  inst = super().__new__(cls, object)
1976
2141
  inst.name = name
2142
+ inst.sync = sync
1977
2143
  return inst
1978
2144
 
1979
2145
 
@@ -2197,6 +2363,21 @@ class MutableProxy(wrapt.ObjectProxy):
2197
2363
  """
2198
2364
  return copy.deepcopy(self.__wrapped__, memo=memo)
2199
2365
 
2366
+ def __reduce_ex__(self, protocol_version):
2367
+ """Get the state for redis serialization.
2368
+
2369
+ This method is called by cloudpickle to serialize the object.
2370
+
2371
+ It explicitly serializes the wrapped object, stripping off the mutable proxy.
2372
+
2373
+ Args:
2374
+ protocol_version: The protocol version.
2375
+
2376
+ Returns:
2377
+ Tuple of (wrapped class, empty args, class __getstate__)
2378
+ """
2379
+ return self.__wrapped__.__reduce_ex__(protocol_version)
2380
+
2200
2381
 
2201
2382
  @serializer
2202
2383
  def serialize_mutable_proxy(mp: MutableProxy) -> SerializedType:
reflex/testing.py CHANGED
@@ -220,6 +220,7 @@ class AppHarness:
220
220
  reflex.config.get_config(reload=True)
221
221
  # reset rx.State subclasses
222
222
  State.class_subclasses.clear()
223
+ State.get_class_substate.cache_clear()
223
224
  # Ensure the AppHarness test does not skip State assignment due to running via pytest
224
225
  os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None)
225
226
  # self.app_module.app.
@@ -24,6 +24,7 @@ import pkg_resources
24
24
  import typer
25
25
  from alembic.util.exc import CommandError
26
26
  from packaging import version
27
+ from redis import Redis as RedisSync
27
28
  from redis.asyncio import Redis
28
29
 
29
30
  import reflex
@@ -189,16 +190,42 @@ def get_compiled_app(reload: bool = False) -> ModuleType:
189
190
 
190
191
 
191
192
  def get_redis() -> Redis | None:
192
- """Get the redis client.
193
+ """Get the asynchronous redis client.
193
194
 
194
195
  Returns:
195
- The redis client.
196
+ The asynchronous redis client.
197
+ """
198
+ if isinstance((redis_url_or_options := parse_redis_url()), str):
199
+ return Redis.from_url(redis_url_or_options)
200
+ elif isinstance(redis_url_or_options, dict):
201
+ return Redis(**redis_url_or_options)
202
+ return None
203
+
204
+
205
+ def get_redis_sync() -> RedisSync | None:
206
+ """Get the synchronous redis client.
207
+
208
+ Returns:
209
+ The synchronous redis client.
210
+ """
211
+ if isinstance((redis_url_or_options := parse_redis_url()), str):
212
+ return RedisSync.from_url(redis_url_or_options)
213
+ elif isinstance(redis_url_or_options, dict):
214
+ return RedisSync(**redis_url_or_options)
215
+ return None
216
+
217
+
218
+ def parse_redis_url() -> str | dict | None:
219
+ """Parse the REDIS_URL in config if applicable.
220
+
221
+ Returns:
222
+ If redis-py syntax, return the URL as it is. Otherwise, return the host/port/db as a dict.
196
223
  """
197
224
  config = get_config()
198
225
  if not config.redis_url:
199
226
  return None
200
227
  if config.redis_url.startswith(("redis://", "rediss://", "unix://")):
201
- return Redis.from_url(config.redis_url)
228
+ return config.redis_url
202
229
  console.deprecate(
203
230
  feature_name="host[:port] style redis urls",
204
231
  reason="redis-py url syntax is now being used",
@@ -209,7 +236,7 @@ def get_redis() -> Redis | None:
209
236
  if not has_port:
210
237
  redis_port = 6379
211
238
  console.info(f"Using redis at {config.redis_url}")
212
- return Redis(host=redis_url, port=int(redis_port), db=0)
239
+ return dict(host=redis_url, port=int(redis_port), db=0)
213
240
 
214
241
 
215
242
  def get_production_backend_url() -> str:
reflex/utils/processes.py CHANGED
@@ -12,6 +12,7 @@ from typing import Callable, Generator, List, Optional, Tuple, Union
12
12
 
13
13
  import psutil
14
14
  import typer
15
+ from redis.exceptions import RedisError
15
16
 
16
17
  from reflex.utils import console, path_ops, prerequisites
17
18
 
@@ -28,10 +29,20 @@ def kill(pid):
28
29
  def get_num_workers() -> int:
29
30
  """Get the number of backend worker processes.
30
31
 
32
+ Raises:
33
+ Exit: If unable to connect to Redis.
34
+
31
35
  Returns:
32
36
  The number of backend worker processes.
33
37
  """
34
- return 1 if prerequisites.get_redis() is None else (os.cpu_count() or 1) * 2 + 1
38
+ if (redis_client := prerequisites.get_redis_sync()) is None:
39
+ return 1
40
+ try:
41
+ redis_client.ping()
42
+ except RedisError as re:
43
+ console.error(f"Unable to connect to Redis: {re}")
44
+ raise typer.Exit(1) from re
45
+ return (os.cpu_count() or 1) * 2 + 1
35
46
 
36
47
 
37
48
  def get_process_on_port(port) -> Optional[psutil.Process]:
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import types as builtin_types
7
+ import warnings
7
8
  from datetime import date, datetime, time, timedelta
8
9
  from typing import Any, Callable, Dict, List, Set, Tuple, Type, Union, get_type_hints
9
10
 
@@ -303,6 +304,7 @@ try:
303
304
  import base64
304
305
  import io
305
306
 
307
+ from PIL.Image import MIME
306
308
  from PIL.Image import Image as Img
307
309
 
308
310
  @serializer
@@ -316,10 +318,24 @@ try:
316
318
  The serialized image.
317
319
  """
318
320
  buff = io.BytesIO()
319
- image.save(buff, format=getattr(image, "format", None) or "PNG")
321
+ image_format = getattr(image, "format", None) or "PNG"
322
+ image.save(buff, format=image_format)
320
323
  image_bytes = buff.getvalue()
321
324
  base64_image = base64.b64encode(image_bytes).decode("utf-8")
322
- mime_type = getattr(image, "get_format_mimetype", lambda: "image/png")()
325
+ try:
326
+ # Newer method to get the mime type, but does not always work.
327
+ mime_type = image.get_format_mimetype() # type: ignore
328
+ except AttributeError:
329
+ try:
330
+ # Fallback method
331
+ mime_type = MIME[image_format]
332
+ except KeyError:
333
+ # Unknown mime_type: warn and return image/png and hope the browser can sort it out.
334
+ warnings.warn(
335
+ f"Unknown mime type for {image} {image_format}. Defaulting to image/png"
336
+ )
337
+ mime_type = "image/png"
338
+
323
339
  return f"data:{mime_type};base64,{base64_image}"
324
340
 
325
341
  except ImportError:
reflex/utils/types.py CHANGED
@@ -10,6 +10,7 @@ from typing import (
10
10
  Any,
11
11
  Callable,
12
12
  Iterable,
13
+ List,
13
14
  Literal,
14
15
  Optional,
15
16
  Type,
@@ -164,7 +165,11 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
164
165
  prop = descriptor.property
165
166
  if not isinstance(prop, Relationship):
166
167
  return None
167
- return prop.mapper.class_
168
+ class_ = prop.mapper.class_
169
+ if prop.uselist:
170
+ return List[class_]
171
+ else:
172
+ return class_
168
173
  elif isinstance(cls, type) and issubclass(cls, Model):
169
174
  # Check in the annotations directly (for sqlmodel.Relationship)
170
175
  hints = get_type_hints(cls)
reflex/vars.py CHANGED
@@ -623,7 +623,9 @@ class Var:
623
623
 
624
624
  # Get the type of the indexed var.
625
625
  if types.is_generic_alias(self._var_type):
626
- type_ = types.get_args(self._var_type)[0]
626
+ index = i if not isinstance(i, Var) else 0
627
+ type_ = types.get_args(self._var_type)
628
+ type_ = type_[index % len(type_)]
627
629
  elif types._issubclass(self._var_type, str):
628
630
  type_ = str
629
631
 
@@ -2003,3 +2005,20 @@ class CallableVar(BaseVar):
2003
2005
  The Var returned from calling the function.
2004
2006
  """
2005
2007
  return self.fn(*args, **kwargs)
2008
+
2009
+
2010
+ def get_uuid_string_var() -> Var:
2011
+ """Return a var that generates UUIDs via .web/utils/state.js.
2012
+
2013
+ Returns:
2014
+ the var to generate UUIDs at runtime.
2015
+ """
2016
+ from reflex.utils.imports import ImportVar
2017
+
2018
+ unique_uuid_var_data = VarData(
2019
+ imports={f"/{constants.Dirs.STATE_PATH}": {ImportVar(tag="generateUUID")}} # type: ignore
2020
+ )
2021
+
2022
+ return BaseVar(
2023
+ _var_name="generateUUID()", _var_type=str, _var_data=unique_uuid_var_data
2024
+ )
reflex/vars.pyi CHANGED
@@ -157,3 +157,5 @@ def cached_var(fget: Callable[[Any], Any]) -> ComputedVar: ...
157
157
  class CallableVar(BaseVar):
158
158
  def __init__(self, fn: Callable[..., BaseVar]): ...
159
159
  def __call__(self, *args, **kwargs) -> BaseVar: ...
160
+
161
+ def get_uuid_string_var() -> Var: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reflex
3
- Version: 0.4.1a1
3
+ Version: 0.4.2a1
4
4
  Summary: Web apps in pure Python.
5
5
  Home-page: https://reflex.dev
6
6
  License: Apache-2.0
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.8
15
15
  Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
18
19
  Requires-Dist: alembic (>=1.11.1,<2.0.0)
19
20
  Requires-Dist: charset-normalizer (>=3.3.2,<4.0.0)
20
21
  Requires-Dist: cloudpickle (>=2.2.1,<3.0.0)