removing deprecated features for 0.7.0 and removing py3.9 support (#4586)

* remove deprecated features and support for py3.9

* remove other deprecated stuff

* update lock file

* fix units tests

* relock poetry

* fix _replace for computed_var

* fix some merge typo

* fix typing of deploy args

* fix benchmarks.yml versions

* console.error instead of raising Exception

* fix tests

* ignore lambdas when resolving annotations

* simplify redirect logic in event.py

* more fixes

* fix unit tests again

* give back default annotations for lambdas

* fix signature check for on_submit

* remove useless stuff

* update pyi

* readd the getattr

* raise if log_level is wrong type

* silly goose, loglevel is a subclass of str

* i don't believe this code

* add guard

---------

Co-authored-by: Khaleel Al-Adhami <khaleel.aladhami@gmail.com>
This commit is contained in:
Thomas Brandého 2025-01-22 11:54:37 -08:00 committed by GitHub
parent db13fe65b8
commit 4c2b2ed1c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 987 additions and 1329 deletions

View File

@ -81,24 +81,18 @@ jobs:
matrix:
# Show OS combos first in GUI
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8"]
python-version: ['3.10.16', '3.11.11', '3.12.8']
exclude:
- os: windows-latest
python-version: "3.10.16"
- os: windows-latest
python-version: "3.9.21"
python-version: '3.10.16'
# keep only one python version for MacOS
- os: macos-latest
python-version: "3.9.21"
- os: macos-latest
python-version: "3.10.16"
python-version: '3.10.16'
- os: macos-latest
python-version: "3.11.11"
include:
- os: windows-latest
python-version: "3.10.11"
- os: windows-latest
python-version: "3.9.13"
python-version: '3.10.11'
runs-on: ${{ matrix.os }}
steps:

View File

@ -16,7 +16,7 @@ jobs:
- uses: ./.github/actions/setup_build_env
with:
python-version: "3.9.21"
python-version: '3.10'
run-poetry-install: true
create-venv-at-path: .venv

View File

@ -43,22 +43,17 @@ jobs:
matrix:
# Show OS combos first in GUI
os: [ubuntu-latest, windows-latest]
python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8", "3.13.1"]
# Windows is a bit behind on Python version availability in Github
python-version: ['3.10.16', '3.11.11', '3.12.8', '3.13.1']
exclude:
- os: windows-latest
python-version: "3.11.11"
- os: windows-latest
python-version: "3.10.16"
- os: windows-latest
python-version: "3.9.21"
python-version: '3.10.16'
include:
- os: windows-latest
python-version: "3.11.9"
- os: windows-latest
python-version: "3.10.11"
- os: windows-latest
python-version: "3.9.13"
python-version: '3.10.11'
runs-on: ${{ matrix.os }}
steps:

View File

@ -28,22 +28,18 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8", "3.13.1"]
python-version: ["3.10.16", "3.11.11", "3.12.8", "3.13.1"]
# Windows is a bit behind on Python version availability in Github
exclude:
- os: windows-latest
python-version: "3.11.11"
- os: windows-latest
python-version: "3.10.16"
- os: windows-latest
python-version: "3.9.21"
include:
- os: windows-latest
python-version: "3.11.9"
- os: windows-latest
python-version: "3.10.11"
- os: windows-latest
python-version: "3.9.13"
runs-on: ${{ matrix.os }}
# Service containers to run with `runner-job`
@ -92,8 +88,8 @@ jobs:
strategy:
fail-fast: false
matrix:
# Note: py39, py310, py311 versions chosen due to available arm64 darwin builds.
python-version: ["3.9.13", "3.10.11", "3.11.9", "3.12.8", "3.13.1"]
# Note: py310, py311 versions chosen due to available arm64 darwin builds.
python-version: ["3.10.11", "3.11.9", "3.12.8", "3.13.1"]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

View File

@ -28,7 +28,7 @@ repos:
entry: python3 scripts/make_pyi.py
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.313
rev: v1.1.334
hooks:
- id: pyright
args: [reflex, tests]

View File

@ -8,7 +8,7 @@ Here is a quick guide on how to run Reflex repo locally so you can start contrib
**Prerequisites:**
- Python >= 3.9
- Python >= 3.10
- Poetry version >= 1.4.0 and add it to your path (see [Poetry Docs](https://python-poetry.org/docs/#installation) for more info).
**1. Fork this repository:**
@ -87,7 +87,7 @@ poetry run ruff format .
```
Consider installing git pre-commit hooks so Ruff, Pyright, Darglint and `make_pyi` will run automatically before each commit.
Note that pre-commit will only be installed when you use a Python version >= 3.9.
Note that pre-commit will only be installed when you use a Python version >= 3.10.
``` bash
pre-commit install

View File

@ -34,7 +34,7 @@ See our [architecture page](https://reflex.dev/blog/2024-03-21-reflex-architectu
## ⚙️ Installation
Open a terminal and run (Requires Python 3.9+):
Open a terminal and run (Requires Python 3.10+):
```bash
pip install reflex

View File

@ -34,7 +34,7 @@ Auf unserer [Architektur-Seite](https://reflex.dev/blog/2024-03-21-reflex-archit
## ⚙️ Installation
Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.9+):
Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.10+):
```bash
pip install reflex

View File

@ -35,7 +35,7 @@ Consulta nuestra [página de arquitectura](https://reflex.dev/blog/2024-03-21-re
## ⚙️ Instalación
Abra un terminal y ejecute (Requiere Python 3.9+):
Abra un terminal y ejecute (Requiere Python 3.10+):
```bash
pip install reflex

View File

@ -35,7 +35,7 @@ Reflex के अंदर के कामकाज को जानने क
## ⚙️ इंस्टॉलेशन (Installation)
एक टर्मिनल खोलें और चलाएं (Python 3.9+ की आवश्यकता है):
एक टर्मिनल खोलें और चलाएं (Python 3.10+ की आवश्यकता है):
```bash
pip install reflex

View File

@ -22,7 +22,7 @@
## ⚙️ Installazione
Apri un terminale ed esegui (Richiede Python 3.9+):
Apri un terminale ed esegui (Richiede Python 3.10+):
```bash
pip install reflex

View File

@ -37,7 +37,7 @@ Reflex がどのように動作しているかを知るには、[アーキテク
## ⚙️ インストール
ターミナルを開いて以下のコマンドを実行してください。Python 3.9 以上が必要です。):
ターミナルを開いて以下のコマンドを実行してください。Python 3.10 以上が必要です。):
```bash
pip install reflex

View File

@ -20,7 +20,7 @@
---
## ⚙️ 설치
터미널을 열고 실행하세요. (Python 3.9+ 필요):
터미널을 열고 실행하세요. (Python 3.10+ 필요):
```bash
pip install reflex

View File

@ -34,7 +34,7 @@
## ⚙️ Installation - نصب و راه اندازی
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.9+):
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.10+):
```bash
pip install reflex

View File

@ -21,7 +21,7 @@
---
## ⚙️ Instalação
Abra um terminal e execute (Requer Python 3.9+):
Abra um terminal e execute (Requer Python 3.10+):
```bash
pip install reflex

View File

@ -24,7 +24,7 @@
## ⚙️ Kurulum
Bir terminal açın ve çalıştırın (Python 3.9+ gerekir):
Bir terminal açın ve çalıştırın (Python 3.10+ gerekir):
```bash
pip install reflex

View File

@ -34,7 +34,7 @@ Các tính năng chính:
## ⚙️ Cài đặt
Mở cửa sổ lệnh và chạy (Yêu cầu Python phiên bản 3.9+):
Mở cửa sổ lệnh và chạy (Yêu cầu Python phiên bản 3.10+):
```bash
pip install reflex

View File

@ -34,7 +34,7 @@ Reflex 是一个使用纯Python构建全栈web应用的库。
## ⚙️ 安装
打开一个终端并且运行(要求Python3.9+):
打开一个终端并且运行(要求Python3.10+):
```bash
pip install reflex

View File

@ -36,7 +36,7 @@ Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫
## ⚙️ 安裝
開啟一個終端機並且執行 (需要 Python 3.9+):
開啟一個終端機並且執行 (需要 Python 3.10+):
```bash
pip install reflex

1437
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ keywords = ["web", "framework"]
classifiers = ["Development Status :: 4 - Beta"]
[tool.poetry.dependencies]
python = "^3.9"
python = "^3.10"
fastapi = ">=0.96.0,!=0.111.0,!=0.111.1"
gunicorn = ">=20.1.0,<24.0"
jinja2 = ">=3.1.2,<4.0"
@ -50,7 +50,6 @@ httpx = ">=0.25.1,<1.0"
twine = ">=4.0.0,<7.0"
tomlkit = ">=0.12.4,<1.0"
lazy_loader = ">=0.4"
reflex-chakra = ">=0.6.0"
typing_extensions = ">=4.6.0"
[tool.poetry.group.dev.dependencies]
@ -102,5 +101,5 @@ asyncio_default_fixture_loop_scope = "function"
asyncio_mode = "auto"
[tool.codespell]
skip = "docs/*,*.html,examples/*, *.pyi"
skip = "docs/*,*.html,examples/*, *.pyi, poetry.lock"
ignore-words-list = "te, TreeE"

View File

@ -8,7 +8,7 @@ version = "0.0.1"
description = "Reflex custom component {{ module_name }}"
readme = "README.md"
license = { text = "Apache-2.0" }
requires-python = ">=3.9"
requires-python = ">=3.10"
authors = [{ name = "", email = "YOUREMAIL@domain.com" }]
keywords = ["reflex","reflex-custom-components"]

View File

@ -303,7 +303,6 @@ _MAPPING: dict = {
"event": [
"EventChain",
"EventHandler",
"background",
"call_script",
"call_function",
"run_script",
@ -367,19 +366,4 @@ getattr, __dir__, __all__ = lazy_loader.attach(
def __getattr__(name):
if name == "chakra":
from reflex.utils import console
console.deprecate(
"rx.chakra",
reason="and moved to a separate package. "
"To continue using Chakra UI components, install the `reflex-chakra` package via `pip install "
"reflex-chakra`.",
deprecation_version="0.6.0",
removal_version="0.7.0",
dedupe=True,
)
import reflex_chakra as rc
return rc
return getattr(name)

View File

@ -156,7 +156,6 @@ from .constants import Env as Env
from .constants.colors import Color as Color
from .event import EventChain as EventChain
from .event import EventHandler as EventHandler
from .event import background as background
from .event import call_function as call_function
from .event import call_script as call_script
from .event import clear_local_storage as clear_local_storage

View File

@ -23,8 +23,6 @@ from typing import (
Union,
)
from typing_extensions import deprecated
import reflex.state
from reflex.base import Base
from reflex.compiler.templates import STATEFUL_COMPONENT
@ -47,11 +45,10 @@ from reflex.event import (
EventChain,
EventHandler,
EventSpec,
EventVar,
no_args_event_spec,
)
from reflex.style import Style, format_as_emotion
from reflex.utils import console, format, imports, types
from reflex.utils import format, imports, types
from reflex.utils.imports import (
ImmutableParsedImportDict,
ImportDict,
@ -547,41 +544,6 @@ class Component(BaseComponent, ABC):
# Construct the component.
super().__init__(*args, **kwargs)
@deprecated("Use rx.EventChain.create instead.")
def _create_event_chain(
self,
args_spec: types.ArgsSpec | Sequence[types.ArgsSpec],
value: Union[
Var,
EventHandler,
EventSpec,
List[Union[EventHandler, EventSpec, EventVar]],
Callable,
],
key: Optional[str] = None,
) -> Union[EventChain, Var]:
"""Create an event chain from a variety of input types.
Args:
args_spec: The args_spec of the event trigger being bound.
value: The value to create the event chain from.
key: The key of the event trigger being bound.
Returns:
The event chain.
"""
console.deprecate(
"Component._create_event_chain",
"Use rx.EventChain.create instead.",
deprecation_version="0.6.8",
removal_version="0.7.0",
)
return EventChain.create(
value=value, # type: ignore
args_spec=args_spec,
key=key,
)
def get_event_triggers(
self,
) -> Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]]:

View File

@ -14,7 +14,7 @@ from reflex.components.radix.themes.layout.box import Box
from reflex.constants.colors import Color
from reflex.event import set_clipboard
from reflex.style import Style
from reflex.utils import console, format
from reflex.utils import format
from reflex.utils.imports import ImportVar
from reflex.vars.base import LiteralVar, Var, VarData
@ -438,6 +438,8 @@ class CodeBlock(Component, MarkdownComponentMap):
can_copy = props.pop("can_copy", False)
copy_button = props.pop("copy_button", None)
# react-syntax-highlighter doesn't have an explicit "light" or "dark" theme so we use one-light and one-dark
# themes respectively to ensure code compatibility.
if "theme" not in props:
# Default color scheme responds to global color mode.
props["theme"] = color_mode_cond(
@ -445,17 +447,6 @@ class CodeBlock(Component, MarkdownComponentMap):
dark=Theme.one_dark,
)
# react-syntax-highlighter doesn't have an explicit "light" or "dark" theme so we use one-light and one-dark
# themes respectively to ensure code compatibility.
if "theme" in props and not isinstance(props["theme"], Var):
props["theme"] = getattr(Theme, format.to_snake_case(props["theme"])) # type: ignore
console.deprecate(
feature_name="theme prop as string",
reason="Use code_block.themes instead.",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
if can_copy:
code = children[0]
copy_button = ( # type: ignore

View File

@ -102,7 +102,7 @@ class Fieldset(Element):
name: Var[Union[str, int, bool]]
def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]:
def on_submit_event_spec() -> Tuple[Var[dict[str, Any]]]:
"""Event handler spec for the on_submit event.
Returns:
@ -111,7 +111,7 @@ def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]:
return (FORM_DATA,)
def on_submit_string_event_spec() -> Tuple[Var[Dict[str, str]]]:
def on_submit_string_event_spec() -> Tuple[Var[dict[str, str]]]:
"""Event handler spec for the on_submit event.
Returns:

View File

@ -270,8 +270,8 @@ class Fieldset(Element):
"""
...
def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]: ...
def on_submit_string_event_spec() -> Tuple[Var[Dict[str, str]]]: ...
def on_submit_event_spec() -> Tuple[Var[dict[str, Any]]]: ...
def on_submit_string_event_spec() -> Tuple[Var[dict[str, str]]]: ...
class Form(BaseHTML):
@overload
@ -341,10 +341,10 @@ class Form(BaseHTML):
on_submit: Optional[
Union[
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE]
],
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE]
],
]
] = None,

View File

@ -132,10 +132,10 @@ class FormRoot(FormComponent, HTMLForm):
on_submit: Optional[
Union[
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE]
],
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE]
],
]
] = None,
@ -608,10 +608,10 @@ class Form(FormRoot):
on_submit: Optional[
Union[
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE]
],
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE]
],
]
] = None,
@ -741,10 +741,10 @@ class FormNamespace(ComponentNamespace):
on_submit: Optional[
Union[
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE]
],
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE]
],
]
] = None,

View File

@ -25,7 +25,6 @@ from typing import (
overload,
)
import typing_extensions
from typing_extensions import (
Concatenate,
ParamSpec,
@ -40,7 +39,11 @@ from typing_extensions import (
from reflex import constants
from reflex.constants.state import FRONTEND_EVENT_STATE
from reflex.utils import console, format
from reflex.utils.exceptions import EventFnArgMismatch, EventHandlerArgTypeMismatch
from reflex.utils.exceptions import (
EventFnArgMismatchError,
EventHandlerArgTypeMismatchError,
MissingAnnotationError,
)
from reflex.utils.types import ArgsSpec, GenericType, typehint_issubclass
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
@ -96,32 +99,6 @@ _EVENT_FIELDS: set[str] = {f.name for f in dataclasses.fields(Event)}
BACKGROUND_TASK_MARKER = "_reflex_background_task"
def background(fn, *, __internal_reflex_call: bool = False):
"""Decorator to mark event handler as running in the background.
Args:
fn: The function to decorate.
Returns:
The same function, but with a marker set.
Raises:
TypeError: If the function is not a coroutine function or async generator.
"""
if not __internal_reflex_call:
console.deprecate(
"background-decorator",
"Use `rx.event(background=True)` instead.",
"0.6.5",
"0.7.0",
)
if not inspect.iscoroutinefunction(fn) and not inspect.isasyncgenfunction(fn):
raise TypeError("Background task must be async function or generator.")
setattr(fn, BACKGROUND_TASK_MARKER, True)
return fn
@dataclasses.dataclass(
init=True,
frozen=True,
@ -813,29 +790,10 @@ def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec:
)
@overload
def redirect(
path: str | Var[str],
is_external: Optional[bool] = None,
is_external: bool = False,
replace: bool = False,
) -> EventSpec: ...
@overload
@typing_extensions.deprecated("`external` is deprecated use `is_external` instead")
def redirect(
path: str | Var[str],
is_external: Optional[bool] = None,
replace: bool = False,
external: Optional[bool] = None,
) -> EventSpec: ...
def redirect(
path: str | Var[str],
is_external: Optional[bool] = None,
replace: bool = False,
external: Optional[bool] = None,
) -> EventSpec:
"""Redirect to a new path.
@ -843,26 +801,10 @@ def redirect(
path: The path to redirect to.
is_external: Whether to open in new tab or not.
replace: If True, the current page will not create a new history entry.
external(Deprecated): Whether to open in new tab or not.
Returns:
An event to redirect to the path.
"""
if external is not None:
console.deprecate(
"The `external` prop in `rx.redirect`",
"use `is_external` instead.",
"0.6.6",
"0.7.0",
)
# is_external should take precedence over external.
is_external = (
(False if external is None else external)
if is_external is None
else is_external
)
return server_side(
"_redirect",
get_fn_signature(redirect),
@ -1279,11 +1221,14 @@ def call_event_handler(
event_spec: The lambda that define the argument(s) to pass to the event handler.
key: The key to pass to the event handler.
Raises:
EventHandlerArgTypeMismatchError: If the event handler arguments do not match the event spec. #noqa: DAR402
TypeError: If the event handler arguments are invalid.
Returns:
The event spec from calling the event handler.
# noqa: DAR401 failure
#noqa: DAR401
"""
event_spec_args = parse_args_spec(event_spec) # type: ignore
@ -1320,10 +1265,15 @@ def call_event_handler(
),
)
)
type_match_found: dict[str, bool] = {}
delayed_exceptions: list[EventHandlerArgTypeMismatchError] = []
try:
type_hints_of_provided_callback = get_type_hints(event_callback.fn)
except NameError:
type_hints_of_provided_callback = {}
if event_spec_return_types:
failures = []
event_callback_spec = inspect.getfullargspec(event_callback.fn)
for event_spec_index, event_spec_return_type in enumerate(
@ -1335,43 +1285,35 @@ def call_event_handler(
arg if get_origin(arg) is not Var else get_args(arg)[0] for arg in args
]
try:
type_hints_of_provided_callback = get_type_hints(event_callback.fn)
except NameError:
type_hints_of_provided_callback = {}
failed_type_check = False
# check that args of event handler are matching the spec if type hints are provided
for i, arg in enumerate(event_callback_spec.args[1:]):
if arg not in type_hints_of_provided_callback:
continue
type_match_found.setdefault(arg, False)
try:
compare_result = typehint_issubclass(
args_types_without_vars[i], type_hints_of_provided_callback[arg]
)
except TypeError:
# TODO: In 0.7.0, remove this block and raise the exception
# raise TypeError(
# f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_handler.fn.__qualname__} provided for {key}." # noqa: ERA001
# ) from e
console.warn(
except TypeError as te:
raise TypeError(
f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_callback.fn.__qualname__} provided for {key}."
)
compare_result = False
) from te
if compare_result:
type_match_found[arg] = True
continue
else:
failure = EventHandlerArgTypeMismatch(
f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {type_hints_of_provided_callback[arg]} as annotated in {event_callback.fn.__qualname__} instead."
type_match_found[arg] = False
delayed_exceptions.append(
EventHandlerArgTypeMismatchError(
f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {type_hints_of_provided_callback[arg]} as annotated in {event_callback.fn.__qualname__} instead."
)
)
failures.append(failure)
failed_type_check = True
break
if not failed_type_check:
if all(type_match_found.values()):
delayed_exceptions.clear()
if event_spec_index:
args = get_args(event_spec_return_types[0])
@ -1393,15 +1335,10 @@ def call_event_handler(
f"Event handler {key} expects ({expect_string}) -> () but got ({given_string}) -> () as annotated in {event_callback.fn.__qualname__} instead. "
f"This may lead to unexpected behavior but is intentionally ignored for {key}."
)
return event_callback(*event_spec_args)
break
if failures:
console.deprecate(
"Mismatched event handler argument types",
"\n".join([str(f) for f in failures]),
"0.6.5",
"0.7.0",
)
if delayed_exceptions:
raise delayed_exceptions[0]
return event_callback(*event_spec_args) # type: ignore
@ -1420,26 +1357,26 @@ def unwrap_var_annotation(annotation: GenericType):
return annotation
def resolve_annotation(annotations: dict[str, Any], arg_name: str):
def resolve_annotation(annotations: dict[str, Any], arg_name: str, spec: ArgsSpec):
"""Resolve the annotation for the given argument name.
Args:
annotations: The annotations.
arg_name: The argument name.
spec: The specs which the annotations come from.
Raises:
MissingAnnotationError: If the annotation is missing for non-lambda methods.
Returns:
The resolved annotation.
"""
annotation = annotations.get(arg_name)
if annotation is None:
console.deprecate(
feature_name="Unannotated event handler arguments",
reason="Provide type annotations for event handler arguments.",
deprecation_version="0.6.3",
removal_version="0.7.0",
)
# Allow arbitrary attribute access two levels deep until removed.
return Dict[str, dict]
if not isinstance(spec, types.LambdaType):
raise MissingAnnotationError(var_name=arg_name)
else:
return dict[str, dict]
return annotation
@ -1461,7 +1398,13 @@ def parse_args_spec(arg_spec: ArgsSpec | Sequence[ArgsSpec]):
arg_spec(
*[
Var(f"_{l_arg}").to(
unwrap_var_annotation(resolve_annotation(annotations, l_arg))
unwrap_var_annotation(
resolve_annotation(
annotations,
l_arg,
spec=arg_spec,
)
)
)
for l_arg in spec.args
]
@ -1477,7 +1420,7 @@ def check_fn_match_arg_spec(
func_name: str | None = None,
):
"""Ensures that the function signature matches the passed argument specification
or raises an EventFnArgMismatch if they do not.
or raises an EventFnArgMismatchError if they do not.
Args:
user_func: The function to be validated.
@ -1487,7 +1430,7 @@ def check_fn_match_arg_spec(
func_name: The name of the function to be validated.
Raises:
EventFnArgMismatch: Raised if the number of mandatory arguments do not match
EventFnArgMismatchError: Raised if the number of mandatory arguments do not match
"""
user_args = inspect.getfullargspec(user_func).args
# Drop the first argument if it's a bound method
@ -1503,7 +1446,7 @@ def check_fn_match_arg_spec(
number_of_event_args = len(parsed_event_args)
if number_of_user_args - number_of_user_default_args > number_of_event_args:
raise EventFnArgMismatch(
raise EventFnArgMismatchError(
f"Event {key} only provides {number_of_event_args} arguments, but "
f"{func_name or user_func} requires at least {number_of_user_args - number_of_user_default_args} "
"arguments to be passed to the event handler.\n"
@ -1812,8 +1755,6 @@ V3 = TypeVar("V3")
V4 = TypeVar("V4")
V5 = TypeVar("V5")
background_event_decorator = background
class EventCallback(Generic[P, T]):
"""A descriptor that wraps a function to be used as an event."""
@ -1986,6 +1927,9 @@ class EventNamespace(types.SimpleNamespace):
func: The function to wrap.
background: Whether the event should be run in the background. Defaults to False.
Raises:
TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402
Returns:
The wrapped function.
"""
@ -1994,7 +1938,13 @@ class EventNamespace(types.SimpleNamespace):
func: Callable[Concatenate[BASE_STATE, P], T],
) -> EventCallback[P, T]:
if background is True:
return background_event_decorator(func, __internal_reflex_call=True) # type: ignore
if not inspect.iscoroutinefunction(
func
) and not inspect.isasyncgenfunction(func):
raise TypeError(
"Background task must be async function or generator."
)
setattr(func, BACKGROUND_TASK_MARKER, True)
return func # type: ignore
if func is not None:

View File

@ -9,7 +9,6 @@ from reflex.components.sonner.toast import toast as toast
from ..utils.console import warn
from . import hooks as hooks
from .assets import asset as asset
from .client_state import ClientStateVar as ClientStateVar
from .layout import layout as layout
from .misc import run_in_thread as run_in_thread
@ -62,7 +61,6 @@ class ExperimentalNamespace(SimpleNamespace):
_x = ExperimentalNamespace(
asset=asset,
client_state=ClientStateVar.create,
hooks=hooks,
layout=layout,

View File

@ -1,37 +0,0 @@
"""Helper functions for adding assets to the app."""
from typing import Optional
from reflex import assets
from reflex.utils import console
def asset(relative_filename: str, subfolder: Optional[str] = None) -> str:
"""DEPRECATED: use `rx.asset` with `shared=True` instead.
Add an asset to the app.
Place the file next to your including python file.
Copies the file to the app's external assets directory.
Example:
```python
rx.script(src=rx._x.asset("my_custom_javascript.js"))
rx.image(src=rx._x.asset("test_image.png","subfolder"))
```
Args:
relative_filename: The relative filename of the asset.
subfolder: The directory to place the asset in.
Returns:
The relative URL to the copied asset.
"""
console.deprecate(
feature_name="rx._x.asset",
reason="Use `rx.asset` with `shared=True` instead of `rx._x.asset`.",
deprecation_version="0.6.6",
removal_version="0.7.0",
)
return assets.asset(
relative_filename, shared=True, subfolder=subfolder, _stack_level=2
)

View File

@ -505,6 +505,7 @@ def deploy(
),
):
"""Deploy the app to the Reflex hosting service."""
from reflex_cli.constants.base import LogLevel as HostingLogLevel
from reflex_cli.utils import dependency
from reflex_cli.v2 import cli as hosting_cli
@ -516,6 +517,21 @@ def deploy(
# Set the log level.
console.set_log_level(loglevel)
def convert_reflex_loglevel_to_reflex_cli_loglevel(
loglevel: constants.LogLevel,
) -> HostingLogLevel:
if loglevel == constants.LogLevel.DEBUG:
return HostingLogLevel.DEBUG
if loglevel == constants.LogLevel.INFO:
return HostingLogLevel.INFO
if loglevel == constants.LogLevel.WARNING:
return HostingLogLevel.WARNING
if loglevel == constants.LogLevel.ERROR:
return HostingLogLevel.ERROR
if loglevel == constants.LogLevel.CRITICAL:
return HostingLogLevel.CRITICAL
return HostingLogLevel.INFO
# Only check requirements if interactive.
# There is user interaction for requirements update.
if interactive:
@ -525,9 +541,7 @@ def deploy(
if prerequisites.needs_reinit(frontend=True):
_init(name=config.app_name, loglevel=loglevel)
prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
extra: dict[str, str] = (
{"config_path": config_path} if config_path is not None else {}
)
hosting_cli.deploy(
app_name=app_name,
app_id=app_id,
@ -551,12 +565,11 @@ def deploy(
envfile=envfile,
hostname=hostname,
interactive=interactive,
loglevel=type(loglevel).INFO, # type: ignore
loglevel=convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel),
token=token,
project=project,
config_path=config_path,
project_name=project_name,
**extra,
**({"config_path": config_path} if config_path is not None else {}),
)

View File

@ -1341,12 +1341,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if field.allow_none and not is_optional(field_type):
field_type = Union[field_type, None]
if not _isinstance(value, field_type):
console.deprecate(
"mismatched-type-assignment",
f"Tried to assign value {value} of type {type(value)} to field {type(self).__name__}.{name} of type {field_type}."
" This might lead to unexpected behavior.",
"0.6.5",
"0.7.0",
console.error(
f"Expected field '{type(self).__name__}.{name}' to receive type '{field_type}',"
f" but got '{value}' of type '{type(value)}'."
)
# Set the attribute.
@ -1831,7 +1828,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if (
isinstance(value, dict)
and inspect.isclass(hinted_args)
and not types.is_generic_alias(hinted_args) # py3.9-py3.10
and not types.is_generic_alias(hinted_args) # py3.10
):
if issubclass(hinted_args, Model):
# Remove non-fields from the payload

View File

@ -287,7 +287,9 @@ class Style(dict):
_var = LiteralVar.create(value)
if _var is not None:
# Carry the imports/hooks when setting a Var as a value.
self._var_data = VarData.merge(self._var_data, _var._get_all_var_data())
self._var_data = VarData.merge(
getattr(self, "_var_data", None), _var._get_all_var_data()
)
super().__setitem__(key, value)

View File

@ -51,20 +51,12 @@ def set_log_level(log_level: LogLevel):
log_level: The log level to set.
Raises:
ValueError: If the log level is invalid.
TypeError: If the log level is a string.
"""
if not isinstance(log_level, LogLevel):
deprecate(
feature_name="Passing a string to set_log_level",
reason="use reflex.constants.LogLevel enum instead",
deprecation_version="0.6.6",
removal_version="0.7.0",
raise TypeError(
f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead."
)
try:
log_level = getattr(LogLevel, log_level.upper())
except AttributeError as ae:
raise ValueError(f"Invalid log level: {log_level}") from ae
global _LOG_LEVEL
_LOG_LEVEL = log_level

View File

@ -1,6 +1,6 @@
"""Custom Exceptions."""
from typing import Any, NoReturn
from typing import Any
class ReflexError(Exception):
@ -75,6 +75,30 @@ class VarAttributeError(ReflexError, AttributeError):
"""Custom AttributeError for var related errors."""
class UntypedComputedVarError(ReflexError, TypeError):
"""Custom TypeError for untyped computed var errors."""
def __init__(self, var_name):
"""Initialize the UntypedComputedVarError.
Args:
var_name: The name of the computed var.
"""
super().__init__(f"Computed var '{var_name}' must have a type annotation.")
class MissingAnnotationError(ReflexError, TypeError):
"""Custom TypeError for missing annotations."""
def __init__(self, var_name):
"""Initialize the MissingAnnotationError.
Args:
var_name: The name of the var.
"""
super().__init__(f"Var '{var_name}' must have a type annotation.")
class UploadValueError(ReflexError, ValueError):
"""Custom ValueError for upload related errors."""
@ -111,11 +135,11 @@ class MatchTypeError(ReflexError, TypeError):
"""Raised when the return types of match cases are different."""
class EventHandlerArgTypeMismatch(ReflexError, TypeError):
class EventHandlerArgTypeMismatchError(ReflexError, TypeError):
"""Raised when the annotations of args accepted by an EventHandler differs from the spec of the event trigger."""
class EventFnArgMismatch(ReflexError, TypeError):
class EventFnArgMismatchError(ReflexError, TypeError):
"""Raised when the number of args required by an event handler is more than provided by the event trigger."""
@ -186,29 +210,27 @@ class StateMismatchError(ReflexError, ValueError):
class SystemPackageMissingError(ReflexError):
"""Raised when a system package is missing."""
def __init__(self, package: str):
"""Initialize the SystemPackageMissingError.
Args:
package: The missing package.
"""
from reflex.constants import IS_MACOS
extra = (
f" You can do so by running 'brew install {package}'." if IS_MACOS else ""
)
super().__init__(
f"System package '{package}' is missing."
f" Please install it through your system package manager.{extra}"
)
class EventDeserializationError(ReflexError, ValueError):
"""Raised when an event cannot be deserialized."""
def raise_system_package_missing_error(package: str) -> NoReturn:
"""Raise a SystemPackageMissingError.
Args:
package: The name of the missing system package.
Raises:
SystemPackageMissingError: The raised exception.
"""
from reflex.constants import IS_MACOS
raise SystemPackageMissingError(
f"System package '{package}' is missing."
" Please install it through your system package manager."
+ (f" You can do so by running 'brew install {package}'." if IS_MACOS else "")
)
class InvalidLockWarningThresholdError(ReflexError):
"""Raised when an invalid lock warning threshold is provided."""

View File

@ -240,25 +240,28 @@ def run_backend(
run_uvicorn_backend(host, port, loglevel)
def get_reload_dirs() -> list[str]:
def get_reload_dirs() -> list[Path]:
"""Get the reload directories for the backend.
Returns:
The reload directories for the backend.
"""
config = get_config()
reload_dirs = [config.app_name]
reload_dirs = [Path(config.app_name)]
if config.app_module is not None and config.app_module.__file__:
module_path = Path(config.app_module.__file__).resolve().parent
while module_path.parent.name:
for parent_file in module_path.parent.iterdir():
if parent_file == "__init__.py":
# go up a level to find dir without `__init__.py`
module_path = module_path.parent
break
if any(
sibling_file.name == "__init__.py"
for sibling_file in module_path.parent.iterdir()
):
# go up a level to find dir without `__init__.py`
module_path = module_path.parent
else:
break
reload_dirs.append(str(module_path))
reload_dirs = [module_path]
return reload_dirs
@ -278,7 +281,7 @@ def run_uvicorn_backend(host, port, loglevel: LogLevel):
port=port,
log_level=loglevel.value,
reload=True,
reload_dirs=get_reload_dirs(),
reload_dirs=list(map(str, get_reload_dirs())),
)
@ -526,48 +529,3 @@ def is_prod_mode() -> bool:
"""
current_mode = environment.REFLEX_ENV_MODE.get()
return current_mode == constants.Env.PROD
def is_frontend_only() -> bool:
"""Check if the app is running in frontend-only mode.
Returns:
True if the app is running in frontend-only mode.
"""
console.deprecate(
"is_frontend_only() is deprecated and will be removed in a future release.",
reason="Use `environment.REFLEX_FRONTEND_ONLY.get()` instead.",
deprecation_version="0.6.5",
removal_version="0.7.0",
)
return environment.REFLEX_FRONTEND_ONLY.get()
def is_backend_only() -> bool:
"""Check if the app is running in backend-only mode.
Returns:
True if the app is running in backend-only mode.
"""
console.deprecate(
"is_backend_only() is deprecated and will be removed in a future release.",
reason="Use `environment.REFLEX_BACKEND_ONLY.get()` instead.",
deprecation_version="0.6.5",
removal_version="0.7.0",
)
return environment.REFLEX_BACKEND_ONLY.get()
def should_skip_compile() -> bool:
"""Whether the app should skip compile.
Returns:
True if the app should skip compile.
"""
console.deprecate(
"should_skip_compile() is deprecated and will be removed in a future release.",
reason="Use `environment.REFLEX_SKIP_COMPILE.get()` instead.",
deprecation_version="0.6.5",
removal_version="0.7.0",
)
return environment.REFLEX_SKIP_COMPILE.get()

View File

@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union
from reflex import constants
from reflex.constants.state import FRONTEND_EVENT_STATE
from reflex.utils import exceptions
from reflex.utils.console import deprecate
if TYPE_CHECKING:
from reflex.components.component import ComponentStyle
@ -502,37 +501,6 @@ if TYPE_CHECKING:
from reflex.vars import Var
def format_event_chain(
event_chain: EventChain | Var[EventChain],
event_arg: Var | None = None,
) -> str:
"""DEPRECATED: format an event chain as a javascript invocation.
Use str(rx.Var.create(event_chain)) instead.
Args:
event_chain: The event chain to format.
event_arg: this argument is ignored.
Returns:
Compiled javascript code to queue the given event chain on the frontend.
"""
deprecate(
feature_name="format_event_chain",
reason="Use str(rx.Var.create(event_chain)) instead",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
from reflex.vars import Var
from reflex.vars.function import ArgsFunctionOperation
result = Var.create(event_chain)
if isinstance(result, ArgsFunctionOperation):
result = result._return_expr
return str(result)
def format_queue_events(
events: EventType | None = None,
args_spec: Optional[ArgsSpec] = None,

View File

@ -38,7 +38,7 @@ from reflex.config import Config, environment, get_config
from reflex.utils import console, net, path_ops, processes, redir
from reflex.utils.exceptions import (
GeneratedCodeHasNoFunctionDefs,
raise_system_package_missing_error,
SystemPackageMissingError,
)
from reflex.utils.format import format_library_name
from reflex.utils.registry import _get_npm_registry
@ -86,18 +86,6 @@ def get_web_dir() -> Path:
return environment.REFLEX_WEB_WORKDIR.get()
def _python_version_check():
"""Emit deprecation warning for deprecated python versions."""
# Check for end-of-life python versions.
if sys.version_info < (3, 10):
console.deprecate(
feature_name="Support for Python 3.9 and older",
reason="please upgrade to Python 3.10 or newer",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
def check_latest_package_version(package_name: str):
"""Check if the latest version of the package is installed.
@ -120,8 +108,6 @@ def check_latest_package_version(package_name: str):
console.warn(
f"Your version ({current_version}) of {package_name} is out of date. Upgrade to {latest_version} with 'pip install {package_name} --upgrade'"
)
# Check for deprecated python versions
_python_version_check()
except Exception:
pass
@ -885,7 +871,11 @@ def install_node():
def install_bun():
"""Install bun onto the user's system."""
"""Install bun onto the user's system.
Raises:
SystemPackageMissingError: If "unzip" is missing.
"""
win_supported = is_windows_bun_supported()
one_drive_in_path = windows_check_onedrive_in_path()
if constants.IS_WINDOWS and (not win_supported or one_drive_in_path):
@ -924,7 +914,7 @@ def install_bun():
else:
unzip_path = path_ops.which("unzip")
if unzip_path is None:
raise_system_package_missing_error("unzip")
raise SystemPackageMissingError("unzip")
# Run the bun install script.
download_and_run(

View File

@ -122,7 +122,7 @@ def _prepare_event(event: str, **kwargs) -> dict:
return {}
if UTC is None:
# for python 3.9 & 3.10
# for python 3.10
stamp = datetime.utcnow().isoformat()
else:
# for python 3.11 & 3.12

View File

@ -46,6 +46,7 @@ from reflex.base import Base
from reflex.constants.compiler import Hooks
from reflex.utils import console, exceptions, imports, serializers, types
from reflex.utils.exceptions import (
UntypedComputedVarError,
VarAttributeError,
VarDependencyError,
VarTypeError,
@ -545,52 +546,21 @@ class Var(Generic[VAR_TYPE]):
def create(
cls,
value: Any,
_var_is_local: bool | None = None,
_var_is_string: bool | None = None,
_var_data: VarData | None = None,
) -> Var:
"""Create a var from a value.
Args:
value: The value to create the var from.
_var_is_local: Whether the var is local. Deprecated.
_var_is_string: Whether the var is a string literal. Deprecated.
_var_data: Additional hooks and imports associated with the Var.
Returns:
The var.
"""
if _var_is_local is not None:
console.deprecate(
feature_name="_var_is_local",
reason="The _var_is_local argument is not supported for Var. "
"If you want to create a Var from a raw Javascript expression, use the constructor directly",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
if _var_is_string is not None:
console.deprecate(
feature_name="_var_is_string",
reason="The _var_is_string argument is not supported for Var. "
"If you want to create a Var from a raw Javascript expression, use the constructor directly",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
# If the value is already a var, do nothing.
if isinstance(value, Var):
return value
# Try to pull the imports and hooks from contained values.
if not isinstance(value, str):
return LiteralVar.create(value, _var_data=_var_data)
if _var_is_string is False or _var_is_local is True:
return cls(
_js_expr=value,
_var_data=_var_data,
)
return LiteralVar.create(value, _var_data=_var_data)
@classmethod
@ -1863,19 +1833,14 @@ class ComputedVar(Var[RETURN_TYPE]):
Raises:
TypeError: If the computed var dependencies are not Var instances or var names.
UntypedComputedVarError: If the computed var is untyped.
"""
hint = kwargs.pop("return_type", None) or get_type_hints(fget).get(
"return", Any
)
if hint is Any:
console.deprecate(
"untyped-computed-var",
"ComputedVar should have a return type annotation.",
"0.6.5",
"0.7.0",
)
raise UntypedComputedVarError(var_name=fget.__name__)
kwargs.setdefault("_js_expr", fget.__name__)
kwargs.setdefault("_var_type", hint)
@ -1948,6 +1913,7 @@ class ComputedVar(Var[RETURN_TYPE]):
"_var_data": kwargs.pop(
"_var_data", VarData.merge(self._var_data, merge_var_data)
),
"return_type": kwargs.pop("return_type", self._var_type),
}
if kwargs:
@ -2082,12 +2048,9 @@ class ComputedVar(Var[RETURN_TYPE]):
value = getattr(instance, self._cache_attr)
if not _isinstance(value, self._var_type):
console.deprecate(
"mismatched-computed-var-return",
f"Computed var {type(instance).__name__}.{self._js_expr} returned value of type {type(value)}, "
f"expected {self._var_type}. This might cause unexpected behavior.",
"0.6.5",
"0.7.0",
console.error(
f"Computed var '{type(instance).__name__}.{self._js_expr}' must return"
f" type '{self._var_type}', got '{type(value)}'."
)
return value

View File

@ -1,4 +1,4 @@
FROM python:3.9
FROM python:3.10
ARG USERNAME=kerrigan
RUN useradd -m $USERNAME

View File

@ -16,7 +16,7 @@ from .utils import SessionStorage
def CallScript():
"""A test app for browser javascript integration."""
from pathlib import Path
from typing import Dict, List, Optional, Union
from typing import Optional, Union
import reflex as rx
@ -43,15 +43,17 @@ def CallScript():
external_scripts = inline_scripts.replace("inline", "external")
class CallScriptState(rx.State):
results: List[Optional[Union[str, Dict, List]]] = []
inline_counter: int = 0
external_counter: int = 0
results: rx.Field[list[Optional[Union[str, dict, list]]]] = rx.field([])
inline_counter: rx.Field[int] = rx.field(0)
external_counter: rx.Field[int] = rx.field(0)
value: str = "Initial"
last_result: str = ""
last_result: int = 0
@rx.event
def call_script_callback(self, result):
self.results.append(result)
@rx.event
def call_script_callback_other_arg(self, result, other_arg):
self.results.append([other_arg, result])
@ -91,7 +93,7 @@ def CallScript():
def call_script_inline_return_lambda(self):
return rx.call_script(
"inline2()",
callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
callback=lambda result: CallScriptState.call_script_callback_other_arg(
result, "lambda"
),
)
@ -100,7 +102,7 @@ def CallScript():
def get_inline_counter(self):
return rx.call_script(
"inline_counter",
callback=CallScriptState.set_inline_counter, # type: ignore
callback=CallScriptState.setvar("inline_counter"),
)
@rx.event
@ -139,7 +141,7 @@ def CallScript():
def call_script_external_return_lambda(self):
return rx.call_script(
"external2()",
callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
callback=lambda result: CallScriptState.call_script_callback_other_arg(
result, "lambda"
),
)
@ -148,28 +150,28 @@ def CallScript():
def get_external_counter(self):
return rx.call_script(
"external_counter",
callback=CallScriptState.set_external_counter, # type: ignore
callback=CallScriptState.setvar("external_counter"),
)
@rx.event
def call_with_var_f_string(self):
return rx.call_script(
f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}",
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
)
@rx.event
def call_with_var_str_cast(self):
return rx.call_script(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}",
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
)
@rx.event
def call_with_var_f_string_wrapped(self):
return rx.call_script(
rx.Var(f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}"),
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
)
@rx.event
@ -178,7 +180,7 @@ def CallScript():
rx.Var(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}"
),
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
)
@rx.event
@ -193,17 +195,17 @@ def CallScript():
def index():
return rx.vstack(
rx.input(
value=CallScriptState.inline_counter.to(str), # type: ignore
value=CallScriptState.inline_counter.to(str),
id="inline_counter",
read_only=True,
),
rx.input(
value=CallScriptState.external_counter.to(str), # type: ignore
value=CallScriptState.external_counter.to(str),
id="external_counter",
read_only=True,
),
rx.text_area(
value=CallScriptState.results.to_string(), # type: ignore
value=CallScriptState.results.to_string(),
id="results",
read_only=True,
),
@ -273,7 +275,7 @@ def CallScript():
CallScriptState.value,
on_click=rx.call_script(
"'updated'",
callback=CallScriptState.set_value, # type: ignore
callback=CallScriptState.setvar("value"),
),
id="update_value",
),
@ -282,7 +284,7 @@ def CallScript():
value=CallScriptState.last_result,
id="last_result",
read_only=True,
on_click=CallScriptState.set_last_result(""), # type: ignore
on_click=CallScriptState.setvar("last_result", 0),
),
rx.button(
"call_with_var_f_string",
@ -308,7 +310,7 @@ def CallScript():
"call_with_var_f_string_inline",
on_click=rx.call_script(
f"{rx.Var('inline_counter')} + {CallScriptState.last_result}",
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_f_string_inline",
),
@ -316,7 +318,7 @@ def CallScript():
"call_with_var_str_cast_inline",
on_click=rx.call_script(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}",
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_str_cast_inline",
),
@ -326,7 +328,7 @@ def CallScript():
rx.Var(
f"{rx.Var('inline_counter')} + {CallScriptState.last_result}"
),
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_f_string_wrapped_inline",
),
@ -336,7 +338,7 @@ def CallScript():
rx.Var(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}"
),
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_str_cast_wrapped_inline",
),
@ -483,7 +485,7 @@ def test_call_script_w_var(
"""
assert_token(driver)
last_result = driver.find_element(By.ID, "last_result")
assert last_result.get_attribute("value") == ""
assert last_result.get_attribute("value") == "0"
inline_return_button = driver.find_element(By.ID, "inline_return")

View File

@ -33,18 +33,18 @@ def ClientSide():
class ClientSideSubState(ClientSideState):
# cookies with default settings
c1: str = rx.Cookie()
c2: rx.Cookie = "c2 default" # type: ignore
c2: str = rx.Cookie("c2 default")
# cookies with custom settings
c3: str = rx.Cookie(max_age=2) # expires after 2 second
c4: rx.Cookie = rx.Cookie(same_site="strict")
c4: str = rx.Cookie(same_site="strict")
c5: str = rx.Cookie(path="/foo/") # only accessible on `/foo/`
c6: str = rx.Cookie(name="c6")
c7: str = rx.Cookie("c7 default")
# local storage with default settings
l1: str = rx.LocalStorage()
l2: rx.LocalStorage = "l2 default" # type: ignore
l2: str = rx.LocalStorage("l2 default")
# local storage with custom settings
l3: str = rx.LocalStorage(name="l3")
@ -56,7 +56,7 @@ def ClientSide():
# Session storage
s1: str = rx.SessionStorage()
s2: rx.SessionStorage = "s2 default" # type: ignore
s2: str = rx.SessionStorage("s2 default")
s3: str = rx.SessionStorage(name="s3")
def set_l6(self, my_param: str):
@ -87,13 +87,13 @@ def ClientSide():
rx.input(
placeholder="state var",
value=ClientSideState.state_var,
on_change=ClientSideState.set_state_var, # type: ignore
on_change=ClientSideState.setvar("state_var"),
id="state_var",
),
rx.input(
placeholder="input value",
value=ClientSideState.input_value,
on_change=ClientSideState.set_input_value, # type: ignore
on_change=ClientSideState.setvar("input_value"),
id="input_value",
),
rx.button(

View File

@ -37,19 +37,6 @@ def test_shared_asset() -> None:
assert not Path(Path.cwd() / "assets/external").exists()
def test_deprecated_x_asset(capsys) -> None:
"""Test that the deprecated asset function raises a warning.
Args:
capsys: Pytest fixture that captures stdout and stderr.
"""
assert rx.asset("custom_script.js", shared=True) == rx._x.asset("custom_script.js")
assert (
"DeprecationWarning: rx._x.asset has been deprecated in version 0.6.6"
in capsys.readouterr().out
)
@pytest.mark.parametrize(
"path,shared",
[

View File

@ -1,7 +1,5 @@
"""Data display component tests fixtures."""
from typing import List
import pandas as pd
import pytest
@ -54,11 +52,11 @@ def data_table_state3():
"""
class DataTableState(BaseState):
_data: List = []
_columns: List = ["col1", "col2"]
_data: list = []
_columns: list = ["col1", "col2"]
@rx.var
def data(self) -> List:
def data(self) -> list:
return self._data
@rx.var
@ -77,15 +75,15 @@ def data_table_state4():
"""
class DataTableState(BaseState):
_data: List = []
_columns: List = ["col1", "col2"]
_data: list = []
_columns: list[str] = ["col1", "col2"]
@rx.var
def data(self):
return self._data
@rx.var
def columns(self) -> List:
def columns(self) -> list:
return self._columns
return DataTableState

View File

@ -4,6 +4,7 @@ import pytest
import reflex as rx
from reflex.components.gridjs.datatable import DataTable
from reflex.utils import types
from reflex.utils.exceptions import UntypedComputedVarError
from reflex.utils.serializers import serialize, serialize_dataframe
@ -75,17 +76,17 @@ def test_invalid_props(props):
[
(
"data_table_state2",
"Annotation of the computed var assigned to the data field should be provided.",
"Computed var 'data' must have a type annotation.",
True,
),
(
"data_table_state3",
"Annotation of the computed var assigned to the column field should be provided.",
"Computed var 'columns' must have a type annotation.",
False,
),
(
"data_table_state4",
"Annotation of the computed var assigned to the data field should be provided.",
"Computed var 'data' must have a type annotation.",
False,
),
],
@ -99,7 +100,7 @@ def test_computed_var_without_annotation(fixture, request, err_msg, is_data_fram
err_msg: expected error message.
is_data_frame: whether data field is a pandas dataframe.
"""
with pytest.raises(ValueError) as err:
with pytest.raises(UntypedComputedVarError) as err:
if is_data_frame:
DataTable.create(data=request.getfixturevalue(fixture).data)
else:

View File

@ -19,6 +19,7 @@ from reflex.constants import EventTriggers
from reflex.event import (
EventChain,
EventHandler,
JavascriptInputEvent,
input_event,
no_args_event_spec,
parse_args_spec,
@ -27,7 +28,11 @@ from reflex.event import (
from reflex.state import BaseState
from reflex.style import Style
from reflex.utils import imports
from reflex.utils.exceptions import ChildrenTypeError, EventFnArgMismatch
from reflex.utils.exceptions import (
ChildrenTypeError,
EventFnArgMismatchError,
EventHandlerArgTypeMismatchError,
)
from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
@ -94,11 +99,14 @@ def component2() -> Type[Component]:
A test component.
"""
def on_prop_event_spec(e0: Any):
return [e0]
class TestComponent2(Component):
# A test list prop.
arr: Var[List[str]]
on_prop_event: EventHandler[lambda e0: [e0]]
on_prop_event: EventHandler[on_prop_event_spec]
def get_event_triggers(self) -> Dict[str, Any]:
"""Test controlled triggers.
@ -818,10 +826,14 @@ def test_component_create_unpack_tuple_child(test_component, element, expected):
assert fragment_wrapper.render() == expected
class _Obj(Base):
custom: int = 0
class C1State(BaseState):
"""State for testing C1 component."""
def mock_handler(self, _e, _bravo, _charlie):
def mock_handler(self, _e: JavascriptInputEvent, _bravo: dict, _charlie: _Obj):
"""Mock handler."""
pass
@ -829,10 +841,12 @@ class C1State(BaseState):
def test_component_event_trigger_arbitrary_args():
"""Test that we can define arbitrary types for the args of an event trigger."""
class Obj(Base):
custom: int = 0
def on_foo_spec(_e, alpha: str, bravo: Dict[str, Any], charlie: Obj):
def on_foo_spec(
_e: Var[JavascriptInputEvent],
alpha: Var[str],
bravo: dict[str, Any],
charlie: Var[_Obj],
):
return [_e.target.value, bravo["nested"], charlie.custom + 42]
class C1(Component):
@ -845,13 +859,7 @@ def test_component_event_trigger_arbitrary_args():
"on_foo": on_foo_spec,
}
comp = C1.create(on_foo=C1State.mock_handler)
assert comp.render()["props"][0] == (
"onFoo={((__e, _alpha, _bravo, _charlie) => (addEvents("
f'[(Event("{C1State.get_full_name()}.mock_handler", ({{ ["_e"] : __e["target"]["value"], ["_bravo"] : _bravo["nested"], ["_charlie"] : (_charlie["custom"] + 42) }}), ({{ }})))], '
"[__e, _alpha, _bravo, _charlie], ({ }))))}"
)
C1.create(on_foo=C1State.mock_handler)
def test_create_custom_component(my_component):
@ -908,30 +916,29 @@ def test_invalid_event_handler_args(component2, test_state):
test_state: A test state.
"""
# EventHandler args must match
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(on_click=test_state.do_something_arg)
# Multiple EventHandler args: all must match
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(
on_click=[test_state.do_something_arg, test_state.do_something]
)
# Enable when 0.7.0 happens
# # Event Handler types must match
# with pytest.raises(EventHandlerArgTypeMismatch):
# component2.create(
# on_user_visited_count_changed=test_state.do_something_with_bool # noqa: ERA001 RUF100
# ) # noqa: ERA001 RUF100
# with pytest.raises(EventHandlerArgTypeMismatch):
# component2.create(on_user_list_changed=test_state.do_something_with_int) #noqa: ERA001
# with pytest.raises(EventHandlerArgTypeMismatch):
# component2.create(on_user_list_changed=test_state.do_something_with_list_int) #noqa: ERA001
with pytest.raises(EventHandlerArgTypeMismatchError):
component2.create(
on_user_visited_count_changed=test_state.do_something_with_bool
)
with pytest.raises(EventHandlerArgTypeMismatchError):
component2.create(on_user_list_changed=test_state.do_something_with_int)
with pytest.raises(EventHandlerArgTypeMismatchError):
component2.create(on_user_list_changed=test_state.do_something_with_list_int)
# component2.create(on_open=test_state.do_something_with_int) #noqa: ERA001
# component2.create(on_open=test_state.do_something_with_bool) #noqa: ERA001
# component2.create(on_user_visited_count_changed=test_state.do_something_with_int) #noqa: ERA001
# component2.create(on_user_list_changed=test_state.do_something_with_list_str) #noqa: ERA001
component2.create(on_open=test_state.do_something_with_int)
component2.create(on_open=test_state.do_something_with_bool)
component2.create(on_user_visited_count_changed=test_state.do_something_with_int)
component2.create(on_user_list_changed=test_state.do_something_with_list_str)
# lambda cannot return weird values.
with pytest.raises(ValueError):
@ -944,15 +951,15 @@ def test_invalid_event_handler_args(component2, test_state):
)
# lambda signature must match event trigger.
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(on_click=lambda _: test_state.do_something_arg(1))
# lambda returning EventHandler must match spec
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(on_click=lambda: test_state.do_something_arg)
# Mixed EventSpec and EventHandler must match spec.
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(
on_click=lambda: [
test_state.do_something_arg(1),
@ -1801,21 +1808,15 @@ def test_custom_component_declare_event_handlers_in_fields():
"""
return {
**super().get_event_triggers(),
"on_a": lambda e0: [e0],
"on_b": input_event,
"on_c": lambda e0: [],
"on_d": lambda: [],
"on_e": lambda: [],
"on_f": lambda a, b, c: [c, b, a],
}
class TestComponent(Component):
on_a: EventHandler[lambda e0: [e0]]
on_b: EventHandler[input_event]
on_c: EventHandler[no_args_event_spec]
on_d: EventHandler[no_args_event_spec]
on_e: EventHandler
on_f: EventHandler[lambda a, b, c: [c, b, a]]
custom_component = ReferenceComponent.create()
test_component = TestComponent.create()

View File

@ -199,16 +199,15 @@ def test_event_redirect(input, output):
input: The input for running the test.
output: The expected output to validate the test.
"""
path, external, replace = input
path, is_external, replace = input
kwargs = {}
if external is not None:
kwargs["external"] = external
if is_external is not None:
kwargs["is_external"] = is_external
if replace is not None:
kwargs["replace"] = replace
spec = event.redirect(path, **kwargs)
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_redirect"
assert format.format_event(spec) == output

View File

@ -1144,7 +1144,7 @@ def test_child_state():
class ChildState(MainState):
@computed_var
def rendered_var(self):
def rendered_var(self) -> int:
return self.v
ms = MainState()
@ -1421,7 +1421,7 @@ def test_computed_var_dependencies():
return self.testprop
@rx.var
def comp_w(self):
def comp_w(self) -> Callable[[], int]:
"""Nested lambda.
Returns:
@ -1430,7 +1430,7 @@ def test_computed_var_dependencies():
return lambda: self.w
@rx.var
def comp_x(self):
def comp_x(self) -> Callable[[], int]:
"""Nested function.
Returns:
@ -1443,7 +1443,7 @@ def test_computed_var_dependencies():
return _
@rx.var
def comp_y(self) -> List[int]:
def comp_y(self) -> list[int]:
"""Comprehension iterating over attribute.
Returns:
@ -3128,7 +3128,7 @@ async def test_get_state_from_sibling_not_cached(mock_app: rx.App, token: str):
child3_var: int = 0
@rx.var(cache=False)
def v(self):
def v(self) -> None:
pass
class Grandchild3(Child3):

View File

@ -11,7 +11,10 @@ import reflex as rx
from reflex.base import Base
from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG
from reflex.state import BaseState
from reflex.utils.exceptions import PrimitiveUnserializableToJSON
from reflex.utils.exceptions import (
PrimitiveUnserializableToJSON,
UntypedComputedVarError,
)
from reflex.utils.imports import ImportVar
from reflex.vars import VarData
from reflex.vars.base import (
@ -804,7 +807,7 @@ def test_shadow_computed_var_error(request: pytest.FixtureRequest, fixture: str)
request: Fixture Request.
fixture: The state fixture.
"""
with pytest.raises(NameError):
with pytest.raises(UntypedComputedVarError):
state = request.getfixturevalue(fixture)
state.var_without_annotation.foo