Skip to content

Commit cdb4d24

Browse files
committed
Merge remote-tracking branch 'origin/main' into rolldown-vite-7116
2 parents fbf536d + 5fd8ea9 commit cdb4d24

31 files changed

+581
-455
lines changed

.github/workflows/integration_app_harness.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
strategy:
2727
matrix:
2828
state_manager: ["redis", "memory"]
29-
python-version: ["3.11", "3.12", "3.13"]
29+
python-version: ["3.11", "3.12", "3.13", "3.14"]
3030
split_index: [1, 2]
3131
fail-fast: false
3232
runs-on: ubuntu-22.04

.github/workflows/integration_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ jobs:
148148
- uses: actions/checkout@v4
149149
- uses: ./.github/actions/setup_build_env
150150
with:
151-
python-version: 3.13
151+
python-version: 3.14
152152
run-uv-sync: true
153153

154154
- name: Create app directory

.github/workflows/performance.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: Set up Python
3232
uses: actions/setup-python@v5
3333
with:
34-
python-version: "3.13"
34+
python-version: "3.14"
3535

3636
- name: Install dependencies
3737
run: uv sync --all-extras --dev

.github/workflows/unit_tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
fail-fast: false
2929
matrix:
3030
os: [ubuntu-latest, windows-latest]
31-
python-version: ["3.10", "3.11", "3.12", "3.13"]
31+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
3232
runs-on: ${{ matrix.os }}
3333

3434
# Service containers to run with `runner-job`
@@ -77,7 +77,7 @@ jobs:
7777
strategy:
7878
fail-fast: false
7979
matrix:
80-
python-version: ["3.10", "3.11", "3.12", "3.13"]
80+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
8181
runs-on: macos-latest
8282
steps:
8383
- uses: actions/checkout@v4

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ requires-python = ">=3.10,<4.0"
2222
dependencies = [
2323
"alembic >=1.15.2,<2.0",
2424
"click >=8.2",
25-
"granian[reload] >=2.4.0",
25+
"granian[reload] >=2.5.5",
2626
"httpx >=0.23.3,<1.0",
2727
"packaging >=24.2,<26",
2828
"platformdirs >=4.3.7,<5.0",
@@ -33,7 +33,7 @@ dependencies = [
3333
"redis >=5.2.1,<7.0",
3434
"reflex-hosting-cli >=0.1.55",
3535
"rich >=13,<15",
36-
"sqlmodel >=0.0.24,<0.1",
36+
"sqlmodel >=0.0.27,<0.1",
3737
"starlette >=0.47.0",
3838
"typing_extensions >=4.13.0",
3939
"wrapt >=1.17.0,<2.0",
@@ -47,6 +47,7 @@ classifiers = [
4747
"Programming Language :: Python :: 3.11",
4848
"Programming Language :: Python :: 3.12",
4949
"Programming Language :: Python :: 3.13",
50+
"Programming Language :: Python :: 3.14",
5051
]
5152

5253
[project.optional-dependencies]

reflex/base.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
if find_spec("pydantic") and find_spec("pydantic.v1"):
66
from pydantic.v1 import BaseModel
77

8-
class Base(BaseModel):
8+
from reflex.utils.compat import ModelMetaclassLazyAnnotations
9+
10+
class Base(BaseModel, metaclass=ModelMetaclassLazyAnnotations):
911
"""The base class subclassed by all Reflex classes.
1012
1113
This class wraps Pydantic and provides common methods such as
@@ -22,22 +24,17 @@ class Config:
2224
use_enum_values = True
2325
extra = "allow"
2426

25-
def __init__(self, *args, **kwargs):
26-
"""Initialize the base class.
27-
28-
Args:
29-
*args: Positional arguments.
30-
**kwargs: Keyword arguments.
31-
"""
27+
def __init_subclass__(cls):
28+
"""Warn that rx.Base is deprecated."""
3229
from reflex.utils import console
3330

3431
console.deprecate(
3532
feature_name="rx.Base",
36-
reason="You can subclass from `pydantic.BaseModel` directly instead or use dataclasses if possible.",
37-
deprecation_version="0.8.2",
33+
reason=f"{cls!r} is subclassing rx.Base. You can subclass from `pydantic.BaseModel` directly instead or use dataclasses if possible.",
34+
deprecation_version="0.8.15",
3835
removal_version="0.9.0",
3936
)
40-
super().__init__(*args, **kwargs)
37+
super().__init_subclass__()
4138

4239
def json(self) -> str:
4340
"""Convert the object to a json string.

reflex/components/field.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Annotated, Any, Generic, TypeVar, get_origin
88

99
from reflex.utils import types
10+
from reflex.utils.compat import annotations_from_namespace
1011

1112
FIELD_TYPE = TypeVar("FIELD_TYPE")
1213

@@ -117,7 +118,8 @@ def _resolve_annotations(
117118
cls, namespace: dict[str, Any], name: str
118119
) -> dict[str, Any]:
119120
return types.resolve_annotations(
120-
namespace.get("__annotations__", {}), namespace["__module__"]
121+
annotations_from_namespace(namespace),
122+
namespace["__module__"],
121123
)
122124

123125
@classmethod

reflex/constants/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ class Templates(SimpleNamespace):
134134
# The reflex.build frontend host
135135
REFLEX_BUILD_FRONTEND = "https://build.reflex.dev"
136136

137+
# The reflex.build frontend with referrer
138+
REFLEX_BUILD_FRONTEND_WITH_REFERRER = (
139+
f"{REFLEX_BUILD_FRONTEND}/?utm_source=reflex_cli"
140+
)
141+
137142
class Dirs(SimpleNamespace):
138143
"""Folders used by the template system of Reflex."""
139144

reflex/event.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,6 +1866,9 @@ def fix_events(
18661866
# Fix the events created by the handler.
18671867
out = []
18681868
for e in events:
1869+
if callable(e) and getattr(e, "__name__", "") == "<lambda>":
1870+
# A lambda was returned, assume the user wants to call it with no args.
1871+
e = e()
18691872
if isinstance(e, Event):
18701873
# If the event is already an event, append it to the list.
18711874
out.append(e)

reflex/istate/manager/__init__.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""State manager for managing client states."""
2+
3+
import contextlib
4+
import dataclasses
5+
from abc import ABC, abstractmethod
6+
from collections.abc import AsyncIterator
7+
8+
from reflex import constants
9+
from reflex.config import get_config
10+
from reflex.state import BaseState
11+
from reflex.utils import console, prerequisites
12+
from reflex.utils.exceptions import InvalidStateManagerModeError
13+
14+
15+
@dataclasses.dataclass
16+
class StateManager(ABC):
17+
"""A class to manage many client states."""
18+
19+
# The state class to use.
20+
state: type[BaseState]
21+
22+
@classmethod
23+
def create(cls, state: type[BaseState]):
24+
"""Create a new state manager.
25+
26+
Args:
27+
state: The state class to use.
28+
29+
Raises:
30+
InvalidStateManagerModeError: If the state manager mode is invalid.
31+
32+
Returns:
33+
The state manager (either disk, memory or redis).
34+
"""
35+
config = get_config()
36+
if prerequisites.parse_redis_url() is not None:
37+
config.state_manager_mode = constants.StateManagerMode.REDIS
38+
if config.state_manager_mode == constants.StateManagerMode.MEMORY:
39+
from reflex.istate.manager.memory import StateManagerMemory
40+
41+
return StateManagerMemory(state=state)
42+
if config.state_manager_mode == constants.StateManagerMode.DISK:
43+
from reflex.istate.manager.disk import StateManagerDisk
44+
45+
return StateManagerDisk(state=state)
46+
if config.state_manager_mode == constants.StateManagerMode.REDIS:
47+
redis = prerequisites.get_redis()
48+
if redis is not None:
49+
from reflex.istate.manager.redis import StateManagerRedis
50+
51+
# make sure expiration values are obtained only from the config object on creation
52+
return StateManagerRedis(
53+
state=state,
54+
redis=redis,
55+
token_expiration=config.redis_token_expiration,
56+
lock_expiration=config.redis_lock_expiration,
57+
lock_warning_threshold=config.redis_lock_warning_threshold,
58+
)
59+
msg = f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
60+
raise InvalidStateManagerModeError(msg)
61+
62+
@abstractmethod
63+
async def get_state(self, token: str) -> BaseState:
64+
"""Get the state for a token.
65+
66+
Args:
67+
token: The token to get the state for.
68+
69+
Returns:
70+
The state for the token.
71+
"""
72+
73+
@abstractmethod
74+
async def set_state(self, token: str, state: BaseState):
75+
"""Set the state for a token.
76+
77+
Args:
78+
token: The token to set the state for.
79+
state: The state to set.
80+
"""
81+
82+
@abstractmethod
83+
@contextlib.asynccontextmanager
84+
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
85+
"""Modify the state for a token while holding exclusive lock.
86+
87+
Args:
88+
token: The token to modify the state for.
89+
90+
Yields:
91+
The state for the token.
92+
"""
93+
yield self.state()
94+
95+
96+
def _default_token_expiration() -> int:
97+
"""Get the default token expiration time.
98+
99+
Returns:
100+
The default token expiration time.
101+
"""
102+
return get_config().redis_token_expiration
103+
104+
105+
def reset_disk_state_manager():
106+
"""Reset the disk state manager."""
107+
console.debug("Resetting disk state manager.")
108+
states_directory = prerequisites.get_states_dir()
109+
if states_directory.exists():
110+
for path in states_directory.iterdir():
111+
path.unlink()
112+
113+
114+
def get_state_manager() -> StateManager:
115+
"""Get the state manager for the app that is currently running.
116+
117+
Returns:
118+
The state manager.
119+
"""
120+
return prerequisites.get_and_validate_app().app.state_manager

0 commit comments

Comments
 (0)