Skip to content

Commit 3ef3929

Browse files
authored
python3.14 compat (#5859)
* Include python3.14 in unit-tests and integration-tests * bypass PyO3 abi check * handle lazy annotations in py3.14 * [sqlmodel] fix get_annotations for python3.14 compat Guard sqlmodel import with find_spec * update deprecation versions to 0.8.15 * Add LOAD_FAST_BORROW opcode to dep_tracking Ensure that computed var dependencies can be determined on python3.14 * Monkeypatch rx.Base for python3.14 compatible annotations * remove pyO3 ABI workaround * Use 3.14 in app-harness, reflex-web, perf tests, and unit_tests for mac * pyi_generator: remove get_type_hints, wasn't doing the right thing * reflex-web is not compatible with 3.14 yet because replicate pkg still uses pydantic v1 * Consolidate implementation around annotationlib * add python3.14 classifier to pyproject * remove deprecated asyncio.iscoroutine* usage * update sqlmodel to 0.0.27 and remove compat hacks * bump granian min dep to 2.5.5
1 parent cdfa8b5 commit 3ef3929

File tree

15 files changed

+82
-28
lines changed

15 files changed

+82
-28
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: 4 additions & 2 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
@@ -34,7 +36,7 @@ def __init__(self, *args, **kwargs):
3436
console.deprecate(
3537
feature_name="rx.Base",
3638
reason="You can subclass from `pydantic.BaseModel` directly instead or use dataclasses if possible.",
37-
deprecation_version="0.8.2",
39+
deprecation_version="0.8.15",
3840
removal_version="0.9.0",
3941
)
4042
super().__init__(*args, **kwargs)

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/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ def __pydantic_init_subclass__(cls):
363363
reason=(
364364
"Register sqlmodel.SQLModel classes with `@rx.ModelRegistry.register`"
365365
),
366-
deprecation_version="0.8.0",
366+
deprecation_version="0.8.15",
367367
removal_version="0.9.0",
368368
)
369369
super().__pydantic_init_subclass__()

reflex/state.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def __call__(self, *args: Any) -> EventSpec:
246246
msg = f"Variable `{args[0]}` cannot be set on `{self.state_cls.get_full_name()}`"
247247
raise AttributeError(msg)
248248

249-
if asyncio.iscoroutinefunction(handler.fn):
249+
if inspect.iscoroutinefunction(handler.fn):
250250
msg = f"Setter for {args[0]} is async, which is not supported."
251251
raise NotImplementedError(msg)
252252

@@ -287,7 +287,7 @@ async def _resolve_delta(delta: Delta) -> Delta:
287287
tasks = {}
288288
for state_name, state_delta in delta.items():
289289
for var_name, value in state_delta.items():
290-
if asyncio.iscoroutine(value):
290+
if inspect.iscoroutine(value):
291291
tasks[state_name, var_name] = asyncio.create_task(
292292
value,
293293
name=f"reflex_resolve_delta|{state_name}|{var_name}|{time.time()}",
@@ -852,7 +852,7 @@ def _check_overridden_basevars(cls):
852852
ComputedVarShadowsBaseVarsError: When a computed var shadows a base var.
853853
"""
854854
for name, computed_var_ in cls._get_computed_vars():
855-
if name in cls.__annotations__:
855+
if name in get_type_hints(cls):
856856
msg = f"The computed var name `{computed_var_._js_expr}` shadows a base var in {cls.__module__}.{cls.__name__}; use a different name instead"
857857
raise ComputedVarShadowsBaseVarsError(msg)
858858

@@ -1735,7 +1735,7 @@ def _is_valid_type(events: Any) -> bool:
17351735
except TypeError:
17361736
pass
17371737

1738-
coroutines = [e for e in events if asyncio.iscoroutine(e)]
1738+
coroutines = [e for e in events if inspect.iscoroutine(e)]
17391739

17401740
for coroutine in coroutines:
17411741
coroutine_name = coroutine.__qualname__
@@ -1897,7 +1897,7 @@ async def _process_event(
18971897
# Wrap the function in a try/except block.
18981898
try:
18991899
# Handle async functions.
1900-
if asyncio.iscoroutinefunction(fn.func):
1900+
if inspect.iscoroutinefunction(fn.func):
19011901
events = await fn(**payload)
19021902

19031903
# Handle regular functions.

reflex/utils/compat.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Compatibility hacks and helpers."""
22

3-
from typing import TYPE_CHECKING
3+
import sys
4+
from collections.abc import Mapping
5+
from importlib.util import find_spec
6+
from typing import TYPE_CHECKING, Any
47

58
if TYPE_CHECKING:
69
from pydantic.fields import FieldInfo
@@ -30,6 +33,51 @@ async def windows_hot_reload_lifespan_hack():
3033
pass
3134

3235

36+
def annotations_from_namespace(namespace: Mapping[str, Any]) -> dict[str, Any]:
37+
"""Get the annotations from a class namespace.
38+
39+
Args:
40+
namespace: The class namespace.
41+
42+
Returns:
43+
The (forward-ref) annotations from the class namespace.
44+
"""
45+
if sys.version_info >= (3, 14) and "__annotations__" not in namespace:
46+
from annotationlib import (
47+
Format,
48+
call_annotate_function,
49+
get_annotate_from_class_namespace,
50+
)
51+
52+
if annotate := get_annotate_from_class_namespace(namespace):
53+
return call_annotate_function(annotate, format=Format.FORWARDREF)
54+
return namespace.get("__annotations__", {})
55+
56+
57+
if find_spec("pydantic") and find_spec("pydantic.v1"):
58+
from pydantic.v1.main import ModelMetaclass
59+
60+
class ModelMetaclassLazyAnnotations(ModelMetaclass):
61+
"""Compatibility metaclass to resolve python3.14 style lazy annotations."""
62+
63+
def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs):
64+
"""Resolve python3.14 style lazy annotations before passing off to pydantic v1.
65+
66+
Args:
67+
name: The class name.
68+
bases: The base classes.
69+
namespace: The class namespace.
70+
**kwargs: Additional keyword arguments.
71+
72+
Returns:
73+
The created class.
74+
"""
75+
namespace["__annotations__"] = annotations_from_namespace(namespace)
76+
return super().__new__(mcs, name, bases, namespace, **kwargs)
77+
else:
78+
ModelMetaclassLazyAnnotations = type # type: ignore[assignment]
79+
80+
3381
def sqlmodel_field_has_primary_key(field_info: "FieldInfo") -> bool:
3482
"""Determines if a field is a primary.
3583

0 commit comments

Comments
 (0)