Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/integration_app_harness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
strategy:
matrix:
state_manager: ["redis", "memory"]
python-version: ["3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
split_index: [1, 2]
fail-fast: false
runs-on: ubuntu-22.04
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env
with:
python-version: 3.13
python-version: 3.14
run-uv-sync: true

- name: Create app directory
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/performance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
python-version: "3.14"

- name: Install dependencies
run: uv sync --all-extras --dev
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
runs-on: ${{ matrix.os }}

# Service containers to run with `runner-job`
Expand Down Expand Up @@ -77,7 +77,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ requires-python = ">=3.10,<4.0"
dependencies = [
"alembic >=1.15.2,<2.0",
"click >=8.2",
"granian[reload] >=2.4.0",
"granian[reload] >=2.5.5",
"httpx >=0.23.3,<1.0",
"packaging >=24.2,<26",
"platformdirs >=4.3.7,<5.0",
Expand All @@ -33,7 +33,7 @@ dependencies = [
"redis >=5.2.1,<7.0",
"reflex-hosting-cli >=0.1.55",
"rich >=13,<15",
"sqlmodel >=0.0.24,<0.1",
"sqlmodel >=0.0.27,<0.1",
"starlette >=0.47.0",
"typing_extensions >=4.13.0",
"wrapt >=1.17.0,<2.0",
Expand All @@ -47,6 +47,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]

[project.optional-dependencies]
Expand Down
6 changes: 4 additions & 2 deletions reflex/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
if find_spec("pydantic") and find_spec("pydantic.v1"):
from pydantic.v1 import BaseModel

class Base(BaseModel):
from reflex.utils.compat import ModelMetaclassLazyAnnotations

class Base(BaseModel, metaclass=ModelMetaclassLazyAnnotations):
"""The base class subclassed by all Reflex classes.

This class wraps Pydantic and provides common methods such as
Expand Down Expand Up @@ -34,7 +36,7 @@ def __init__(self, *args, **kwargs):
console.deprecate(
feature_name="rx.Base",
reason="You can subclass from `pydantic.BaseModel` directly instead or use dataclasses if possible.",
deprecation_version="0.8.2",
deprecation_version="0.8.15",
removal_version="0.9.0",
)
super().__init__(*args, **kwargs)
Expand Down
4 changes: 3 additions & 1 deletion reflex/components/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Annotated, Any, Generic, TypeVar, get_origin

from reflex.utils import types
from reflex.utils.compat import annotations_from_namespace

FIELD_TYPE = TypeVar("FIELD_TYPE")

Expand Down Expand Up @@ -117,7 +118,8 @@ def _resolve_annotations(
cls, namespace: dict[str, Any], name: str
) -> dict[str, Any]:
return types.resolve_annotations(
namespace.get("__annotations__", {}), namespace["__module__"]
annotations_from_namespace(namespace),
namespace["__module__"],
)

@classmethod
Expand Down
2 changes: 1 addition & 1 deletion reflex/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ def __pydantic_init_subclass__(cls):
reason=(
"Register sqlmodel.SQLModel classes with `@rx.ModelRegistry.register`"
),
deprecation_version="0.8.0",
deprecation_version="0.8.15",
removal_version="0.9.0",
)
super().__pydantic_init_subclass__()
Expand Down
10 changes: 5 additions & 5 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def __call__(self, *args: Any) -> EventSpec:
msg = f"Variable `{args[0]}` cannot be set on `{self.state_cls.get_full_name()}`"
raise AttributeError(msg)

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

Expand Down Expand Up @@ -287,7 +287,7 @@ async def _resolve_delta(delta: Delta) -> Delta:
tasks = {}
for state_name, state_delta in delta.items():
for var_name, value in state_delta.items():
if asyncio.iscoroutine(value):
if inspect.iscoroutine(value):
tasks[state_name, var_name] = asyncio.create_task(
value,
name=f"reflex_resolve_delta|{state_name}|{var_name}|{time.time()}",
Expand Down Expand Up @@ -852,7 +852,7 @@ def _check_overridden_basevars(cls):
ComputedVarShadowsBaseVarsError: When a computed var shadows a base var.
"""
for name, computed_var_ in cls._get_computed_vars():
if name in cls.__annotations__:
if name in get_type_hints(cls):
msg = f"The computed var name `{computed_var_._js_expr}` shadows a base var in {cls.__module__}.{cls.__name__}; use a different name instead"
raise ComputedVarShadowsBaseVarsError(msg)

Expand Down Expand Up @@ -1733,7 +1733,7 @@ def _is_valid_type(events: Any) -> bool:
except TypeError:
pass

coroutines = [e for e in events if asyncio.iscoroutine(e)]
coroutines = [e for e in events if inspect.iscoroutine(e)]

for coroutine in coroutines:
coroutine_name = coroutine.__qualname__
Expand Down Expand Up @@ -1895,7 +1895,7 @@ async def _process_event(
# Wrap the function in a try/except block.
try:
# Handle async functions.
if asyncio.iscoroutinefunction(fn.func):
if inspect.iscoroutinefunction(fn.func):
events = await fn(**payload)

# Handle regular functions.
Expand Down
50 changes: 49 additions & 1 deletion reflex/utils/compat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Compatibility hacks and helpers."""

from typing import TYPE_CHECKING
import sys
from collections.abc import Mapping
from importlib.util import find_spec
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from pydantic.fields import FieldInfo
Expand Down Expand Up @@ -30,6 +33,51 @@ async def windows_hot_reload_lifespan_hack():
pass


def annotations_from_namespace(namespace: Mapping[str, Any]) -> dict[str, Any]:
"""Get the annotations from a class namespace.

Args:
namespace: The class namespace.

Returns:
The (forward-ref) annotations from the class namespace.
"""
if sys.version_info >= (3, 14) and "__annotations__" not in namespace:
from annotationlib import (
Format,
call_annotate_function,
get_annotate_from_class_namespace,
)

if annotate := get_annotate_from_class_namespace(namespace):
return call_annotate_function(annotate, format=Format.FORWARDREF)
return namespace.get("__annotations__", {})


if find_spec("pydantic") and find_spec("pydantic.v1"):
from pydantic.v1.main import ModelMetaclass

class ModelMetaclassLazyAnnotations(ModelMetaclass):
"""Compatibility metaclass to resolve python3.14 style lazy annotations."""

def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs):
"""Resolve python3.14 style lazy annotations before passing off to pydantic v1.

Args:
name: The class name.
bases: The base classes.
namespace: The class namespace.
**kwargs: Additional keyword arguments.

Returns:
The created class.
"""
namespace["__annotations__"] = annotations_from_namespace(namespace)
return super().__new__(mcs, name, bases, namespace, **kwargs)
else:
ModelMetaclassLazyAnnotations = type # type: ignore[assignment]


def sqlmodel_field_has_primary_key(field_info: "FieldInfo") -> bool:
"""Determines if a field is a primary.

Expand Down
3 changes: 2 additions & 1 deletion reflex/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import contextlib
import inspect
import sys
import threading
from collections.abc import Callable
Expand Down Expand Up @@ -62,7 +63,7 @@ async def run_in_thread(func: Callable) -> Any:
Returns:
Any: The return value of the function.
"""
if asyncio.coroutines.iscoroutinefunction(func):
if inspect.iscoroutinefunction(func):
msg = "func must be a non-async function"
raise ValueError(msg)
return await asyncio.get_event_loop().run_in_executor(None, func)
Expand Down
3 changes: 1 addition & 2 deletions reflex/utils/monitoring.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""PyLeak integration for monitoring event loop blocking and resource leaks in Reflex applications."""

import asyncio
import contextlib
import functools
import inspect
Expand Down Expand Up @@ -154,7 +153,7 @@ async def async_gen_wrapper(*args, **kwargs):

return async_gen_wrapper

if asyncio.iscoroutinefunction(func):
if inspect.iscoroutinefunction(func):

@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
Expand Down
3 changes: 2 additions & 1 deletion reflex/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from reflex.constants.compiler import Hooks
from reflex.constants.state import FIELD_MARKER
from reflex.utils import console, exceptions, imports, serializers, types
from reflex.utils.compat import annotations_from_namespace
from reflex.utils.decorator import once
from reflex.utils.exceptions import (
ComputedVarSignatureError,
Expand Down Expand Up @@ -3478,7 +3479,7 @@ def __new__(
inherited_fields: dict[str, Field] = {}
own_fields: dict[str, Field] = {}
resolved_annotations = types.resolve_annotations(
namespace.get("__annotations__", {}), namespace["__module__"]
annotations_from_namespace(namespace), namespace["__module__"]
)

for base in bases[::-1]:
Expand Down
4 changes: 2 additions & 2 deletions reflex/vars/dep_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def handle_getting_state(self, instruction: dis.Instruction) -> None:
"""
from reflex.state import BaseState

if instruction.opname == "LOAD_FAST":
if instruction.opname in ("LOAD_FAST", "LOAD_FAST_BORROW"):
msg = f"Dependency detection cannot identify get_state class from local var {instruction.argval}."
raise VarValueError(msg)
if isinstance(self.func, CodeType):
Expand Down Expand Up @@ -305,7 +305,7 @@ def _populate_dependencies(self) -> None:
elif self.scan_status == ScanStatus.GETTING_VAR:
self.handle_getting_var(instruction)
elif (
instruction.opname in ("LOAD_FAST", "LOAD_DEREF")
instruction.opname in ("LOAD_FAST", "LOAD_DEREF", "LOAD_FAST_BORROW")
and instruction.argval in self.tracked_locals
):
# bytecode loaded the class instance to the top of stack, next load instruction
Expand Down
10 changes: 5 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.