From ea5a3d44e7f21fc39635b978e6ac9cb647a97568 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Wed, 15 May 2024 01:47:08 +0000 Subject: [PATCH 001/496] [REF-2814]Throw Warning for Projects Created in OneDrive on Windows (#3304) * Throw Warning for Projects Created in OneDrive on Windows * precommit * remove dead code * REFLEX_USE_NPM escape hatch to opt out of bun In some unsupported environments, we need to just not use bun. Further investigation needed. --------- Co-authored-by: Masen Furer --- reflex/utils/prerequisites.py | 46 +++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index de4741c1e..cd4739c44 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -181,7 +181,12 @@ def get_install_package_manager() -> str | None: Returns: The path to the package manager. """ - if constants.IS_WINDOWS and not is_windows_bun_supported(): + if ( + constants.IS_WINDOWS + and not is_windows_bun_supported() + or windows_check_onedrive_in_path() + or windows_npm_escape_hatch() + ): return get_package_manager() return get_config().bun_path @@ -199,6 +204,24 @@ def get_package_manager() -> str | None: return npm_path +def windows_check_onedrive_in_path() -> bool: + """For windows, check if oneDrive is present in the project dir path. + + Returns: + If oneDrive is in the path of the project directory. + """ + return "onedrive" in str(Path.cwd()).lower() + + +def windows_npm_escape_hatch() -> bool: + """For windows, if the user sets REFLEX_USE_NPM, use npm instead of bun. + + Returns: + If the user has set REFLEX_USE_NPM. + """ + return os.environ.get("REFLEX_USE_NPM", "").lower() in ["true", "1", "yes"] + + def get_app(reload: bool = False) -> ModuleType: """Get the app module based on the default config. @@ -744,10 +767,17 @@ def install_bun(): Raises: FileNotFoundError: If required packages are not found. """ - if constants.IS_WINDOWS and not is_windows_bun_supported(): - console.warn( - "Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm." - ) + 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: + if not win_supported: + console.warn( + "Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm." + ) + if one_drive_in_path: + console.warn( + "Creating project directories in OneDrive is not recommended for bun usage on windows. This will fallback to npm." + ) # Skip if bun is already installed. if os.path.exists(get_config().bun_path) and get_bun_version() == version.parse( @@ -850,6 +880,7 @@ def install_frontend_packages(packages: set[str], config: Config): if not constants.IS_WINDOWS or constants.IS_WINDOWS and is_windows_bun_supported() + and not windows_check_onedrive_in_path() else None ) processes.run_process_with_fallback( @@ -939,6 +970,11 @@ def needs_reinit(frontend: bool = True) -> bool: console.warn( f"""On Python < 3.12, `uvicorn==0.20.0` is recommended for improved hot reload times. Found {uvi_ver} instead.""" ) + + if windows_check_onedrive_in_path(): + console.warn( + "Creating project directories in OneDrive may lead to performance issues. For optimal performance, It is recommended to avoid using OneDrive for your reflex app." + ) # No need to reinitialize if the app is already initialized. return False From 89a83ecd42cbc506180e57073e6f8afaafc7159d Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Wed, 15 May 2024 06:19:46 +0000 Subject: [PATCH 002/496] [REF-2803] Add imports benchmarks (#3272) --- .github/workflows/integration_tests.yml | 24 +++- scripts/benchmarks/benchmark_imports.py | 160 ++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 scripts/benchmarks/benchmark_imports.py diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index b1b622cb2..432a7f946 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -35,6 +35,8 @@ env: jobs: example-counter: + env: + OUTPUT_FILE: import_benchmark.json timeout-minutes: 30 strategy: # Prioritize getting more information out of the workflow (even if something fails) @@ -98,13 +100,31 @@ jobs: npm -v poetry run bash scripts/integration.sh ./reflex-examples/counter dev - name: Measure and upload .web size - if: ${{ env.DATABASE_URL }} + if: ${{ env.DATABASE_URL && github.event.pull_request.merged == true }} run: poetry run python scripts/benchmarks/benchmark_reflex_size.py --os "${{ matrix.os }}" --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}" --db-url "${{ env.DATABASE_URL }}" --branch-name "${{ github.head_ref || github.ref_name }}" --measurement-type "counter-app-dot-web" --path ./reflex-examples/counter/.web + - name: Install hyperfine + if: github.event.pull_request.merged == true + run: cargo install --locked hyperfine + - name: Benchmark imports + if: github.event.pull_request.merged == true + working-directory: ./reflex-examples/counter + run: hyperfine --warmup 3 "export POETRY_VIRTUALENVS_PATH=../../.venv; poetry run python counter/counter.py" --show-output --export-json "${{ env.OUTPUT_FILE }}" --shell bash + - name: Upload Benchmarks + if : ${{ env.DATABASE_URL && github.event.pull_request.merged == true }} + run: + poetry run python scripts/benchmarks/benchmark_imports.py --os "${{ matrix.os }}" + --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" + --benchmark-json "./reflex-examples/counter/${{ env.OUTPUT_FILE }}" + --db-url "${{ env.DATABASE_URL }}" --branch-name "${{ github.head_ref || github.ref_name }}" + --event-type "${{ github.event_name }}" --actor "${{ github.actor }}" --pr-id "${{ github.event.pull_request.id }}" + + + reflex-web: strategy: @@ -146,7 +166,7 @@ jobs: npm -v poetry run bash scripts/integration.sh ./reflex-web prod - name: Measure and upload .web size - if: ${{ env.DATABASE_URL }} + if: ${{ env.DATABASE_URL && github.event.pull_request.merged == true }} run: poetry run python scripts/benchmarks/benchmark_reflex_size.py --os "${{ matrix.os }}" --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" diff --git a/scripts/benchmarks/benchmark_imports.py b/scripts/benchmarks/benchmark_imports.py new file mode 100644 index 000000000..6258434d6 --- /dev/null +++ b/scripts/benchmarks/benchmark_imports.py @@ -0,0 +1,160 @@ +"""Runs the benchmarks and inserts the results into the database.""" + +from __future__ import annotations + +import argparse +import json +import os +from datetime import datetime + +import psycopg2 + + +def extract_stats_from_json(json_file: str) -> dict: + """Extracts the stats from the JSON data and returns them as dictionaries. + + Args: + json_file: The JSON file to extract the stats data from. + + Returns: + dict: The stats for each test. + """ + with open(json_file, "r") as file: + json_data = json.load(file) + + # Load the JSON data if it is a string, otherwise assume it's already a dictionary + data = json.loads(json_data) if isinstance(json_data, str) else json_data + + result = data.get("results", [{}])[0] + return { + k: v + for k, v in result.items() + if k in ("mean", "stddev", "median", "min", "max") + } + + +def insert_benchmarking_data( + db_connection_url: str, + os_type_version: str, + python_version: str, + performance_data: dict, + commit_sha: str, + pr_title: str, + branch_name: str, + event_type: str, + actor: str, + pr_id: str, +): + """Insert the benchmarking data into the database. + + Args: + db_connection_url: The URL to connect to the database. + os_type_version: The OS type and version to insert. + python_version: The Python version to insert. + performance_data: The imports performance data to insert. + commit_sha: The commit SHA to insert. + pr_title: The PR title to insert. + branch_name: The name of the branch. + event_type: Type of github event(push, pull request, etc) + actor: Username of the user that triggered the run. + pr_id: Id of the PR. + """ + # Serialize the JSON data + simple_app_performance_json = json.dumps(performance_data) + # Get the current timestamp + current_timestamp = datetime.now() + + # Connect to the database and insert the data + with psycopg2.connect(db_connection_url) as conn, conn.cursor() as cursor: + insert_query = """ + INSERT INTO import_benchmarks (os, python_version, commit_sha, time, pr_title, branch_name, event_type, actor, performance, pr_id) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s); + """ + cursor.execute( + insert_query, + ( + os_type_version, + python_version, + commit_sha, + current_timestamp, + pr_title, + branch_name, + event_type, + actor, + simple_app_performance_json, + pr_id, + ), + ) + # Commit the transaction + conn.commit() + + +def main(): + """Runs the benchmarks and inserts the results.""" + # Get the commit SHA and JSON directory from the command line arguments + parser = argparse.ArgumentParser(description="Run benchmarks and process results.") + parser.add_argument( + "--os", help="The OS type and version to insert into the database." + ) + parser.add_argument( + "--python-version", help="The Python version to insert into the database." + ) + parser.add_argument( + "--commit-sha", help="The commit SHA to insert into the database." + ) + parser.add_argument( + "--benchmark-json", + help="The JSON file containing the benchmark results.", + ) + parser.add_argument( + "--db-url", + help="The URL to connect to the database.", + required=True, + ) + parser.add_argument( + "--pr-title", + help="The PR title to insert into the database.", + ) + parser.add_argument( + "--branch-name", + help="The current branch", + required=True, + ) + parser.add_argument( + "--event-type", + help="The github event type", + required=True, + ) + parser.add_argument( + "--actor", + help="Username of the user that triggered the run.", + required=True, + ) + parser.add_argument( + "--pr-id", + help="ID of the PR.", + required=True, + ) + args = parser.parse_args() + + # Get the PR title from env or the args. For the PR merge or push event, there is no PR title, leaving it empty. + pr_title = args.pr_title or os.getenv("PR_TITLE", "") + + cleaned_benchmark_results = extract_stats_from_json(args.benchmark_json) + # Insert the data into the database + insert_benchmarking_data( + db_connection_url=args.db_url, + os_type_version=args.os, + python_version=args.python_version, + performance_data=cleaned_benchmark_results, + commit_sha=args.commit_sha, + pr_title=pr_title, + branch_name=args.branch_name, + event_type=args.event_type, + actor=args.actor, + pr_id=args.pr_id, + ) + + +if __name__ == "__main__": + main() From 76c8b2dfbdc8f88fc88fb2a43bda85f6fa884ed3 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 15 May 2024 02:54:52 -0700 Subject: [PATCH 003/496] Get `action`, `cancel`, `on_dismiss` and `on_auto_close` working for rx.toast (#3216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Get `action` and `cancel` working for rx.toast Respect defaults set in ToastProvider toast_options when firing a toast with it's own ToastProps set. * Update reflex/components/sonner/toast.py Co-authored-by: Thomas Brandého * Move queueEvent formatting into rx.utils.format module Implement on_auto_close and on_dismiss callbacks inside ToastProps * Update rx.call_script to use new format.format_queue_events Replace duplicate logic in rx.call_script for handling the callback function. * Move PropsBase to reflex.components.props This base class will be exposed via rx._x.PropsBase and can be shared by other wrapped components that need to pass a JS object full of extra props. --------- Co-authored-by: Thomas Brandého --- reflex/components/props.py | 30 ++++++++ reflex/components/sonner/toast.py | 120 ++++++++++++++++++++++------- reflex/components/sonner/toast.pyi | 41 ++++++---- reflex/event.py | 22 +++--- reflex/experimental/__init__.py | 2 + reflex/utils/format.py | 75 +++++++++++++++++- 6 files changed, 233 insertions(+), 57 deletions(-) create mode 100644 reflex/components/props.py diff --git a/reflex/components/props.py b/reflex/components/props.py new file mode 100644 index 000000000..92ee8a955 --- /dev/null +++ b/reflex/components/props.py @@ -0,0 +1,30 @@ +"""A class that holds props to be passed or applied to a component.""" +from __future__ import annotations + +from reflex.base import Base +from reflex.utils import format +from reflex.utils.serializers import serialize + + +class PropsBase(Base): + """Base for a class containing props that can be serialized as a JS object.""" + + def json(self) -> str: + """Convert the object to a json-like string. + + Vars will be unwrapped so they can represent actual JS var names and functions. + + Keys will be converted to camelCase. + + Returns: + The object as a Javascript Object literal. + """ + return format.unwrap_vars( + self.__config__.json_dumps( + { + format.to_camel_case(key): value + for key, value in self.dict().items() + }, + default=serialize, + ) + ) diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index 8a6c59a54..23a855aee 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -2,16 +2,20 @@ from __future__ import annotations -from typing import Literal +from typing import Any, Literal, Optional from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon -from reflex.event import EventSpec, call_script +from reflex.components.props import PropsBase +from reflex.event import ( + EventSpec, + call_script, +) from reflex.style import Style, color_mode from reflex.utils import format from reflex.utils.imports import ImportVar -from reflex.utils.serializers import serialize +from reflex.utils.serializers import serialize, serializer from reflex.vars import Var, VarData LiteralPosition = Literal[ @@ -27,46 +31,68 @@ LiteralPosition = Literal[ toast_ref = Var.create_safe("refs['__toast']") -class PropsBase(Base): - """Base class for all props classes.""" +class ToastAction(Base): + """A toast action that render a button in the toast.""" - def json(self) -> str: - """Convert the object to a json string. + label: str + on_click: Any - Returns: - The object as a json string. - """ - from reflex.utils.serializers import serialize - return self.__config__.json_dumps( - {format.to_camel_case(key): value for key, value in self.dict().items()}, - default=serialize, +@serializer +def serialize_action(action: ToastAction) -> dict: + """Serialize a toast action. + + Args: + action: The toast action to serialize. + + Returns: + The serialized toast action with on_click formatted to queue the given event. + """ + return { + "label": action.label, + "onClick": format.format_queue_events(action.on_click), + } + + +def _toast_callback_signature(toast: Var) -> list[Var]: + """The signature for the toast callback, stripping out unserializable keys. + + Args: + toast: The toast variable. + + Returns: + A function call stripping non-serializable members of the toast object. + """ + return [ + Var.create_safe( + f"(() => {{let {{action, cancel, onDismiss, onAutoClose, ...rest}} = {toast}; return rest}})()" ) + ] class ToastProps(PropsBase): """Props for the toast component.""" # Toast's description, renders underneath the title. - description: str = "" + description: Optional[str] # Whether to show the close button. - close_button: bool = False + close_button: Optional[bool] # Dark toast in light mode and vice versa. - invert: bool = False + invert: Optional[bool] # Control the sensitivity of the toast for screen readers - important: bool = False + important: Optional[bool] # Time in milliseconds that should elapse before automatically closing the toast. - duration: int = 4000 + duration: Optional[int] # Position of the toast. - position: LiteralPosition = "bottom-right" + position: Optional[LiteralPosition] # If false, it'll prevent the user from dismissing the toast. - dismissible: bool = True + dismissible: Optional[bool] # TODO: fix serialization of icons for toast? (might not be possible yet) # Icon displayed in front of toast's text, aligned vertically. @@ -74,25 +100,63 @@ class ToastProps(PropsBase): # TODO: fix implementation for action / cancel buttons # Renders a primary button, clicking it will close the toast. - # action: str = "" + action: Optional[ToastAction] # Renders a secondary button, clicking it will close the toast. - # cancel: str = "" + cancel: Optional[ToastAction] # Custom id for the toast. - id: str = "" + id: Optional[str] # Removes the default styling, which allows for easier customization. - unstyled: bool = False + unstyled: Optional[bool] # Custom style for the toast. - style: Style = Style() + style: Optional[Style] + # XXX: These still do not seem to work # Custom style for the toast primary button. - # action_button_styles: Style = Style() + action_button_styles: Optional[Style] # Custom style for the toast secondary button. - # cancel_button_styles: Style = Style() + cancel_button_styles: Optional[Style] + + # The function gets called when either the close button is clicked, or the toast is swiped. + on_dismiss: Optional[Any] + + # Function that gets called when the toast disappears automatically after it's timeout (duration` prop). + on_auto_close: Optional[Any] + + def dict(self, *args, **kwargs) -> dict: + """Convert the object to a dictionary. + + Args: + *args: The arguments to pass to the base class. + **kwargs: The keyword arguments to pass to the base + + Returns: + The object as a dictionary with ToastAction fields intact. + """ + kwargs.setdefault("exclude_none", True) + d = super().dict(*args, **kwargs) + # Keep these fields as ToastAction so they can be serialized specially + if "action" in d: + d["action"] = self.action + if isinstance(self.action, dict): + d["action"] = ToastAction(**self.action) + if "cancel" in d: + d["cancel"] = self.cancel + if isinstance(self.cancel, dict): + d["cancel"] = ToastAction(**self.cancel) + if "on_dismiss" in d: + d["on_dismiss"] = format.format_queue_events( + self.on_dismiss, _toast_callback_signature + ) + if "on_auto_close" in d: + d["on_auto_close"] = format.format_queue_events( + self.on_auto_close, _toast_callback_signature + ) + return d class Toaster(Component): diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index 2bf937703..5bd6cdeb4 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -7,15 +7,16 @@ from typing import Any, Dict, Literal, Optional, Union, overload from reflex.vars import Var, BaseVar, ComputedVar from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style -from typing import Literal +from typing import Any, Literal, Optional from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon +from reflex.components.props import PropsBase from reflex.event import EventSpec, call_script from reflex.style import Style, color_mode from reflex.utils import format from reflex.utils.imports import ImportVar -from reflex.utils.serializers import serialize +from reflex.utils.serializers import serialize, serializer from reflex.vars import Var, VarData LiteralPosition = Literal[ @@ -28,20 +29,32 @@ LiteralPosition = Literal[ ] toast_ref = Var.create_safe("refs['__toast']") -class PropsBase(Base): - def json(self) -> str: ... +class ToastAction(Base): + label: str + on_click: Any + +@serializer +def serialize_action(action: ToastAction) -> dict: ... class ToastProps(PropsBase): - description: str - close_button: bool - invert: bool - important: bool - duration: int - position: LiteralPosition - dismissible: bool - id: str - unstyled: bool - style: Style + description: Optional[str] + close_button: Optional[bool] + invert: Optional[bool] + important: Optional[bool] + duration: Optional[int] + position: Optional[LiteralPosition] + dismissible: Optional[bool] + action: Optional[ToastAction] + cancel: Optional[ToastAction] + id: Optional[str] + unstyled: Optional[bool] + style: Optional[Style] + action_button_styles: Optional[Style] + cancel_button_styles: Optional[Style] + on_dismiss: Optional[Any] + on_auto_close: Optional[Any] + + def dict(self, *args, **kwargs) -> dict: ... class Toaster(Component): @staticmethod diff --git a/reflex/event.py b/reflex/event.py index e1ae88076..3f1487c19 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -4,7 +4,6 @@ from __future__ import annotations import inspect from base64 import b64encode -from types import FunctionType from typing import ( Any, Callable, @@ -706,7 +705,11 @@ def _callback_arg_spec(eval_result): def call_script( javascript_code: str, - callback: EventHandler | Callable | None = None, + callback: EventSpec + | EventHandler + | Callable + | List[EventSpec | EventHandler | Callable] + | None = None, ) -> EventSpec: """Create an event handler that executes arbitrary javascript code. @@ -716,21 +719,14 @@ def call_script( Returns: EventSpec: An event that will execute the client side javascript. - - Raises: - ValueError: If the callback is not a valid event handler. """ callback_kwargs = {} if callback is not None: - arg_name = parse_args_spec(_callback_arg_spec)[0]._var_name - if isinstance(callback, EventHandler): - event_spec = call_event_handler(callback, _callback_arg_spec) - elif isinstance(callback, FunctionType): - event_spec = call_event_fn(callback, _callback_arg_spec)[0] - else: - raise ValueError("Cannot use {callback!r} as a call_script callback.") callback_kwargs = { - "callback": f"({arg_name}) => queueEvents([{format.format_event(event_spec)}], {constants.CompileVars.SOCKET})" + "callback": format.format_queue_events( + callback, + args_spec=lambda result: [result], + ) } return server_side( "_call_script", diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index 991dffd50..29bda8545 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -2,6 +2,7 @@ from types import SimpleNamespace +from reflex.components.props import PropsBase from reflex.components.radix.themes.components.progress import progress as progress from reflex.components.sonner.toast import toast as toast @@ -18,6 +19,7 @@ _x = SimpleNamespace( hooks=hooks, layout=layout, progress=progress, + PropsBase=PropsBase, run_in_thread=run_in_thread, toast=toast, ) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 70f6b5b25..5a0d1b959 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -6,7 +6,7 @@ import inspect import json import os import re -from typing import TYPE_CHECKING, Any, List, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union from reflex import constants from reflex.utils import exceptions, serializers, types @@ -15,7 +15,7 @@ from reflex.vars import BaseVar, Var if TYPE_CHECKING: from reflex.components.component import ComponentStyle - from reflex.event import EventChain, EventHandler, EventSpec + from reflex.event import ArgsSpec, EventChain, EventHandler, EventSpec WRAP_MAP = { "{": "}", @@ -590,6 +590,77 @@ def format_event_chain( ) +def format_queue_events( + events: EventSpec + | EventHandler + | Callable + | List[EventSpec | EventHandler | Callable] + | None = None, + args_spec: Optional[ArgsSpec] = None, +) -> Var[EventChain]: + """Format a list of event handler / event spec as a javascript callback. + + The resulting code can be passed to interfaces that expect a callback + function and when triggered it will directly call queueEvents. + + It is intended to be executed in the rx.call_script context, where some + existing API needs a callback to trigger a backend event handler. + + Args: + events: The events to queue. + args_spec: The argument spec for the callback. + + Returns: + The compiled javascript callback to queue the given events on the frontend. + """ + from reflex.event import ( + EventChain, + EventHandler, + EventSpec, + call_event_fn, + call_event_handler, + ) + + if not events: + return Var.create_safe( + "() => null", _var_is_string=False, _var_is_local=False + ).to(EventChain) + + # If no spec is provided, the function will take no arguments. + def _default_args_spec(): + return [] + + # Construct the arguments that the function accepts. + sig = inspect.signature(args_spec or _default_args_spec) # type: ignore + if sig.parameters: + arg_def = ",".join(f"_{p}" for p in sig.parameters) + arg_def = f"({arg_def})" + else: + arg_def = "()" + + payloads = [] + if not isinstance(events, list): + events = [events] + + # Process each event/spec/lambda (similar to Component._create_event_chain). + for spec in events: + specs: list[EventSpec] = [] + if isinstance(spec, (EventHandler, EventSpec)): + specs = [call_event_handler(spec, args_spec or _default_args_spec)] + elif isinstance(spec, type(lambda: None)): + specs = call_event_fn(spec, args_spec or _default_args_spec) + payloads.extend(format_event(s) for s in specs) + + # Return the final code snippet, expecting queueEvents, processEvent, and socket to be in scope. + # Typically this snippet will _only_ run from within an rx.call_script eval context. + return Var.create_safe( + f"{arg_def} => {{queueEvents([{','.join(payloads)}], {constants.CompileVars.SOCKET}); " + f"processEvent({constants.CompileVars.SOCKET})}}", + _var_is_string=False, + _var_is_local=False, + ).to(EventChain) + + def format_query_params(router_data: dict[str, Any]) -> dict[str, str]: """Convert back query params name to python-friendly case. From bd3df68bef9513553474bb5cb829effe2d0c6d89 Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Wed, 15 May 2024 02:56:16 -0700 Subject: [PATCH 004/496] Mirgrate from pip to uv (#3285) In order to improve build time performance, this change switches usage of pip to uv. The uv command is a pip alternative promising much faster installs of Python packages. For more information on uv, see: https://github.com/astral-sh/uv Closes: #2748 Signed-off-by: Eric Brown --- .github/actions/setup_build_env/action.yml | 5 +++++ .github/workflows/benchmarks.yml | 13 +++++++++---- .github/workflows/integration_app_harness.yml | 2 +- .github/workflows/integration_tests.yml | 8 ++++---- .github/workflows/integration_tests_wsl.yml | 7 ++++++- .github/workflows/pre-commit.yml | 2 +- .github/workflows/unit_tests.yml | 2 +- 7 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.github/actions/setup_build_env/action.yml b/.github/actions/setup_build_env/action.yml index 425f2d233..560b53749 100644 --- a/.github/actions/setup_build_env/action.yml +++ b/.github/actions/setup_build_env/action.yml @@ -106,3 +106,8 @@ runs: run: | source ${{ inputs.create-venv-at-path }}/*/activate poetry install --only-root --no-interaction + + - name: Install uv + shell: bash + run: | + poetry run pip install uv diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 5de54a528..617519f75 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -54,7 +54,7 @@ jobs: - name: Install Requirements for reflex-web working-directory: ./reflex-web - run: poetry run pip install -r requirements.txt + run: poetry run uv pip install -r requirements.txt - name: Init Website for reflex-web working-directory: ./reflex-web run: poetry run reflex init @@ -117,7 +117,7 @@ jobs: run-poetry-install: true create-venv-at-path: .venv - name: Install additional dependencies for DB access - run: poetry run pip install psycopg2-binary + run: poetry run uv pip install psycopg2-binary - name: Run benchmark tests env: APP_HARNESS_HEADLESS: 1 @@ -149,7 +149,7 @@ jobs: run-poetry-install: true create-venv-at-path: .venv - name: Install additional dependencies for DB access - run: poetry run pip install psycopg2-binary + run: poetry run uv pip install psycopg2-binary - name: Build reflex run: | poetry build @@ -192,8 +192,13 @@ jobs: source .venv/*/activate poetry install --without dev --no-interaction --no-root + - name: Install uv + shell: bash + run: | + poetry run pip install uv + - name: Install additional dependencies for DB access - run: poetry run pip install psycopg2-binary + run: poetry run uv pip install psycopg2-binary - if: ${{ env.DATABASE_URL }} name: calculate and upload size diff --git a/.github/workflows/integration_app_harness.yml b/.github/workflows/integration_app_harness.yml index 6b113cd79..e92fdb6d0 100644 --- a/.github/workflows/integration_app_harness.yml +++ b/.github/workflows/integration_app_harness.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} run-poetry-install: true create-venv-at-path: .venv - - run: poetry run pip install pyvirtualdisplay pillow + - run: poetry run uv pip install pyvirtualdisplay pillow - name: Run app harness tests env: SCREENSHOT_DIR: /tmp/screenshots diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 432a7f946..b83107504 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -76,9 +76,9 @@ jobs: - name: Install requirements for counter example working-directory: ./reflex-examples/counter run: | - poetry run pip install -r requirements.txt + poetry run uv pip install -r requirements.txt - name: Install additional dependencies for DB access - run: poetry run pip install psycopg2-binary + run: poetry run uv pip install psycopg2-binary - name: Check export --backend-only before init for counter example working-directory: ./reflex-examples/counter run: | @@ -154,9 +154,9 @@ jobs: - name: Install Requirements for reflex-web working-directory: ./reflex-web - run: poetry run pip install -r requirements.txt + run: poetry run uv pip install -r requirements.txt - name: Install additional dependencies for DB access - run: poetry run pip install psycopg2-binary + run: poetry run uv pip install psycopg2-binary - name: Init Website for reflex-web working-directory: ./reflex-web run: poetry run reflex init diff --git a/.github/workflows/integration_tests_wsl.yml b/.github/workflows/integration_tests_wsl.yml index 87f611898..6750fcd46 100644 --- a/.github/workflows/integration_tests_wsl.yml +++ b/.github/workflows/integration_tests_wsl.yml @@ -56,11 +56,16 @@ jobs: run: | poetry install + - name: Install uv + shell: wsl-bash {0} + run: | + poetry run pip install uv + - name: Install requirements for counter example working-directory: ./reflex-examples/counter shell: wsl-bash {0} run: | - poetry run pip install -r requirements.txt + poetry run uv pip install -r requirements.txt - name: Check export --backend-only before init for counter example working-directory: ./reflex-examples/counter shell: wsl-bash {0} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 452399c45..9e6e42a38 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -28,7 +28,7 @@ jobs: create-venv-at-path: .venv # TODO pre-commit related stuff can be cached too (not a bottleneck yet) - run: | - poetry run pip install pre-commit + poetry run uv pip install pre-commit poetry run pre-commit run --all-files env: SKIP: update-pyi-files diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 178859d29..f49a9b279 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -80,6 +80,6 @@ jobs: - name: Run unit tests w/ pydantic v1 run: | export PYTHONUNBUFFERED=1 - poetry run pip install "pydantic~=1.10" + poetry run uv pip install "pydantic~=1.10" poetry run pytest tests --cov --no-cov-on-fail --cov-report= - run: poetry run coverage html From ccc40e0c8d8e596ee69a08d6502aee8a655daeff Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Wed, 15 May 2024 09:57:11 +0000 Subject: [PATCH 005/496] Bump Bun version (#3281) --- reflex/constants/installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 5bf9b365e..3cca265e6 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -35,7 +35,7 @@ class Bun(SimpleNamespace): """Bun constants.""" # The Bun version. - VERSION = "1.1.6" + VERSION = "1.1.8" # Min Bun Version MIN_VERSION = "0.7.0" # The directory to store the bun. From efa9fcd6d549e2ce2f50c084e74bc417cf35a271 Mon Sep 17 00:00:00 2001 From: Yummy-Yums <77977520+Yummy-Yums@users.noreply.github.com> Date: Wed, 15 May 2024 18:52:50 +0000 Subject: [PATCH 006/496] FIX - Error: img is a self-closing tag and must neither have children nor use dangerouslySetInnerHTML. (#3307) * fixed errors * fix CI errors * CI fix again * used ruff formatting * pyi fix * fix app harness --------- Co-authored-by: Elijah --- reflex/components/component.py | 2 +- reflex/components/el/elements/media.py | 19 +++++++++++++++++++ reflex/components/el/elements/media.pyi | 4 +++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 39a97792e..3608f8fa5 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -374,7 +374,7 @@ class Component(BaseComponent, ABC): raise ValueError( f"The {(comp_name := type(self).__name__)} does not take in an `{key}` event trigger. If {comp_name}" f" is a third party component make sure to add `{key}` to the component's event triggers. " - f"visit https://reflex.dev/docs/wrapping-react/logic/#event-triggers for more info." + f"visit https://reflex.dev/docs/wrapping-react/guide/#event-triggers for more info." ) if key in triggers: # Event triggers are bound to event chains. diff --git a/reflex/components/el/elements/media.py b/reflex/components/el/elements/media.py index 2865ca66a..88c35e4cc 100644 --- a/reflex/components/el/elements/media.py +++ b/reflex/components/el/elements/media.py @@ -1,6 +1,7 @@ """Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" from typing import Any, Union +from reflex import Component from reflex.vars import Var as Var from .base import BaseHTML @@ -116,6 +117,24 @@ class Img(BaseHTML): # The name of the map to use with the image use_map: Var[Union[str, int, bool]] + @classmethod + def create(cls, *children, **props) -> Component: + """Override create method to apply source attribute to value if user fails to pass in attribute. + + Args: + *children: The children of the component. + **props: The props of the component. + + Returns: + The component. + + """ + return ( + super().create(src=children[0], **props) + if children + else super().create(*children, **props) + ) + class Map(BaseHTML): """Display the map element.""" diff --git a/reflex/components/el/elements/media.pyi b/reflex/components/el/elements/media.pyi index d1aa40978..53e2a341f 100644 --- a/reflex/components/el/elements/media.pyi +++ b/reflex/components/el/elements/media.pyi @@ -8,6 +8,7 @@ from reflex.vars import Var, BaseVar, ComputedVar from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style from typing import Any, Union +from reflex import Component from reflex.vars import Var as Var from .base import BaseHTML @@ -470,7 +471,7 @@ class Img(BaseHTML): ] = None, **props ) -> "Img": - """Create the component. + """Override create method to apply source attribute to value if user fails to pass in attribute. Args: *children: The children of the component. @@ -512,6 +513,7 @@ class Img(BaseHTML): Returns: The component. + """ ... From 30c8a07ba87c56465274ccce8bc3067809b3b80f Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Wed, 15 May 2024 11:55:18 -0700 Subject: [PATCH 007/496] Adds dependency review action to verify allowed licensed dependencies (#3306) This change will add a new action to scan the dependency's licenses for any that may not be allowed for this project. The pip-licenses command was run to get a dump of all the licenses associated with this repo and put into the allow-licenses list. Normally, you might only want to use deny-licenses list, but for packages like Redis, there is no defined SPDX identifier for it. Note: this list will require future maintenance as dependencies get added that are not already in the allow list. https://spdx.org/licenses/ https://github.com/raimon49/pip-licenses Related to issue #2901 Signed-off-by: Eric Brown --- .github/workflows/dependency-review.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 000000000..eb6eac00c --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,16 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 + with: + allow-licenses: Apache-2.0, BSD-2-Clause, BSD-3-Clause, HPND, ISC, MIT, MPL-2.0, PSF-2.0, Unlicense From 87a3ddea7fb2fdcd52f7e5431a8de5563693a46b Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Wed, 15 May 2024 20:58:10 +0200 Subject: [PATCH 008/496] allow passing kwargs and options to selenium webdriver (#2894) * allow passing kwargs to selenium webdriver * always create driver options, add x11 class to chromium * add driver_option_args --- reflex/testing.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/reflex/testing.py b/reflex/testing.py index 5613af333..aa8cbb893 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -26,6 +26,7 @@ from typing import ( AsyncIterator, Callable, Coroutine, + List, Optional, Type, TypeVar, @@ -513,12 +514,19 @@ class AppHarness: raise TimeoutError("Backend is not listening.") return backend.servers[0].sockets[0] - def frontend(self, driver_clz: Optional[Type["WebDriver"]] = None) -> "WebDriver": + def frontend( + self, + driver_clz: Optional[Type["WebDriver"]] = None, + driver_kwargs: dict[str, Any] | None = None, + driver_option_args: List[str] | None = None, + ) -> "WebDriver": """Get a selenium webdriver instance pointed at the app. Args: driver_clz: webdriver.Chrome (default), webdriver.Firefox, webdriver.Safari, webdriver.Edge, etc + driver_kwargs: additional keyword arguments to pass to the webdriver constructor + driver_option_args: additional arguments for the webdriver options Returns: Instance of the given webdriver navigated to the frontend url of the app. @@ -541,19 +549,30 @@ class AppHarness: requested_driver = os.environ.get("APP_HARNESS_DRIVER", "Chrome") driver_clz = getattr(webdriver, requested_driver) options = getattr(webdriver, f"{requested_driver}Options")() - if driver_clz is webdriver.Chrome and want_headless: + if driver_clz is webdriver.Chrome: options = webdriver.ChromeOptions() - options.add_argument("--headless=new") - elif driver_clz is webdriver.Firefox and want_headless: + options.add_argument("--class=AppHarness") + if want_headless: + options.add_argument("--headless=new") + elif driver_clz is webdriver.Firefox: options = webdriver.FirefoxOptions() - options.add_argument("-headless") - elif driver_clz is webdriver.Edge and want_headless: + if want_headless: + options.add_argument("-headless") + elif driver_clz is webdriver.Edge: options = webdriver.EdgeOptions() - options.add_argument("headless") - if options and (args := os.environ.get("APP_HARNESS_DRIVER_ARGS")): + if want_headless: + options.add_argument("headless") + if options is None: + raise RuntimeError(f"Could not determine options for {driver_clz}") + if args := os.environ.get("APP_HARNESS_DRIVER_ARGS"): for arg in args.split(","): options.add_argument(arg) - driver = driver_clz(options=options) # type: ignore + if driver_option_args is not None: + for arg in driver_option_args: + options.add_argument(arg) + if driver_kwargs is None: + driver_kwargs = {} + driver = driver_clz(options=options, **driver_kwargs) # type: ignore driver.get(self.frontend_url) self._frontends.append(driver) return driver From d96baac7d9f3e6e663994e6944abe048eae041f7 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Wed, 15 May 2024 21:07:41 +0200 Subject: [PATCH 009/496] typed mixins and ComponentState (#3196) * typed mixins * implicit mixin=True kwarg for ComponentState subclasses * fix: always init other subclasses * adjust tests: all mixins support base vars now --- integration/test_component_state.py | 1 + integration/test_state_inheritance.py | 66 ++++++++++++++++++--------- reflex/state.py | 37 ++++++++++++--- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/integration/test_component_state.py b/integration/test_component_state.py index d2c10c766..e903a1b74 100644 --- a/integration/test_component_state.py +++ b/integration/test_component_state.py @@ -1,4 +1,5 @@ """Test that per-component state scaffold works and operates independently.""" + from typing import Generator import pytest diff --git a/integration/test_state_inheritance.py b/integration/test_state_inheritance.py index 08a7fc951..86ab625e1 100644 --- a/integration/test_state_inheritance.py +++ b/integration/test_state_inheritance.py @@ -45,17 +45,15 @@ def StateInheritance(): """Test that state inheritance works as expected.""" import reflex as rx - class ChildMixin: - # mixin basevars only work with pydantic/rx.Base models - # child_mixin: str = "child_mixin" + class ChildMixin(rx.State, mixin=True): + child_mixin: str = "child_mixin" @rx.var def computed_child_mixin(self) -> str: return "computed_child_mixin" - class Mixin(ChildMixin): - # mixin basevars only work with pydantic/rx.Base models - # mixin: str = "mixin" + class Mixin(ChildMixin, mixin=True): + mixin: str = "mixin" @rx.var def computed_mixin(self) -> str: @@ -64,7 +62,7 @@ def StateInheritance(): def on_click_mixin(self): return rx.call_script("alert('clicked')") - class OtherMixin(rx.Base): + class OtherMixin(rx.State, mixin=True): other_mixin: str = "other_mixin" other_mixin_clicks: int = 0 @@ -78,7 +76,7 @@ def StateInheritance(): f"{self.__class__.__name__}.clicked.{self.other_mixin_clicks}" ) - class Base1(rx.State, Mixin): + class Base1(Mixin, rx.State): _base1: str = "_base1" base1: str = "base1" @@ -122,14 +120,15 @@ def StateInheritance(): def index() -> rx.Component: return rx.vstack( - rx.chakra.input( + rx.input( id="token", value=Base1.router.session.client_token, is_read_only=True ), - # Base 1 + # Base 1 (Mixin, ChildMixin) rx.heading(Base1.computed_mixin, id="base1-computed_mixin"), rx.heading(Base1.computed_basevar, id="base1-computed_basevar"), - rx.heading(Base1.computed_child_mixin, id="base1-child-mixin"), + rx.heading(Base1.computed_child_mixin, id="base1-computed-child-mixin"), rx.heading(Base1.base1, id="base1-base1"), + rx.heading(Base1.child_mixin, id="base1-child-mixin"), rx.button( "Base1.on_click_mixin", on_click=Base1.on_click_mixin, # type: ignore @@ -138,31 +137,33 @@ def StateInheritance(): rx.heading( Base1.computed_backend_vars_base1, id="base1-computed_backend_vars" ), - # Base 2 + # Base 2 (no mixins) rx.heading(Base2.computed_basevar, id="base2-computed_basevar"), rx.heading(Base2.base2, id="base2-base2"), rx.heading( Base2.computed_backend_vars_base2, id="base2-computed_backend_vars" ), - # Child 1 + # Child 1 (Mixin, ChildMixin, OtherMixin) rx.heading(Child1.computed_basevar, id="child1-computed_basevar"), rx.heading(Child1.computed_mixin, id="child1-computed_mixin"), rx.heading(Child1.computed_other_mixin, id="child1-other-mixin"), - rx.heading(Child1.computed_child_mixin, id="child1-child-mixin"), + rx.heading(Child1.computed_child_mixin, id="child1-computed-child-mixin"), rx.heading(Child1.base1, id="child1-base1"), rx.heading(Child1.other_mixin, id="child1-other_mixin"), + rx.heading(Child1.child_mixin, id="child1-child-mixin"), rx.button( "Child1.on_click_other_mixin", on_click=Child1.on_click_other_mixin, # type: ignore id="child1-other-mixin-btn", ), - # Child 2 + # Child 2 (Mixin, ChildMixin, OtherMixin) rx.heading(Child2.computed_basevar, id="child2-computed_basevar"), rx.heading(Child2.computed_mixin, id="child2-computed_mixin"), rx.heading(Child2.computed_other_mixin, id="child2-other-mixin"), - rx.heading(Child2.computed_child_mixin, id="child2-child-mixin"), + rx.heading(Child2.computed_child_mixin, id="child2-computed-child-mixin"), rx.heading(Child2.base2, id="child2-base2"), rx.heading(Child2.other_mixin, id="child2-other_mixin"), + rx.heading(Child2.child_mixin, id="child2-child-mixin"), rx.button( "Child2.on_click_mixin", on_click=Child2.on_click_mixin, # type: ignore @@ -173,15 +174,16 @@ def StateInheritance(): on_click=Child2.on_click_other_mixin, # type: ignore id="child2-other-mixin-btn", ), - # Child 3 + # Child 3 (Mixin, ChildMixin, OtherMixin) rx.heading(Child3.computed_basevar, id="child3-computed_basevar"), rx.heading(Child3.computed_mixin, id="child3-computed_mixin"), rx.heading(Child3.computed_other_mixin, id="child3-other-mixin"), rx.heading(Child3.computed_childvar, id="child3-computed_childvar"), - rx.heading(Child3.computed_child_mixin, id="child3-child-mixin"), + rx.heading(Child3.computed_child_mixin, id="child3-computed-child-mixin"), rx.heading(Child3.child3, id="child3-child3"), rx.heading(Child3.base2, id="child3-base2"), rx.heading(Child3.other_mixin, id="child3-other_mixin"), + rx.heading(Child3.child_mixin, id="child3-child-mixin"), rx.button( "Child3.on_click_mixin", on_click=Child3.on_click_mixin, # type: ignore @@ -282,7 +284,9 @@ def test_state_inheritance( base1_computed_basevar = driver.find_element(By.ID, "base1-computed_basevar") assert base1_computed_basevar.text == "computed_basevar1" - base1_computed_child_mixin = driver.find_element(By.ID, "base1-child-mixin") + base1_computed_child_mixin = driver.find_element( + By.ID, "base1-computed-child-mixin" + ) assert base1_computed_child_mixin.text == "computed_child_mixin" base1_base1 = driver.find_element(By.ID, "base1-base1") @@ -293,6 +297,9 @@ def test_state_inheritance( ) assert base1_computed_backend_vars.text == "_base1" + base1_child_mixin = driver.find_element(By.ID, "base1-child-mixin") + assert base1_child_mixin.text == "child_mixin" + # Base 2 base2_computed_basevar = driver.find_element(By.ID, "base2-computed_basevar") assert base2_computed_basevar.text == "computed_basevar2" @@ -315,7 +322,9 @@ def test_state_inheritance( child1_computed_other_mixin = driver.find_element(By.ID, "child1-other-mixin") assert child1_computed_other_mixin.text == "other_mixin" - child1_computed_child_mixin = driver.find_element(By.ID, "child1-child-mixin") + child1_computed_child_mixin = driver.find_element( + By.ID, "child1-computed-child-mixin" + ) assert child1_computed_child_mixin.text == "computed_child_mixin" child1_base1 = driver.find_element(By.ID, "child1-base1") @@ -324,6 +333,9 @@ def test_state_inheritance( child1_other_mixin = driver.find_element(By.ID, "child1-other_mixin") assert child1_other_mixin.text == "other_mixin" + child1_child_mixin = driver.find_element(By.ID, "child1-child-mixin") + assert child1_child_mixin.text == "child_mixin" + # Child 2 child2_computed_basevar = driver.find_element(By.ID, "child2-computed_basevar") assert child2_computed_basevar.text == "computed_basevar2" @@ -334,7 +346,9 @@ def test_state_inheritance( child2_computed_other_mixin = driver.find_element(By.ID, "child2-other-mixin") assert child2_computed_other_mixin.text == "other_mixin" - child2_computed_child_mixin = driver.find_element(By.ID, "child2-child-mixin") + child2_computed_child_mixin = driver.find_element( + By.ID, "child2-computed-child-mixin" + ) assert child2_computed_child_mixin.text == "computed_child_mixin" child2_base2 = driver.find_element(By.ID, "child2-base2") @@ -343,6 +357,9 @@ def test_state_inheritance( child2_other_mixin = driver.find_element(By.ID, "child2-other_mixin") assert child2_other_mixin.text == "other_mixin" + child2_child_mixin = driver.find_element(By.ID, "child2-child-mixin") + assert child2_child_mixin.text == "child_mixin" + # Child 3 child3_computed_basevar = driver.find_element(By.ID, "child3-computed_basevar") assert child3_computed_basevar.text == "computed_basevar2" @@ -356,7 +373,9 @@ def test_state_inheritance( child3_computed_childvar = driver.find_element(By.ID, "child3-computed_childvar") assert child3_computed_childvar.text == "computed_childvar" - child3_computed_child_mixin = driver.find_element(By.ID, "child3-child-mixin") + child3_computed_child_mixin = driver.find_element( + By.ID, "child3-computed-child-mixin" + ) assert child3_computed_child_mixin.text == "computed_child_mixin" child3_child3 = driver.find_element(By.ID, "child3-child3") @@ -368,6 +387,9 @@ def test_state_inheritance( child3_other_mixin = driver.find_element(By.ID, "child3-other_mixin") assert child3_other_mixin.text == "other_mixin" + child3_child_mixin = driver.find_element(By.ID, "child3-child-mixin") + assert child3_child_mixin.text == "child_mixin" + child3_computed_backend_vars = driver.find_element( By.ID, "child3-computed_backend_vars" ) diff --git a/reflex/state.py b/reflex/state.py index ebe33aa26..287f70073 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -359,6 +359,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # Whether the state has ever been touched since instantiation. _was_touched: bool = False + # Whether this state class is a mixin and should not be instantiated. + _mixin: ClassVar[bool] = False + # A special event handler for setting base vars. setvar: ClassVar[EventHandler] @@ -428,17 +431,17 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): """ return [ v - for mixin in cls.__mro__ - if mixin is cls or not issubclass(mixin, (BaseState, ABC)) + for mixin in cls._mixins() + [cls] for v in mixin.__dict__.values() if isinstance(v, ComputedVar) ] @classmethod - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls, mixin: bool = False, **kwargs): """Do some magic for the subclass initialization. Args: + mixin: Whether the subclass is a mixin and should not be initialized. **kwargs: The kwargs to pass to the pydantic init_subclass method. Raises: @@ -447,6 +450,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): from reflex.utils.exceptions import StateValueError super().__init_subclass__(**kwargs) + + cls._mixin = mixin + if mixin: + return + # Event handlers should not shadow builtin state methods. cls._check_overridden_methods() # Computed vars should not shadow builtin state props. @@ -618,8 +626,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): return [ mixin for mixin in cls.__mro__ - if not issubclass(mixin, (BaseState, ABC)) - and mixin not in [pydantic.BaseModel, Base] + if ( + mixin not in [pydantic.BaseModel, Base, cls] + and issubclass(mixin, BaseState) + and mixin._mixin is True + ) ] @classmethod @@ -742,7 +753,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): parent_states = [ base for base in cls.__bases__ - if types._issubclass(base, BaseState) and base is not BaseState + if issubclass(base, BaseState) and base is not BaseState and not base._mixin ] assert len(parent_states) < 2, "Only one parent state is allowed." return parent_states[0] if len(parent_states) == 1 else None # type: ignore @@ -1833,7 +1844,7 @@ class OnLoadInternalState(State): ] -class ComponentState(Base): +class ComponentState(State, mixin=True): """Base class to allow for the creation of a state instance per component. This allows for the bundling of UI and state logic into a single class, @@ -1875,6 +1886,18 @@ class ComponentState(Base): # The number of components created from this class. _per_component_state_instance_count: ClassVar[int] = 0 + @classmethod + def __init_subclass__(cls, mixin: bool = False, **kwargs): + """Overwrite mixin default to True. + + Args: + mixin: Whether the subclass is a mixin and should not be initialized. + **kwargs: The kwargs to pass to the pydantic init_subclass method. + """ + if ComponentState in cls.__bases__: + mixin = True + super().__init_subclass__(mixin=mixin, **kwargs) + @classmethod def get_component(cls, *children, **props) -> "Component": """Get the component instance. From c5f32db75659c220026bf0e99602876a78b27aaa Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 15 May 2024 14:59:45 -0700 Subject: [PATCH 010/496] [REF-2787] add_hooks supports Var-wrapped hooks (#3248) * [REF-2787] add_hooks supports Var-wrapped hooks * Fix VarData definition in .pyi file to allow removal of type ignore comments * Var.create and Var.create_safe accept _var_data parameter * Replace instances where a set of imports was being passed to VarData * Update code throughout reduce use of `._replace` to add VarData * Fixup: user hooks _var_data.imports will never be iterable, just a single ImportDict --- reflex/components/component.py | 57 +++++++++++++++---- reflex/components/core/banner.py | 12 ++-- reflex/components/core/cond.py | 2 +- reflex/components/core/debounce.py | 6 +- reflex/components/core/upload.py | 19 +++---- reflex/components/el/elements/forms.py | 18 ++++-- reflex/components/gridjs/datatable.py | 6 +- .../radix/themes/components/tabs.py | 2 +- reflex/components/sonner/toast.py | 17 +++--- reflex/constants/compiler.py | 6 +- reflex/style.py | 6 +- reflex/vars.py | 18 ++++-- reflex/vars.pyi | 12 ++-- tests/components/test_component.py | 40 ++++++++++++- 14 files changed, 158 insertions(+), 63 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 3608f8fa5..b039ebe0d 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -241,7 +241,7 @@ class Component(BaseComponent, ABC): """ return {} - def add_hooks(self) -> list[str]: + def add_hooks(self) -> list[str | Var]: """Add hooks inside the component function. Hooks are pieces of literal Javascript code that is inserted inside the @@ -1265,11 +1265,20 @@ class Component(BaseComponent, ABC): }, ) + other_imports = [] user_hooks = self._get_hooks() - if user_hooks is not None and isinstance(user_hooks, Var): - _imports = imports.merge_imports(_imports, user_hooks._var_data.imports) # type: ignore + if ( + user_hooks is not None + and isinstance(user_hooks, Var) + and user_hooks._var_data is not None + and user_hooks._var_data.imports + ): + other_imports.append(user_hooks._var_data.imports) + other_imports.extend( + hook_imports for hook_imports in self._get_added_hooks().values() + ) - return _imports + return imports.merge_imports(_imports, *other_imports) def _get_imports(self) -> imports.ImportDict: """Get all the libraries and fields that are used by the component. @@ -1416,6 +1425,36 @@ class Component(BaseComponent, ABC): **self._get_special_hooks(), } + def _get_added_hooks(self) -> dict[str, imports.ImportDict]: + """Get the hooks added via `add_hooks` method. + + Returns: + The deduplicated hooks and imports added by the component and parent components. + """ + code = {} + + def extract_var_hooks(hook: Var): + _imports = {} + if hook._var_data is not None: + for sub_hook in hook._var_data.hooks: + code[sub_hook] = {} + if hook._var_data.imports: + _imports = hook._var_data.imports + if str(hook) in code: + code[str(hook)] = imports.merge_imports(code[str(hook)], _imports) + else: + code[str(hook)] = _imports + + # Add the hook code from add_hooks for each parent class (this is reversed to preserve + # the order of the hooks in the final output) + for clz in reversed(tuple(self._iter_parent_classes_with_method("add_hooks"))): + for hook in clz.add_hooks(self): + if isinstance(hook, Var): + extract_var_hooks(hook) + else: + code[hook] = {} + return code + def _get_hooks(self) -> str | None: """Get the React hooks for this component. @@ -1454,11 +1493,7 @@ class Component(BaseComponent, ABC): if hooks is not None: code[hooks] = None - # Add the hook code from add_hooks for each parent class (this is reversed to preserve - # the order of the hooks in the final output) - for clz in reversed(tuple(self._iter_parent_classes_with_method("add_hooks"))): - for hook in clz.add_hooks(self): - code[hook] = None + code.update(self._get_added_hooks()) # Add the hook code for the children. for child in self.children: @@ -2092,8 +2127,8 @@ class StatefulComponent(BaseComponent): var_deps.extend(cls._get_hook_deps(hook)) memo_var_data = VarData.merge( *[var._var_data for var in event_args], - VarData( # type: ignore - imports={"react": {ImportVar(tag="useCallback")}}, + VarData( + imports={"react": [ImportVar(tag="useCallback")]}, ), ) diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py index 0c781fba8..c2fe3e688 100644 --- a/reflex/components/core/banner.py +++ b/reflex/components/core/banner.py @@ -29,23 +29,27 @@ connection_error: Var = Var.create_safe( value="(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''", _var_is_local=False, _var_is_string=False, -)._replace(merge_var_data=connect_error_var_data) + _var_data=connect_error_var_data, +) connection_errors_count: Var = Var.create_safe( value="connectErrors.length", _var_is_string=False, _var_is_local=False, -)._replace(merge_var_data=connect_error_var_data) + _var_data=connect_error_var_data, +) has_connection_errors: Var = Var.create_safe( value="connectErrors.length > 0", _var_is_string=False, -)._replace(_var_type=bool, merge_var_data=connect_error_var_data) + _var_data=connect_error_var_data, +).to(bool) has_too_many_connection_errors: Var = Var.create_safe( value="connectErrors.length >= 2", _var_is_string=False, -)._replace(_var_type=bool, merge_var_data=connect_error_var_data) + _var_data=connect_error_var_data, +).to(bool) class WebsocketTargetURL(Bare): diff --git a/reflex/components/core/cond.py b/reflex/components/core/cond.py index 0e3e43672..9ace92b98 100644 --- a/reflex/components/core/cond.py +++ b/reflex/components/core/cond.py @@ -13,7 +13,7 @@ from reflex.utils import format, imports from reflex.vars import BaseVar, Var, VarData _IS_TRUE_IMPORT = { - f"/{Dirs.STATE_PATH}": {imports.ImportVar(tag="isTrue")}, + f"/{Dirs.STATE_PATH}": [imports.ImportVar(tag="isTrue")], } diff --git a/reflex/components/core/debounce.py b/reflex/components/core/debounce.py index 5fabd4486..88d1e1f94 100644 --- a/reflex/components/core/debounce.py +++ b/reflex/components/core/debounce.py @@ -109,13 +109,11 @@ class DebounceInput(Component): "{%s}" % (child.alias or child.tag), _var_is_local=False, _var_is_string=False, - )._replace( - _var_type=Type[Component], - merge_var_data=VarData( # type: ignore + _var_data=VarData( imports=child._get_imports(), hooks=child._get_hooks_internal(), ), - ), + ).to(Type[Component]), ) component = super().create(**props) diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index b3ac37c15..65c441924 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -24,12 +24,12 @@ from reflex.vars import BaseVar, CallableVar, Var, VarData DEFAULT_UPLOAD_ID: str = "default" -upload_files_context_var_data: VarData = VarData( # type: ignore +upload_files_context_var_data: VarData = VarData( imports={ - "react": {imports.ImportVar(tag="useContext")}, - f"/{Dirs.CONTEXTS_PATH}": { + "react": [imports.ImportVar(tag="useContext")], + f"/{Dirs.CONTEXTS_PATH}": [ imports.ImportVar(tag="UploadFilesContext"), - }, + ], }, hooks={ "const [filesById, setFilesById] = useContext(UploadFilesContext);": None, @@ -118,14 +118,13 @@ def get_upload_dir() -> Path: uploaded_files_url_prefix: Var = Var.create_safe( - "${getBackendURL(env.UPLOAD)}" -)._replace( - merge_var_data=VarData( # type: ignore + "${getBackendURL(env.UPLOAD)}", + _var_data=VarData( imports={ - f"/{Dirs.STATE_PATH}": {imports.ImportVar(tag="getBackendURL")}, - "/env.json": {imports.ImportVar(tag="env", is_default=True)}, + f"/{Dirs.STATE_PATH}": [imports.ImportVar(tag="getBackendURL")], + "/env.json": [imports.ImportVar(tag="env", is_default=True)], } - ) + ), ) diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index a98bd47c7..37051b279 100644 --- a/reflex/components/el/elements/forms.py +++ b/reflex/components/el/elements/forms.py @@ -216,13 +216,17 @@ class Form(BaseHTML): if ref.startswith("refs_"): ref_var = Var.create_safe(ref[:-3]).as_ref() form_refs[ref[5:-3]] = Var.create_safe( - f"getRefValues({str(ref_var)})", _var_is_local=False - )._replace(merge_var_data=ref_var._var_data) + f"getRefValues({str(ref_var)})", + _var_is_local=False, + _var_data=ref_var._var_data, + ) else: ref_var = Var.create_safe(ref).as_ref() form_refs[ref[4:]] = Var.create_safe( - f"getRefValue({str(ref_var)})", _var_is_local=False - )._replace(merge_var_data=ref_var._var_data) + f"getRefValue({str(ref_var)})", + _var_is_local=False, + _var_data=ref_var._var_data, + ) return form_refs def _get_vars(self, include_children: bool = True) -> Iterator[Var]: @@ -619,14 +623,16 @@ class Textarea(BaseHTML): on_key_down=Var.create_safe( f"(e) => enterKeySubmitOnKeyDown(e, {self.enter_key_submit._var_name_unwrapped})", _var_is_local=False, - )._replace(merge_var_data=self.enter_key_submit._var_data), + _var_data=self.enter_key_submit._var_data, + ) ) if self.auto_height is not None: tag.add_props( on_input=Var.create_safe( f"(e) => autoHeightOnInput(e, {self.auto_height._var_name_unwrapped})", _var_is_local=False, - )._replace(merge_var_data=self.auto_height._var_data), + _var_data=self.auto_height._var_data, + ) ) return tag diff --git a/reflex/components/gridjs/datatable.py b/reflex/components/gridjs/datatable.py index 6c05dfd81..fd0a22021 100644 --- a/reflex/components/gridjs/datatable.py +++ b/reflex/components/gridjs/datatable.py @@ -114,12 +114,14 @@ class DataTable(Gridjs): _var_name=f"{self.data._var_name}.columns", _var_type=List[Any], _var_full_name_needs_state_prefix=True, - )._replace(merge_var_data=self.data._var_data) + _var_data=self.data._var_data, + ) self.data = BaseVar( _var_name=f"{self.data._var_name}.data", _var_type=List[List[Any]], _var_full_name_needs_state_prefix=True, - )._replace(merge_var_data=self.data._var_data) + _var_data=self.data._var_data, + ) if types.is_dataframe(type(self.data)): # If given a pandas df break up the data and columns data = serialize(self.data) diff --git a/reflex/components/radix/themes/components/tabs.py b/reflex/components/radix/themes/components/tabs.py index af1b6b521..130cfd166 100644 --- a/reflex/components/radix/themes/components/tabs.py +++ b/reflex/components/radix/themes/components/tabs.py @@ -68,7 +68,7 @@ class TabsTrigger(RadixThemesComponent): _valid_parents: List[str] = ["TabsList"] @classmethod - def create(self, *children, **props) -> Component: + def create(cls, *children, **props) -> Component: """Create a TabsTrigger component. Args: diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index 23a855aee..d820af2da 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -162,7 +162,7 @@ class ToastProps(PropsBase): class Toaster(Component): """A Toaster Component for displaying toast notifications.""" - library = "sonner@1.4.41" + library: str = "sonner@1.4.41" tag = "Toaster" @@ -209,12 +209,15 @@ class Toaster(Component): pause_when_page_is_hidden: Var[bool] def _get_hooks(self) -> Var[str]: - hook = Var.create_safe(f"{toast_ref} = toast", _var_is_local=True) - hook._var_data = VarData( # type: ignore - imports={ - "/utils/state": [ImportVar(tag="refs")], - self.library: [ImportVar(tag="toast", install=False)], - } + hook = Var.create_safe( + f"{toast_ref} = toast", + _var_is_local=True, + _var_data=VarData( + imports={ + "/utils/state": [ImportVar(tag="refs")], + self.library: [ImportVar(tag="toast", install=False)], + } + ), ) return hook diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index b99e31e8c..96e8b03ba 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -103,9 +103,9 @@ class Imports(SimpleNamespace): """Common sets of import vars.""" EVENTS = { - "react": {ImportVar(tag="useContext")}, - f"/{Dirs.CONTEXTS_PATH}": {ImportVar(tag="EventLoopContext")}, - f"/{Dirs.STATE_PATH}": {ImportVar(tag=CompileVars.TO_EVENT)}, + "react": [ImportVar(tag="useContext")], + f"/{Dirs.CONTEXTS_PATH}": [ImportVar(tag="EventLoopContext")], + f"/{Dirs.STATE_PATH}": [ImportVar(tag=CompileVars.TO_EVENT)], } diff --git a/reflex/style.py b/reflex/style.py index e48aa3dd8..21a601dd0 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -16,10 +16,10 @@ LIGHT_COLOR_MODE: str = "light" DARK_COLOR_MODE: str = "dark" # Reference the global ColorModeContext -color_mode_var_data = VarData( # type: ignore +color_mode_var_data = VarData( imports={ - f"/{constants.Dirs.CONTEXTS_PATH}": {ImportVar(tag="ColorModeContext")}, - "react": {ImportVar(tag="useContext")}, + f"/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="ColorModeContext")], + "react": [ImportVar(tag="useContext")], }, hooks={ f"const [ {constants.ColorMode.NAME}, {constants.ColorMode.TOGGLE} ] = useContext(ColorModeContext)": None, diff --git a/reflex/vars.py b/reflex/vars.py index 6ac78706a..cccef95eb 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -341,7 +341,11 @@ class Var: @classmethod def create( - cls, value: Any, _var_is_local: bool = True, _var_is_string: bool = False + cls, + value: Any, + _var_is_local: bool = True, + _var_is_string: bool = False, + _var_data: Optional[VarData] = None, ) -> Var | None: """Create a var from a value. @@ -349,6 +353,7 @@ class Var: value: The value to create the var from. _var_is_local: Whether the var is local. _var_is_string: Whether the var is a string literal. + _var_data: Additional hooks and imports associated with the Var. Returns: The var. @@ -365,9 +370,8 @@ class Var: return value # Try to pull the imports and hooks from contained values. - _var_data = None if not isinstance(value, str): - _var_data = VarData.merge(*_extract_var_data(value)) + _var_data = VarData.merge(*_extract_var_data(value), _var_data) # Try to serialize the value. type_ = type(value) @@ -388,7 +392,11 @@ class Var: @classmethod def create_safe( - cls, value: Any, _var_is_local: bool = True, _var_is_string: bool = False + cls, + value: Any, + _var_is_local: bool = True, + _var_is_string: bool = False, + _var_data: Optional[VarData] = None, ) -> Var: """Create a var from a value, asserting that it is not None. @@ -396,6 +404,7 @@ class Var: value: The value to create the var from. _var_is_local: Whether the var is local. _var_is_string: Whether the var is a string literal. + _var_data: Additional hooks and imports associated with the Var. Returns: The var. @@ -404,6 +413,7 @@ class Var: value, _var_is_local=_var_is_local, _var_is_string=_var_is_string, + _var_data=_var_data, ) assert var is not None return var diff --git a/reflex/vars.pyi b/reflex/vars.pyi index fb2ed4657..8251563f8 100644 --- a/reflex/vars.pyi +++ b/reflex/vars.pyi @@ -34,10 +34,10 @@ def _decode_var(value: str) -> tuple[VarData, str]: ... def _extract_var_data(value: Iterable) -> list[VarData | None]: ... class VarData(Base): - state: str - imports: dict[str, set[ImportVar]] - hooks: Dict[str, None] - interpolations: List[Tuple[int, int]] + state: str = "" + imports: dict[str, List[ImportVar]] = {} + hooks: Dict[str, None] = {} + interpolations: List[Tuple[int, int]] = [] @classmethod def merge(cls, *others: VarData | None) -> VarData | None: ... @@ -50,11 +50,11 @@ class Var: _var_data: VarData | None = None @classmethod def create( - cls, value: Any, _var_is_local: bool = False, _var_is_string: bool = False + cls, value: Any, _var_is_local: bool = False, _var_is_string: bool = False, _var_data: VarData | None = None, ) -> Optional[Var]: ... @classmethod def create_safe( - cls, value: Any, _var_is_local: bool = False, _var_is_string: bool = False + cls, value: Any, _var_is_local: bool = False, _var_is_string: bool = False, _var_data: VarData | None = None, ) -> Var: ... @classmethod def __class_getitem__(cls, type_: Type) -> _GenericAlias: ... diff --git a/tests/components/test_component.py b/tests/components/test_component.py index 6245746c9..e4d7205d7 100644 --- a/tests/components/test_component.py +++ b/tests/components/test_component.py @@ -1063,7 +1063,7 @@ def test_stateful_banner(): TEST_VAR = Var.create_safe("test")._replace( merge_var_data=VarData( hooks={"useTest": None}, - imports={"test": {ImportVar(tag="test")}}, + imports={"test": [ImportVar(tag="test")]}, state="Test", interpolations=[], ) @@ -1953,6 +1953,44 @@ def test_component_add_custom_code(): } +def test_component_add_hooks_var(): + class HookComponent(Component): + def add_hooks(self): + return [ + "const hook3 = useRef(null)", + "const hook1 = 42", + Var.create( + "useEffect(() => () => {}, [])", + _var_data=VarData( + hooks={ + "const hook2 = 43": None, + "const hook3 = useRef(null)": None, + }, + imports={"react": [ImportVar(tag="useEffect")]}, + ), + ), + Var.create( + "const hook3 = useRef(null)", + _var_data=VarData( + imports={"react": [ImportVar(tag="useRef")]}, + ), + ), + ] + + assert list(HookComponent()._get_all_hooks()) == [ + "const hook3 = useRef(null)", + "const hook1 = 42", + "const hook2 = 43", + "useEffect(() => () => {}, [])", + ] + imports = HookComponent()._get_all_imports() + assert len(imports) == 1 + assert "react" in imports + assert len(imports["react"]) == 2 + assert ImportVar(tag="useRef") in imports["react"] + assert ImportVar(tag="useEffect") in imports["react"] + + def test_add_style_embedded_vars(test_state: BaseState): """Test that add_style works with embedded vars when returning a plain dict. From 48c504666eef1b954f92c110eb239befdad90aae Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Thu, 16 May 2024 00:01:14 +0200 Subject: [PATCH 011/496] properly replace ComputedVars (#3254) * properly replace ComputedVars * provide dummy override decorator for older python versions * adjust pyi * fix darglint * cleanup var_data for computed vars inherited from mixin --------- Co-authored-by: Masen Furer --- reflex/state.py | 4 +++- reflex/utils/types.py | 16 ++++++++++++++++ reflex/vars.py | 40 ++++++++++++++++++++++++++++++++++------ reflex/vars.pyi | 1 + 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 287f70073..86a222b66 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -550,7 +550,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): for name, value in mixin.__dict__.items(): if isinstance(value, ComputedVar): fget = cls._copy_fn(value.fget) - newcv = ComputedVar(fget=fget, _var_name=value._var_name) + newcv = value._replace(fget=fget) + # cleanup refs to mixin cls in var_data + newcv._var_data = None newcv._var_set_state(cls) setattr(cls, name, newcv) cls.computed_vars[newcv._var_name] = newcv diff --git a/reflex/utils/types.py b/reflex/utils/types.py index f75e20dcc..6dd120e3d 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -44,6 +44,22 @@ from reflex import constants from reflex.base import Base from reflex.utils import console, serializers +if sys.version_info >= (3, 12): + from typing import override +else: + + def override(func: Callable) -> Callable: + """Fallback for @override decorator. + + Args: + func: The function to decorate. + + Returns: + The unmodified function. + """ + return func + + # Potential GenericAlias types for isinstance checks. GenericAliasTypes = [_GenericAlias] diff --git a/reflex/vars.py b/reflex/vars.py index cccef95eb..be6aa7eb8 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -39,10 +39,12 @@ from reflex.utils.exceptions import VarAttributeError, VarTypeError, VarValueErr # This module used to export ImportVar itself, so we still import it for export here from reflex.utils.imports import ImportDict, ImportVar +from reflex.utils.types import override if TYPE_CHECKING: from reflex.state import BaseState + # Set of unique variable names. USED_VARIABLES = set() @@ -832,19 +834,19 @@ class Var: if invoke_fn: # invoke the function on left operand. operation_name = ( - f"{left_operand_full_name}.{fn}({right_operand_full_name})" - ) # type: ignore + f"{left_operand_full_name}.{fn}({right_operand_full_name})" # type: ignore + ) else: # pass the operands as arguments to the function. operation_name = ( - f"{left_operand_full_name} {op} {right_operand_full_name}" - ) # type: ignore + f"{left_operand_full_name} {op} {right_operand_full_name}" # type: ignore + ) operation_name = f"{fn}({operation_name})" else: # apply operator to operands (left operand right_operand) operation_name = ( - f"{left_operand_full_name} {op} {right_operand_full_name}" - ) # type: ignore + f"{left_operand_full_name} {op} {right_operand_full_name}" # type: ignore + ) operation_name = format.wrap(operation_name, "(") else: # apply operator to left operand ( left_operand) @@ -1882,6 +1884,32 @@ class ComputedVar(Var, property): kwargs["_var_type"] = kwargs.pop("_var_type", self._determine_var_type()) BaseVar.__init__(self, **kwargs) # type: ignore + @override + def _replace(self, merge_var_data=None, **kwargs: Any) -> ComputedVar: + """Replace the attributes of the ComputedVar. + + Args: + merge_var_data: VarData to merge into the existing VarData. + **kwargs: Var fields to update. + + Returns: + The new ComputedVar instance. + """ + return ComputedVar( + fget=kwargs.get("fget", self.fget), + initial_value=kwargs.get("initial_value", self._initial_value), + cache=kwargs.get("cache", self._cache), + _var_name=kwargs.get("_var_name", self._var_name), + _var_type=kwargs.get("_var_type", self._var_type), + _var_is_local=kwargs.get("_var_is_local", self._var_is_local), + _var_is_string=kwargs.get("_var_is_string", self._var_is_string), + _var_full_name_needs_state_prefix=kwargs.get( + "_var_full_name_needs_state_prefix", + self._var_full_name_needs_state_prefix, + ), + _var_data=VarData.merge(self._var_data, merge_var_data), + ) + @property def _cache_attr(self) -> str: """Get the attribute used to cache the value on the instance. diff --git a/reflex/vars.pyi b/reflex/vars.pyi index 8251563f8..169e2d919 100644 --- a/reflex/vars.pyi +++ b/reflex/vars.pyi @@ -139,6 +139,7 @@ class ComputedVar(Var): def _cache_attr(self) -> str: ... def __get__(self, instance, owner): ... def _deps(self, objclass: Type, obj: Optional[FunctionType] = ...) -> Set[str]: ... + def _replace(self, merge_var_data=None, **kwargs: Any) -> ComputedVar: ... def mark_dirty(self, instance) -> None: ... def _determine_var_type(self) -> Type: ... @overload From 47043ae7872f8a7600addde5f999931274b39c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Thu, 16 May 2024 02:59:23 +0200 Subject: [PATCH 012/496] throw error for componentstate in foreach (#3243) --- reflex/components/core/foreach.py | 11 ++++++++++ tests/components/core/test_foreach.py | 31 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/reflex/components/core/foreach.py b/reflex/components/core/foreach.py index 88f2886a8..9a6765491 100644 --- a/reflex/components/core/foreach.py +++ b/reflex/components/core/foreach.py @@ -8,6 +8,7 @@ from reflex.components.base.fragment import Fragment from reflex.components.component import Component from reflex.components.tags import IterTag from reflex.constants import MemoizationMode +from reflex.state import ComponentState from reflex.utils import console from reflex.vars import Var @@ -50,6 +51,7 @@ class Foreach(Component): Raises: ForeachVarError: If the iterable is of type Any. + TypeError: If the render function is a ComponentState. """ if props: console.deprecate( @@ -65,6 +67,15 @@ class Foreach(Component): "(If you are trying to foreach over a state var, add a type annotation to the var). " "See https://reflex.dev/docs/library/layout/foreach/" ) + + if ( + hasattr(render_fn, "__qualname__") + and render_fn.__qualname__ == ComponentState.create.__qualname__ + ): + raise TypeError( + "Using a ComponentState as `render_fn` inside `rx.foreach` is not supported yet." + ) + component = cls( iterable=iterable, render_fn=render_fn, diff --git a/tests/components/core/test_foreach.py b/tests/components/core/test_foreach.py index 9691ed50e..6c4184590 100644 --- a/tests/components/core/test_foreach.py +++ b/tests/components/core/test_foreach.py @@ -3,8 +3,9 @@ from typing import Dict, List, Set, Tuple, Union import pytest from reflex.components import box, el, foreach, text +from reflex.components.component import Component from reflex.components.core.foreach import Foreach, ForeachRenderError, ForeachVarError -from reflex.state import BaseState +from reflex.state import BaseState, ComponentState from reflex.vars import Var @@ -37,6 +38,25 @@ class ForEachState(BaseState): color_index_tuple: Tuple[int, str] = (0, "red") +class TestComponentState(ComponentState): + """A test component state.""" + + foo: bool + + @classmethod + def get_component(cls, *children, **props) -> Component: + """Get the component. + + Args: + children: The children components. + props: The component props. + + Returns: + The component. + """ + return el.div(*children, **props) + + def display_color(color): assert color._var_type == str return box(text(color)) @@ -252,3 +272,12 @@ def test_foreach_component_styles(): ) component._add_style_recursive({box: {"color": "red"}}) assert 'css={{"color": "red"}}' in str(component) + + +def test_foreach_component_state(): + """Test that using a component state to render in the foreach raises an error.""" + with pytest.raises(TypeError): + Foreach.create( + ForEachState.colors_list, + TestComponentState.create, + ) From 89352ac10e7a55a2406fd25170ce3dfe0522270f Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 16 May 2024 13:20:26 -0700 Subject: [PATCH 013/496] rx._x.client_state: react useState Var integration for frontend and backend (#3269) New experimental feature to create client-side react state vars, save them in the global `refs` object and access them in frontend rendering/event triggers as well on the backend via call_script. --- reflex/experimental/__init__.py | 2 + reflex/experimental/client_state.py | 198 ++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 reflex/experimental/client_state.py diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index 29bda8545..6972fdfe0 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -8,6 +8,7 @@ from reflex.components.sonner.toast import toast as toast from ..utils.console import warn from . import hooks as hooks +from .client_state import ClientStateVar as ClientStateVar from .layout import layout as layout from .misc import run_in_thread as run_in_thread @@ -16,6 +17,7 @@ warn( ) _x = SimpleNamespace( + client_state=ClientStateVar.create, hooks=hooks, layout=layout, progress=progress, diff --git a/reflex/experimental/client_state.py b/reflex/experimental/client_state.py new file mode 100644 index 000000000..d0028991c --- /dev/null +++ b/reflex/experimental/client_state.py @@ -0,0 +1,198 @@ +"""Handle client side state with `useState`.""" + +import dataclasses +import sys +from typing import Any, Callable, Optional, Type + +from reflex import constants +from reflex.event import EventChain, EventHandler, EventSpec, call_script +from reflex.utils.imports import ImportVar +from reflex.vars import Var, VarData + + +def _client_state_ref(var_name: str) -> str: + """Get the ref path for a ClientStateVar. + + Args: + var_name: The name of the variable. + + Returns: + An accessor for ClientStateVar ref as a string. + """ + return f"refs['_client_state_{var_name}']" + + +@dataclasses.dataclass( + eq=False, + **{"slots": True} if sys.version_info >= (3, 10) else {}, +) +class ClientStateVar(Var): + """A Var that exists on the client via useState.""" + + # The name of the var. + _var_name: str = dataclasses.field() + + # Track the names of the getters and setters + _setter_name: str = dataclasses.field() + _getter_name: str = dataclasses.field() + + # The type of the var. + _var_type: Type = dataclasses.field(default=Any) + + # Whether this is a local javascript variable. + _var_is_local: bool = dataclasses.field(default=False) + + # Whether the var is a string literal. + _var_is_string: bool = dataclasses.field(default=False) + + # _var_full_name should be prefixed with _var_state + _var_full_name_needs_state_prefix: bool = dataclasses.field(default=False) + + # Extra metadata associated with the Var + _var_data: Optional[VarData] = dataclasses.field(default=None) + + def __hash__(self) -> int: + """Define a hash function for a var. + + Returns: + The hash of the var. + """ + return hash( + (self._var_name, str(self._var_type), self._getter_name, self._setter_name) + ) + + @classmethod + def create(cls, var_name, default=None) -> "ClientStateVar": + """Create a local_state Var that can be accessed and updated on the client. + + The `ClientStateVar` should be included in the highest parent component + that contains the components which will access and manipulate the client + state. It has no visual rendering, including it ensures that the + `useState` hook is called in the correct scope. + + To render the var in a component, use the `value` property. + + To update the var in a component, use the `set` property. + + To access the var in an event handler, use the `retrieve` method with + `callback` set to the event handler which should receive the value. + + To update the var in an event handler, use the `push` method with the + value to update. + + Args: + var_name: The name of the variable. + default: The default value of the variable. + + Returns: + ClientStateVar + """ + if default is None: + default_var = Var.create_safe("", _var_is_local=False, _var_is_string=False) + elif not isinstance(default, Var): + default_var = Var.create_safe(default) + else: + default_var = default + setter_name = f"set{var_name.capitalize()}" + return cls( + _var_name="", + _setter_name=setter_name, + _getter_name=var_name, + _var_is_local=False, + _var_is_string=False, + _var_type=default_var._var_type, + _var_data=VarData.merge( + default_var._var_data, + VarData( # type: ignore + hooks={ + f"const [{var_name}, {setter_name}] = useState({default_var._var_name_unwrapped})": None, + f"{_client_state_ref(var_name)} = {var_name}": None, + f"{_client_state_ref(setter_name)} = {setter_name}": None, + }, + imports={ + "react": {ImportVar(tag="useState", install=False)}, + f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")], + }, + ), + ), + ) + + @property + def value(self) -> Var: + """Get a placeholder for the Var. + + This property can only be rendered on the frontend. + + To access the value in a backend event handler, see `retrieve`. + + Returns: + an accessor for the client state variable. + """ + return ( + Var.create_safe( + _client_state_ref(self._getter_name), + _var_is_local=False, + _var_is_string=False, + ) + .to(self._var_type) + ._replace( + merge_var_data=VarData( # type: ignore + imports={ + f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")], + } + ) + ) + ) + + @property + def set(self) -> Var: + """Set the value of the client state variable. + + This property can only be attached to a frontend event trigger. + + To set a value from a backend event handler, see `push`. + + Returns: + A special EventChain Var which will set the value when triggered. + """ + return ( + Var.create_safe( + _client_state_ref(self._setter_name), + _var_is_local=False, + _var_is_string=False, + ) + .to(EventChain) + ._replace( + merge_var_data=VarData( # type: ignore + imports={ + f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")], + } + ) + ) + ) + + def retrieve(self, callback: EventHandler | Callable | None = None) -> EventSpec: + """Pass the value of the client state variable to a backend EventHandler. + + The event handler must `yield` or `return` the EventSpec to trigger the event. + + Args: + callback: The callback to pass the value to. + + Returns: + An EventSpec which will retrieve the value when triggered. + """ + return call_script(_client_state_ref(self._getter_name), callback=callback) + + def push(self, value: Any) -> EventSpec: + """Push a value to the client state variable from the backend. + + The event handler must `yield` or `return` the EventSpec to trigger the event. + + Args: + value: The value to update. + + Returns: + An EventSpec which will push the value when triggered. + """ + return call_script(f"{_client_state_ref(self._setter_name)}({value})") From bc6f0f70cb66e85faa58e5a1b474b3ce8f1c8d04 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 16 May 2024 13:21:40 -0700 Subject: [PATCH 014/496] Support replacing route on redirect (#3072) * Support replacing route on redirect Support next/router `.replace` interface to change page without creating a history entry. * test_event: include test cases for new "replace" kwarg --- reflex/.templates/web/utils/state.js | 9 +++++-- reflex/event.py | 13 ++++++++-- tests/test_event.py | 39 +++++++++++++++++++++------- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 5bc6b8b8b..8386261e9 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -126,8 +126,13 @@ export const applyDelta = (state, delta) => { export const applyEvent = async (event, socket) => { // Handle special events if (event.name == "_redirect") { - if (event.payload.external) window.open(event.payload.path, "_blank"); - else Router.push(event.payload.path); + if (event.payload.external) { + window.open(event.payload.path, "_blank"); + } else if (event.payload.replace) { + Router.replace(event.payload.path); + } else { + Router.push(event.payload.path); + } return false; } diff --git a/reflex/event.py b/reflex/event.py index 3f1487c19..96a59fdc1 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -467,18 +467,27 @@ def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec: ) -def redirect(path: str | Var[str], external: Optional[bool] = False) -> EventSpec: +def redirect( + path: str | Var[str], + external: Optional[bool] = False, + replace: Optional[bool] = False, +) -> EventSpec: """Redirect to a new path. Args: path: The path to redirect to. external: Whether to open in new tab or not. + replace: If True, the current page will not create a new history entry. Returns: An event to redirect to the path. """ return server_side( - "_redirect", get_fn_signature(redirect), path=path, external=external + "_redirect", + get_fn_signature(redirect), + path=path, + external=external, + replace=replace, ) diff --git a/tests/test_event.py b/tests/test_event.py index 885263157..284542a43 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -158,12 +158,29 @@ def test_fix_events(arg1, arg2): @pytest.mark.parametrize( "input,output", [ - (("/path", None), 'Event("_redirect", {path:`/path`,external:false})'), - (("/path", True), 'Event("_redirect", {path:`/path`,external:true})'), - (("/path", False), 'Event("_redirect", {path:`/path`,external:false})'), ( - (Var.create_safe("path"), None), - 'Event("_redirect", {path:path,external:false})', + ("/path", None, None), + 'Event("_redirect", {path:`/path`,external:false,replace:false})', + ), + ( + ("/path", True, None), + 'Event("_redirect", {path:`/path`,external:true,replace:false})', + ), + ( + ("/path", False, None), + 'Event("_redirect", {path:`/path`,external:false,replace:false})', + ), + ( + (Var.create_safe("path"), None, None), + 'Event("_redirect", {path:path,external:false,replace:false})', + ), + ( + ("/path", None, True), + 'Event("_redirect", {path:`/path`,external:false,replace:true})', + ), + ( + ("/path", True, True), + 'Event("_redirect", {path:`/path`,external:true,replace:true})', ), ], ) @@ -174,11 +191,13 @@ def test_event_redirect(input, output): input: The input for running the test. output: The expected output to validate the test. """ - path, external = input - if external is None: - spec = event.redirect(path) - else: - spec = event.redirect(path, external=external) + path, external, replace = input + kwargs = {} + if external is not None: + kwargs["external"] = external + if replace is not None: + kwargs["replace"] = replace + spec = event.redirect(path, **kwargs) assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_redirect" From 99d59104ad71b6dd28426b14ff5721c685a5bf5d Mon Sep 17 00:00:00 2001 From: Santiago Botero <98826652+boterop@users.noreply.github.com> Date: Thu, 16 May 2024 15:22:44 -0500 Subject: [PATCH 015/496] Add end line in .gitignore (#3309) --- reflex/utils/prerequisites.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index cd4739c44..1852f0434 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -413,7 +413,7 @@ def initialize_gitignore( # Write files to the .gitignore file. with open(gitignore_file, "w", newline="\n") as f: console.debug(f"Creating {gitignore_file}") - f.write(f"{(path_ops.join(sorted(files_to_ignore))).lstrip()}") + f.write(f"{(path_ops.join(sorted(files_to_ignore))).lstrip()}\n") def initialize_requirements_txt(): From 9ba179410b5d5a0be26c13ff53517918717b38ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Fri, 17 May 2024 07:08:32 +0200 Subject: [PATCH 016/496] wip connection toaster (#3242) * wip connection toaster * never duplicate toast for websocket-error * wip update banner * clean up PR * fix for 3.8 * update pyi * ConnectionToaster tweaks * Use `has_too_many_connection_errors` to avoid showing the banner immediately * Increase toast duration to avoid frequent, distracting flashing of the toast * Automatically dismiss the toast when the connection comes back up * Include `close_button` for user to dismiss the toast * If the user dismisses the toast, do not show it again until the connection comes back and drops again * Use `connection_error` var instead of a custom util_hook to get the message * ConnectionPulser: hide behind toast * Hide the connection pulser behind the toast (33x33) * Add a title (tooltip) that shows the connection error * Re-add connection pulser to default overlay_component If the user dismisses the toast, we still want to indicate that the backend is actually down. * Fix pre-commit issue from main --------- Co-authored-by: Masen Furer --- reflex/app.py | 4 +- reflex/components/core/__init__.py | 8 +- reflex/components/core/banner.py | 73 +++++++++++++++- reflex/components/core/banner.pyi | 130 ++++++++++++++++++++++++++++ reflex/components/sonner/toast.py | 4 +- reflex/components/sonner/toast.pyi | 4 +- reflex/experimental/client_state.py | 2 +- 7 files changed, 215 insertions(+), 10 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 4d99d6949..4a7c60e2e 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -41,7 +41,6 @@ from reflex.base import Base from reflex.compiler import compiler from reflex.compiler import utils as compiler_utils from reflex.compiler.compiler import ExecutorSafeFunctions -from reflex.components import connection_modal, connection_pulser from reflex.components.base.app_wrap import AppWrap from reflex.components.base.fragment import Fragment from reflex.components.component import ( @@ -49,6 +48,7 @@ from reflex.components.component import ( ComponentStyle, evaluate_style_namespaces, ) +from reflex.components.core import connection_pulser, connection_toaster from reflex.components.core.client_side_routing import ( Default404Page, wait_for_client_redirect, @@ -91,7 +91,7 @@ def default_overlay_component() -> Component: Returns: The default overlay_component, which is a connection_modal. """ - return Fragment.create(connection_pulser(), connection_modal()) + return Fragment.create(connection_pulser(), connection_toaster()) class OverlayFragment(Fragment): diff --git a/reflex/components/core/__init__.py b/reflex/components/core/__init__.py index 80c73add8..877d27739 100644 --- a/reflex/components/core/__init__.py +++ b/reflex/components/core/__init__.py @@ -1,7 +1,12 @@ """Core Reflex components.""" from . import layout as layout -from .banner import ConnectionBanner, ConnectionModal, ConnectionPulser +from .banner import ( + ConnectionBanner, + ConnectionModal, + ConnectionPulser, + ConnectionToaster, +) from .colors import color from .cond import Cond, color_mode_cond, cond from .debounce import DebounceInput @@ -26,6 +31,7 @@ from .upload import ( connection_banner = ConnectionBanner.create connection_modal = ConnectionModal.create +connection_toaster = ConnectionToaster.create connection_pulser = ConnectionPulser.create debounce_input = DebounceInput.create foreach = Foreach.create diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py index c2fe3e688..c6250743c 100644 --- a/reflex/components/core/banner.py +++ b/reflex/components/core/banner.py @@ -16,8 +16,11 @@ from reflex.components.radix.themes.components.dialog import ( ) from reflex.components.radix.themes.layout import Flex from reflex.components.radix.themes.typography.text import Text +from reflex.components.sonner.toast import Toaster, ToastProps from reflex.constants import Dirs, Hooks, Imports +from reflex.constants.compiler import CompileVars from reflex.utils import imports +from reflex.utils.serializers import serialize from reflex.vars import Var, VarData connect_error_var_data: VarData = VarData( # type: ignore @@ -25,6 +28,13 @@ connect_error_var_data: VarData = VarData( # type: ignore hooks={Hooks.EVENTS: None}, ) +connect_errors: Var = Var.create_safe( + value=CompileVars.CONNECT_ERROR, + _var_is_local=True, + _var_is_string=False, + _var_data=connect_error_var_data, +) + connection_error: Var = Var.create_safe( value="(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''", _var_is_local=False, @@ -85,6 +95,64 @@ def default_connection_error() -> list[str | Var | Component]: ] +class ConnectionToaster(Toaster): + """A connection toaster component.""" + + def add_hooks(self) -> list[str]: + """Add the hooks for the connection toaster. + + Returns: + The hooks for the connection toaster. + """ + toast_id = "websocket-error" + target_url = WebsocketTargetURL.create() + props = ToastProps( # type: ignore + description=Var.create( + f"`Check if server is reachable at ${target_url}`", + _var_is_string=False, + _var_is_local=False, + ), + close_button=True, + duration=120000, + id=toast_id, + ) + hook = Var.create( + f""" +const toast_props = {serialize(props)}; +const [userDismissed, setUserDismissed] = useState(false); +useEffect(() => {{ + if ({has_too_many_connection_errors}) {{ + if (!userDismissed) {{ + toast.error( + `Cannot connect to server: {connection_error}.`, + {{...toast_props, onDismiss: () => setUserDismissed(true)}}, + ) + }} + }} else {{ + toast.dismiss("{toast_id}"); + setUserDismissed(false); // after reconnection reset dismissed state + }} +}}, [{connect_errors}]);""" + ) + + hook._var_data = VarData.merge( # type: ignore + connect_errors._var_data, + VarData( + imports={ + "react": [ + imports.ImportVar(tag="useEffect"), + imports.ImportVar(tag="useState"), + ], + **target_url._get_imports(), + } + ), + ) + return [ + Hooks.EVENTS, + hook, # type: ignore + ] + + class ConnectionBanner(Component): """A connection banner component.""" @@ -162,8 +230,8 @@ class WifiOffPulse(Icon): size=props.pop("size", 32), z_index=props.pop("z_index", 9999), position=props.pop("position", "fixed"), - bottom=props.pop("botton", "30px"), - right=props.pop("right", "30px"), + bottom=props.pop("botton", "33px"), + right=props.pop("right", "33px"), animation=Var.create(f"${{pulse}} 1s infinite", _var_is_string=True), **props, ) @@ -205,6 +273,7 @@ class ConnectionPulser(Div): has_connection_errors, WifiOffPulse.create(**props), ), + title=f"Connection Error: {connection_error}", position="fixed", width="100vw", height="0", diff --git a/reflex/components/core/banner.pyi b/reflex/components/core/banner.pyi index 43fc53e29..64f9761f9 100644 --- a/reflex/components/core/banner.pyi +++ b/reflex/components/core/banner.pyi @@ -20,11 +20,15 @@ from reflex.components.radix.themes.components.dialog import ( ) from reflex.components.radix.themes.layout import Flex from reflex.components.radix.themes.typography.text import Text +from reflex.components.sonner.toast import Toaster, ToastProps from reflex.constants import Dirs, Hooks, Imports +from reflex.constants.compiler import CompileVars from reflex.utils import imports +from reflex.utils.serializers import serialize from reflex.vars import Var, VarData connect_error_var_data: VarData +connect_errors: Var connection_error: Var connection_errors_count: Var has_connection_errors: Var @@ -99,6 +103,132 @@ class WebsocketTargetURL(Bare): def default_connection_error() -> list[str | Var | Component]: ... +class ConnectionToaster(Toaster): + def add_hooks(self) -> list[str]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + theme: Optional[Union[Var[str], str]] = None, + rich_colors: Optional[Union[Var[bool], bool]] = None, + expand: Optional[Union[Var[bool], bool]] = None, + visible_toasts: Optional[Union[Var[int], int]] = None, + position: Optional[ + Union[ + Var[ + Literal[ + "top-left", + "top-center", + "top-right", + "bottom-left", + "bottom-center", + "bottom-right", + ] + ], + Literal[ + "top-left", + "top-center", + "top-right", + "bottom-left", + "bottom-center", + "bottom-right", + ], + ] + ] = None, + close_button: Optional[Union[Var[bool], bool]] = None, + offset: Optional[Union[Var[str], str]] = None, + dir: Optional[Union[Var[str], str]] = None, + hotkey: Optional[Union[Var[str], str]] = None, + invert: Optional[Union[Var[bool], bool]] = None, + toast_options: Optional[Union[Var[ToastProps], ToastProps]] = None, + gap: Optional[Union[Var[int], int]] = None, + loading_icon: Optional[Union[Var[Icon], Icon]] = None, + pause_when_page_is_hidden: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, + on_blur: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_click: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_context_menu: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_double_click: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_focus: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mount: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_down: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_enter: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_leave: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_move: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_out: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_over: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_mouse_up: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_scroll: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + on_unmount: Optional[ + Union[EventHandler, EventSpec, list, function, BaseVar] + ] = None, + **props + ) -> "ConnectionToaster": + """Create the component. + + Args: + *children: The children of the component. + theme: the theme of the toast + rich_colors: whether to show rich colors + expand: whether to expand the toast + visible_toasts: the number of toasts that are currently visible + position: the position of the toast + close_button: whether to show the close button + offset: offset of the toast + dir: directionality of the toast (default: ltr) + hotkey: Keyboard shortcut that will move focus to the toaster area. + invert: Dark toasts in light mode and vice versa. + toast_options: These will act as default options for all toasts. See toast() for all available options. + gap: Gap between toasts when expanded + loading_icon: Changes the default loading icon + pause_when_page_is_hidden: Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The props of the component. + + Returns: + The component. + """ + ... + class ConnectionBanner(Component): @overload @classmethod diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index d820af2da..648b0db9c 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Literal, Optional +from typing import Any, Literal, Optional, Union from reflex.base import Base from reflex.components.component import Component, ComponentNamespace @@ -74,7 +74,7 @@ class ToastProps(PropsBase): """Props for the toast component.""" # Toast's description, renders underneath the title. - description: Optional[str] + description: Optional[Union[str, Var]] # Whether to show the close button. close_button: Optional[bool] diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index 5bd6cdeb4..6bc5ab2b5 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -7,7 +7,7 @@ from typing import Any, Dict, Literal, Optional, Union, overload from reflex.vars import Var, BaseVar, ComputedVar from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style -from typing import Any, Literal, Optional +from typing import Any, Literal, Optional, Union from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon @@ -37,7 +37,7 @@ class ToastAction(Base): def serialize_action(action: ToastAction) -> dict: ... class ToastProps(PropsBase): - description: Optional[str] + description: Optional[Union[str, Var]] close_button: Optional[bool] invert: Optional[bool] important: Optional[bool] diff --git a/reflex/experimental/client_state.py b/reflex/experimental/client_state.py index d0028991c..93405b29f 100644 --- a/reflex/experimental/client_state.py +++ b/reflex/experimental/client_state.py @@ -110,7 +110,7 @@ class ClientStateVar(Var): f"{_client_state_ref(setter_name)} = {setter_name}": None, }, imports={ - "react": {ImportVar(tag="useState", install=False)}, + "react": [ImportVar(tag="useState", install=False)], f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")], }, ), From 590d86ebf451fadff95215d4357d22769be39249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Sat, 18 May 2024 01:29:38 +0200 Subject: [PATCH 017/496] update README to use 0.5.0 (#3313) --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 675f29cc3..c6270f120 100644 --- a/README.md +++ b/README.md @@ -120,14 +120,15 @@ def index(): on_blur=State.set_prompt, width="25em", ), - rx.button("Generate Image", on_click=State.get_image, width="25em"), + rx.button( + "Generate Image", + on_click=State.get_image, + width="25em", + loading=State.processing + ), rx.cond( - State.processing, - rx.chakra.circular_progress(is_indeterminate=True), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), + State.complete, + rx.image(src=State.image_url, width="20em"), ), align="center", ), @@ -185,7 +186,7 @@ class State(rx.State): The state defines all the variables (called vars) in an app that can change and the functions that change them. -Here the state is comprised of a `prompt` and `image_url`. There are also the booleans `processing` and `complete` to indicate when to show the circular progress and image. +Here the state is comprised of a `prompt` and `image_url`. There are also the booleans `processing` and `complete` to indicate when to disable the button (during image generation) and when to show the resulting image. ### **Event Handlers** From b614e38047c1757328604203b013a1ad82f38971 Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Sun, 19 May 2024 12:13:24 -0700 Subject: [PATCH 018/496] Add some minimal validation of pyproject.toml (#3339) --- reflex/custom_components/custom_components.py | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index ec35fb5c4..7ee24ab0c 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -667,6 +667,9 @@ def publish( # Validate the credentials. username, password = _validate_credentials(username, password, token) + # Minimal Validation of the pyproject.toml. + _min_validate_project_info() + # Get the version to publish from the pyproject.toml. version_to_publish = _get_version_to_publish() @@ -735,6 +738,34 @@ def _process_entered_list(input: str | None) -> list | None: return [t.strip() for t in (input or "").split(",") if t if input] or None +def _min_validate_project_info(): + """Ensures minimal project information in the pyproject.toml file. + + Raises: + Exit: If the pyproject.toml file is ill-formed. + """ + pyproject_toml = _get_package_config() + + project = pyproject_toml.get("project") + if project is None: + console.error( + f"The project section is not found in {CustomComponents.PYPROJECT_TOML}" + ) + raise typer.Exit(code=1) + + if not project.get("name"): + console.error( + f"The project name is not found in {CustomComponents.PYPROJECT_TOML}" + ) + raise typer.Exit(code=1) + + if not project.get("version"): + console.error( + f"The project version is not found in {CustomComponents.PYPROJECT_TOML}" + ) + raise typer.Exit(code=1) + + def _validate_project_info(): """Validate the project information in the pyproject.toml file. @@ -742,18 +773,10 @@ def _validate_project_info(): Exit: If the pyproject.toml file is ill-formed. """ pyproject_toml = _get_package_config() - - try: - project = pyproject_toml.get("project", {}) - if not project: - console.error("The project section not found in pyproject.toml") - raise typer.Exit(code=1) - console.print( - f'Double check the information before publishing: {project["name"]} version {project["version"]}' - ) - except KeyError as ke: - console.error(f"The pyproject.toml is possibly ill-formed due to {ke}") - raise typer.Exit(code=1) from ke + project = pyproject_toml["project"] + console.print( + f'Double check the information before publishing: {project["name"]} version {project["version"]}' + ) console.print("Update or enter to keep the current information.") project["description"] = console.ask( From 656914edefa7658c76e3d350abe7e9223db313bb Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Sun, 19 May 2024 21:14:46 +0200 Subject: [PATCH 019/496] fix rx.cond with ComputedVars and use union type (#3336) --- reflex/components/core/cond.py | 12 ++++++++--- tests/components/core/test_cond.py | 33 +++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/reflex/components/core/cond.py b/reflex/components/core/cond.py index 9ace92b98..fcc12bc51 100644 --- a/reflex/components/core/cond.py +++ b/reflex/components/core/cond.py @@ -1,7 +1,8 @@ """Create a list of components from an iterable.""" + from __future__ import annotations -from typing import Any, Dict, Optional, overload +from typing import Any, Dict, Optional, Union, overload from reflex.components.base.fragment import Fragment from reflex.components.component import BaseComponent, Component, MemoizationLeaf @@ -10,7 +11,7 @@ from reflex.constants import Dirs from reflex.constants.colors import Color from reflex.style import LIGHT_COLOR_MODE, color_mode from reflex.utils import format, imports -from reflex.vars import BaseVar, Var, VarData +from reflex.vars import Var, VarData _IS_TRUE_IMPORT = { f"/{Dirs.STATE_PATH}": [imports.ImportVar(tag="isTrue")], @@ -171,6 +172,11 @@ def cond(condition: Any, c1: Any, c2: Any = None): c2 = create_var(c2) var_datas.extend([c1._var_data, c2._var_data]) + c1_type = c1._var_type if isinstance(c1, Var) else type(c1) + c2_type = c2._var_type if isinstance(c2, Var) else type(c2) + + var_type = c1_type if c1_type == c2_type else Union[c1_type, c2_type] + # Create the conditional var. return cond_var._replace( _var_name=format.format_cond( @@ -179,7 +185,7 @@ def cond(condition: Any, c1: Any, c2: Any = None): false_value=c2, is_prop=True, ), - _var_type=c1._var_type if isinstance(c1, BaseVar) else type(c1), + _var_type=var_type, _var_is_local=False, _var_full_name_needs_state_prefix=False, merge_var_data=VarData.merge(*var_datas), diff --git a/tests/components/core/test_cond.py b/tests/components/core/test_cond.py index a7604fb9a..4bfa902af 100644 --- a/tests/components/core/test_cond.py +++ b/tests/components/core/test_cond.py @@ -1,13 +1,14 @@ import json -from typing import Any +from typing import Any, Union import pytest from reflex.components.base.fragment import Fragment from reflex.components.core.cond import Cond, cond from reflex.components.radix.themes.typography.text import Text -from reflex.state import BaseState -from reflex.vars import Var +from reflex.state import BaseState, State +from reflex.utils.format import format_state_name +from reflex.vars import BaseVar, Var, computed_var @pytest.fixture @@ -118,3 +119,29 @@ def test_cond_no_else(): # Props do not support the use of cond without else with pytest.raises(ValueError): cond(True, "hello") # type: ignore + + +def test_cond_computed_var(): + """Test if cond works with computed vars.""" + + class CondStateComputed(State): + @computed_var + def computed_int(self) -> int: + return 0 + + @computed_var + def computed_str(self) -> str: + return "a string" + + comp = cond(True, CondStateComputed.computed_int, CondStateComputed.computed_str) + + # TODO: shouln't this be a ComputedVar? + assert isinstance(comp, BaseVar) + + state_name = format_state_name(CondStateComputed.get_full_name()) + assert ( + str(comp) + == f"{{isTrue(true) ? {state_name}.computed_int : {state_name}.computed_str}}" + ) + + assert comp._var_type == Union[int, str] From ec72448b8b570d05f44724e00535a5359930b8a2 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Mon, 20 May 2024 16:55:41 -0700 Subject: [PATCH 020/496] Catch more errors in frontend/backend (#3346) --- reflex/app.py | 7 +++-- reflex/app_module_for_backend.py | 3 +++ reflex/state.py | 4 +-- reflex/utils/prerequisites.py | 7 +++-- reflex/utils/telemetry.py | 46 ++++++++++++++++++++++++-------- tests/test_telemetry.py | 4 +-- 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 4a7c60e2e..72a09462a 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1068,13 +1068,12 @@ async def process( client_ip: The client_ip. Raises: - ReflexError: If a reflex specific error occurs during processing the event. + Exception: If a reflex specific error occurs during processing the event. Yields: The state updates after processing the event. """ from reflex.utils import telemetry - from reflex.utils.exceptions import ReflexError try: # Add request data to the state. @@ -1118,8 +1117,8 @@ async def process( # Yield the update. yield update - except ReflexError as ex: - telemetry.send("error", context="backend", detail=str(ex)) + except Exception as ex: + telemetry.send_error(ex, context="backend") raise diff --git a/reflex/app_module_for_backend.py b/reflex/app_module_for_backend.py index 8d97725f9..17cd1973c 100644 --- a/reflex/app_module_for_backend.py +++ b/reflex/app_module_for_backend.py @@ -4,12 +4,14 @@ Only the app attribute is explicitly exposed. from concurrent.futures import ThreadPoolExecutor from reflex import constants +from reflex.utils import telemetry from reflex.utils.exec import is_prod_mode from reflex.utils.prerequisites import get_app if "app" != constants.CompileVars.APP: raise AssertionError("unexpected variable name for 'app'") +telemetry.send("compile") app_module = get_app(reload=False) app = getattr(app_module, constants.CompileVars.APP) # For py3.8 and py3.9 compatibility when redis is used, we MUST add any decorator pages @@ -29,5 +31,6 @@ del app_module del compile_future del get_app del is_prod_mode +del telemetry del constants del ThreadPoolExecutor diff --git a/reflex/state.py b/reflex/state.py index 86a222b66..ba87029c8 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1476,7 +1476,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): StateUpdate object """ from reflex.utils import telemetry - from reflex.utils.exceptions import ReflexError # Get the function to process the event. fn = functools.partial(handler.fn, state) @@ -1516,8 +1515,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): except Exception as ex: error = traceback.format_exc() print(error) - if isinstance(ex, ReflexError): - telemetry.send("error", context="backend", detail=str(ex)) + telemetry.send_error(ex, context="backend") yield state._as_state_update( handler, window_alert("An error occurred. See logs for details."), diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 1852f0434..f77e1d63c 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -233,9 +233,8 @@ def get_app(reload: bool = False) -> ModuleType: Raises: RuntimeError: If the app name is not set in the config. - exceptions.ReflexError: Reflex specific errors. """ - from reflex.utils import exceptions, telemetry + from reflex.utils import telemetry try: os.environ[constants.RELOAD_CONFIG] = str(reload) @@ -259,8 +258,8 @@ def get_app(reload: bool = False) -> ModuleType: importlib.reload(app) return app - except exceptions.ReflexError as ex: - telemetry.send("error", context="frontend", detail=str(ex)) + except Exception as ex: + telemetry.send_error(ex, context="frontend") raise diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index f7398f01a..e10ad94ee 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import multiprocessing import platform @@ -157,17 +158,7 @@ def _send_event(event_data: dict) -> bool: return False -def send(event: str, telemetry_enabled: bool | None = None, **kwargs) -> bool: - """Send anonymous telemetry for Reflex. - - Args: - event: The event name. - telemetry_enabled: Whether to send the telemetry (If None, get from config). - kwargs: Additional data to send with the event. - - Returns: - Whether the telemetry was sent successfully. - """ +def _send(event, telemetry_enabled, **kwargs): from reflex.config import get_config # Get the telemetry_enabled from the config if it is not specified. @@ -182,3 +173,36 @@ def send(event: str, telemetry_enabled: bool | None = None, **kwargs) -> bool: if not event_data: return False return _send_event(event_data) + + +def send(event: str, telemetry_enabled: bool | None = None, **kwargs): + """Send anonymous telemetry for Reflex. + + Args: + event: The event name. + telemetry_enabled: Whether to send the telemetry (If None, get from config). + kwargs: Additional data to send with the event. + """ + + async def async_send(event, telemetry_enabled, **kwargs): + return _send(event, telemetry_enabled, **kwargs) + + try: + # Within an event loop context, send the event asynchronously. + asyncio.create_task(async_send(event, telemetry_enabled, **kwargs)) + except RuntimeError: + # If there is no event loop, send the event synchronously. + _send(event, telemetry_enabled, **kwargs) + + +def send_error(error: Exception, context: str): + """Send an error event. + + Args: + error: The error to send. + context: The context of the error (e.g. "frontend" or "backend") + + Returns: + Whether the telemetry was sent successfully. + """ + return send("error", detail=type(error).__name__, context=context) diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 58786c67b..3a9eb17d0 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -29,7 +29,7 @@ def test_telemetry(): def test_disable(): """Test that disabling telemetry works.""" - assert not telemetry.send("test", telemetry_enabled=False) + assert not telemetry._send("test", telemetry_enabled=False) @pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"]) @@ -43,7 +43,7 @@ def test_send(mocker, event): ) mocker.patch("platform.platform", return_value="Mocked Platform") - telemetry.send(event, telemetry_enabled=True) + telemetry._send(event, telemetry_enabled=True) httpx.post.assert_called_once() if telemetry.get_os() == "Windows": open.assert_called_with(".web\\reflex.json", "r") From ff985aee226a85b1134171e6e0c087ce16a44482 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Tue, 21 May 2024 12:34:25 -0700 Subject: [PATCH 021/496] Suppress runtime warnings (#3354) --- reflex/utils/telemetry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index e10ad94ee..e027ed81a 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio import multiprocessing import platform +import warnings try: from datetime import UTC, datetime @@ -192,6 +193,7 @@ def send(event: str, telemetry_enabled: bool | None = None, **kwargs): asyncio.create_task(async_send(event, telemetry_enabled, **kwargs)) except RuntimeError: # If there is no event loop, send the event synchronously. + warnings.filterwarnings("ignore", category=RuntimeWarning) _send(event, telemetry_enabled, **kwargs) From d50be7eab1470a385e0598504507d94336115b9e Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Tue, 21 May 2024 14:43:49 -0700 Subject: [PATCH 022/496] Use twine environment variables if set (#3353) --- reflex/custom_components/custom_components.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index 7ee24ab0c..a47de6feb 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -616,15 +616,17 @@ def publish( help="The API token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time", ), username: Optional[str] = typer.Option( - None, + os.getenv("TWINE_USERNAME"), "-u", "--username", + show_default="TWINE_USERNAME environment variable value if set", help="The username to use for authentication on python package repository. Username and password must both be provided.", ), password: Optional[str] = typer.Option( - None, + os.getenv("TWINE_PASSWORD"), "-p", "--password", + show_default="TWINE_PASSWORD environment variable value if set", help="The password to use for authentication on python package repository. Username and password must both be provided.", ), build: bool = typer.Option( From f961e2daa05119eb0f22321c4333366d5a95b42b Mon Sep 17 00:00:00 2001 From: seewind Date: Wed, 22 May 2024 05:50:36 +0800 Subject: [PATCH 023/496] #3321: Update Chinese README.md (#3350) --- docs/zh/zh_cn/README.md | 108 +++++++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 40 deletions(-) diff --git a/docs/zh/zh_cn/README.md b/docs/zh/zh_cn/README.md index 8c1fa8572..37e8286f4 100644 --- a/docs/zh/zh_cn/README.md +++ b/docs/zh/zh_cn/README.md @@ -8,25 +8,36 @@
-**✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.** - - +### **✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨** [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) ![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - --- + [English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) + --- + +# Reflex + +Reflex 是一个使用纯Python构建全栈web应用的库。 + +关键特性: +* **纯Python** - 前端、后端开发全都使用Python,不需要学习Javascript。 +* **完整的灵活性** - Reflex很容易上手, 并且也可以扩展到复杂的应用程序。 +* **立即部署** - 构建后,使用[单个命令](https://reflex.dev/docs/hosting/deploy-quick-start/)就能部署应用程序;或者也可以将其托管在您自己的服务器上。 + +请参阅我们的[架构页](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)了解Reflex如何工作。 + ## ⚙️ 安装 打开一个终端并且运行(要求Python3.8+): -``` +```bash pip install reflex ``` @@ -36,7 +47,7 @@ pip install reflex 通过创建一个新项目来测试是否安装成功(请把 my_app_name 替代为您的项目名字): -``` +```bash mkdir my_app_name cd my_app_name reflex init @@ -45,6 +56,7 @@ reflex init 这段命令会在新文件夹初始化一个应用程序模板. 您可以在开发者模式下运行这个应用程序: + ```bash reflex run ``` @@ -55,7 +67,7 @@ reflex run ## 🫧 范例 -让我们来看一个例子: 创建一个使用 DALL·E 进行图像生成的图形界面.为了保持范例简单,我们只使用 OpenAI API,但是您可以将其替换成本地端的 ML 模型. +让我们来看一个例子: 创建一个使用 [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node) 进行图像生成的图形界面.为了保持范例简单,我们只使用 OpenAI API,但是您可以将其替换成本地端的 ML 模型.   @@ -67,14 +79,19 @@ reflex run 这是这个范例的完整代码,只需要一个 Python 文件就可以完成! + + + ```python import reflex as rx import openai -openai.api_key = "YOUR_API_KEY" +openai_client = openai.OpenAI() + class State(rx.State): """The app state.""" + prompt = "" image_url = "" processing = False @@ -87,33 +104,33 @@ class State(rx.State): self.processing, self.complete = True, False yield - response = openai.Image.create(prompt=self.prompt, n=1, size="1024x1024") - self.image_url = response["data"][0]["url"] + response = openai_client.images.generate( + prompt=self.prompt, n=1, size="1024x1024" + ) + self.image_url = response.data[0].url self.processing, self.complete = False, True - + def index(): return rx.center( rx.vstack( - rx.heading("DALL·E"), - rx.input(placeholder="Enter a prompt", on_blur=State.set_prompt), + rx.heading("DALL-E", font_size="1.5em"), + rx.input( + placeholder="Enter a prompt..", + on_blur=State.set_prompt, + width="25em", + ), rx.button( - "Generate Image", + "Generate Image", on_click=State.get_image, - is_loading=State.processing, - width="100%", + width="25em", + loading=State.processing ), rx.cond( State.complete, - rx.image( - src=State.image_url, - height="25em", - width="25em", - ) + rx.image(src=State.image_url, width="20em"), ), - padding="2em", - shadow="lg", - border_radius="lg", + align="center", ), width="100%", height="100vh", @@ -121,11 +138,20 @@ def index(): # Add state and page to the app. app = rx.App() -app.add_page(index, title="reflex:DALL·E") +app.add_page(index, title="Reflex:DALL-E") ``` + + + + ## 让我们分解以上步骤. +
+Explaining the differences between backend and frontend parts of the DALL-E app. +
+ + ### **Reflex UI** 让我们从UI开始. @@ -142,7 +168,7 @@ def index(): 我们用不同的组件比如 `center`, `vstack`, `input`, 和 `button` 来创建前端, 组件之间可以相互嵌入,来创建复杂的布局. 并且您可以使用关键字参数来使用 CSS 的全部功能. -Reflex 拥有 [60+ 个内置组件](https://reflex.dev/docs/library) 来帮助您开始创建应用程序. 我们正在积极添加组件, 但是您也可以 [创建自己的组件](https://reflex.dev/docs/wrapping-react/overview/). +Reflex 拥有 [60+ 个内置组件](https://reflex.dev/docs/library) 来帮助您开始创建应用程序. 我们正在积极添加组件, 但是您也可以容易的 [创建自己的组件](https://reflex.dev/docs/wrapping-react/overview/). ### **State** @@ -155,12 +181,12 @@ class State(rx.State): image_url = "" processing = False complete = False -``` +``` State定义了所有可能会发生变化的变量(称为 vars)以及能够改变这些变量的函数. -在这个范例中,State由 prompt 和 image_url 组成.此外,State还包含有两个布尔值 processing 和 complete,用于指示何时显示循环进度指示器和图像. +在这个范例中,State由 `prompt` 和 `image_url` 组成.此外,State还包含有两个布尔值 `processing` 和 `complete`,用于指示何时显示循环进度指示器和图像. ### **Event Handlers** @@ -172,14 +198,16 @@ def get_image(self): self.processing, self.complete = True, False yield - response = openai.Image.create(prompt=self.prompt, n=1, size="1024x1024") - self.image_url = response["data"][0]["url"] + response = openai_client.images.generate( + prompt=self.prompt, n=1, size="1024x1024" + ) + self.image_url = response.data[0].url self.processing, self.complete = False, True ``` 在 State 中,我们定义了称为事件处理器(event handlers)的函数,用于改变状态变量(state vars).在Reflex中,事件处理器是我们可以修改状态的方式.它们可以作为对用户操作的响应而被调用,例如点击一个按钮或在文本框中输入.这些操作被称为事件. -我们的DALL·E应用有一个事件处理器,名为 get_image,它用于从OpenAI API获取图像.在事件处理器中使用 yield 将导致UI进行更新.否则,UI将在事件处理器结束时进行更新. +我们的DALL·E应用有一个事件处理器,名为 `get_image`,它用于从OpenAI API获取图像.在事件处理器中使用 `yield` 将导致UI进行更新.否则,UI将在事件处理器结束时进行更新. ### **Routing** @@ -206,19 +234,13 @@ app.add_page(index, title="DALL-E") - ## ✅ Reflex 的状态 Reflex 于 2022 年 12 月以Pynecone的名称推出. -在2023年7月, 我们处于 **Public Beta** 阶段. +截至2024年2月,我们的托管服务处于alpha测试阶段!在此期间,任何人都可以免费部署他们的应用程序。请查看我们的[路线图](https://github.com/reflex-dev/reflex/issues/2727)以了解我们的计划。 -- :white_check_mark: **Public Alpha**: 任何人都可以安装与使用 Reflex,或许包含问题, 但我们正在积极的解决他们. -- :large_orange_diamond: **Public Beta**: 对于非软件产品来说足够稳定. -- **Public Hosting Beta**: _Optionally_, 部属跟托管您的 Reflex! -- **Public**: 这版本的 Reflex 是可用于软件产品的. - -Reflex 每周都有新功能和发布新版本! 确保您按下 :star: 和 :eyes: watch 这个 repository 来确保知道最新信息. +Reflex 每周都有新功能和发布新版本! 确保您按下 :star: 收藏和 :eyes: 关注 这个 仓库来确保知道最新信息. ## 贡献 @@ -226,10 +248,16 @@ Reflex 每周都有新功能和发布新版本! 确保您按下 :star: 和 :eyes - **加入我们的 Discord**: 我们的 [Discord](https://discord.gg/T5WSbC2YtQ) 是帮助您加入 Reflex 项目和讨论或贡献最棒的地方. - **GitHub Discussions**: 一个来讨论您想要添加的功能或是需要澄清的事情的好地方. -- **GitHub Issues**: 报告错误的绝佳地方,另外您可以试着解决一些 issue 和送出 PR. +- **GitHub Issues**: [报告错误](https://github.com/reflex-dev/reflex/issues)的绝佳地方,另外您可以试着解决一些 issue 和送出 PR. 我们正在积极寻找贡献者,无关您的技能或经验水平. + +## 感谢我们所有的贡献者: + + + + ## 授权 Reflex 是一个开源项目,使用 [Apache License 2.0](LICENSE) 授权. From d40d992670063abac2b512e4fd2443c5859d7a70 Mon Sep 17 00:00:00 2001 From: Santiago Botero <98826652+boterop@users.noreply.github.com> Date: Tue, 21 May 2024 16:51:50 -0500 Subject: [PATCH 024/496] Update Spanish README.md (#3330) --- docs/es/README.md | 136 +++++++++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 62 deletions(-) diff --git a/docs/es/README.md b/docs/es/README.md index 40b3313ec..06ddbca8d 100644 --- a/docs/es/README.md +++ b/docs/es/README.md @@ -1,5 +1,5 @@ ```diff -+ ¿Buscando Pynecone? Estas en el repositorio correcto. Pynecone ha sido renomabrado a Reflex. + ++ ¿Buscando Pynecone? Estás en el repositorio correcto. Pynecone ha sido renombrado a Reflex. + ```
@@ -8,28 +8,42 @@
-### **✨ Aplicaciones web personalizables y eficaces en Python puro. Despliega tú aplicación en segundos. ✨** +### **✨ Aplicaciones web personalizables y eficaces en Python puro. Despliega tu aplicación en segundos. ✨** [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) +![Pruebas](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg) +![Versiones](https://img.shields.io/pypi/pyversions/reflex.svg) +[![Documentación](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
--- -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) + +[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) + --- + +# Reflex + +Reflex es una biblioteca para construir aplicaciones web full-stack en Python puro. + +Características clave: +* **Python puro** - Escribe el frontend y backend de tu aplicación en Python, sin necesidad de aprender JavaScript. +* **Flexibilidad total** - Reflex es fácil para empezar, pero también puede escalar a aplicaciones complejas. +* **Despliegue instantáneo** - Después de construir, despliega tu aplicación con un [solo comando](https://reflex.dev/docs/hosting/deploy-quick-start/) u hospédala en tu propio servidor. + +Consulta nuestra [página de arquitectura](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) para aprender cómo funciona Reflex en detalle. + ## ⚙️ Instalación -Abra un terminal y ejecute (Requiere Python 3.7+): +Abra un terminal y ejecute (Requiere Python 3.8+): ```bash pip install reflex ``` -## 🥳 Crea tú primera aplicación +## 🥳 Crea tu primera aplicación -Al instalar `reflex` tambien se instala la herramienta de línea de comandos `reflex`. +Al instalar `reflex` también se instala la herramienta de línea de comandos `reflex`. Compruebe que la instalación se ha realizado correctamente creando un nuevo proyecto. (Sustituye `my_app_name` por el nombre de tu proyecto): @@ -39,7 +53,7 @@ cd my_app_name reflex init ``` -Este comando inicializa una aplicación de ejemplo (plantilla) en tu nuevo directorio. +Este comando inicializa una plantilla en tu nuevo directorio. Puedes iniciar esta aplicación en modo de desarrollo: @@ -54,12 +68,12 @@ Ahora puede modificar el código fuente en `my_app_name/my_app_name.py`. Reflex ## 🫧 Ejemplo de una Aplicación -Veamos un ejemplo: crearemos una UI de generación de imágenes en torno a DALL-E. Para simplificar, solo llamamos a la API de OpenAI, pero podrías reeemplazar esto con un modelo ML ejecutado localmente. +Veamos un ejemplo: crearemos una UI de generación de imágenes en torno a [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Para simplificar, solo llamamos a la [API de OpenAI](https://platform.openai.com/docs/api-reference/authentication), pero podrías reemplazar esto con un modelo ML ejecutado localmente.  
-A frontend wrapper for DALL·E, shown in the process of generating an image. +Un envoltorio frontend para DALL·E, mostrado en el proceso de generar una imagen.
  @@ -70,61 +84,65 @@ Aquí está el código completo para crear esto. ¡Todo esto se hace en un archi import reflex as rx import openai -openai.api_key = "YOUR_API_KEY" +openai_client = openai.OpenAI() class State(rx.State): - """The app state.""" + """El estado de la aplicación""" prompt = "" image_url = "" processing = False complete = False def get_image(self): - """Get the image from the prompt.""" + """Obtiene la imagen desde la consulta.""" if self.prompt == "": return rx.window_alert("Prompt Empty") self.processing, self.complete = True, False yield - response = openai.Image.create(prompt=self.prompt, n=1, size="1024x1024") - self.image_url = response["data"][0]["url"] + response = openai_client.images.generate( + prompt=self.prompt, n=1, size="1024x1024" + ) + self.image_url = response.data[0].url self.processing, self.complete = False, True def index(): return rx.center( rx.vstack( - rx.heading("DALL·E"), - rx.input(placeholder="Enter a prompt", on_blur=State.set_prompt), + rx.heading("DALL-E", font_size="1.5em"), + rx.input( + placeholder="Enter a prompt..", + on_blur=State.set_prompt, + width="25em", + ), rx.button( - "Generate Image", + "Generate Image", on_click=State.get_image, - is_loading=State.processing, - width="100%", + width="25em", + loading=State.processing ), rx.cond( State.complete, - rx.image( - src=State.image_url, - height="25em", - width="25em", - ) + rx.image(src=State.image_url, width="20em"), ), - padding="2em", - shadow="lg", - border_radius="lg", + align="center", ), width="100%", height="100vh", ) -# Add state and page to the app. +# Agrega el estado y la pagina a la aplicación app = rx.App() -app.add_page(index, title="reflex:DALL·E") +app.add_page(index, title="Reflex:DALL-E") ``` ## Vamos a analizarlo. +
+Explicando las diferencias entre las partes del backend y frontend de la aplicación DALL-E. +
+ ### **Reflex UI** Empezemos por la interfaz de usuario (UI). @@ -138,17 +156,17 @@ def index(): Esta función `index` define el frontend de la aplicación. -Utilizamos diferentes componentes como `center`, `vstack`, `input`, y `button` para construir el frontend. Los componentes pueden anidarse unos dentro de otros para crear diseños complejos. Además, puedes usar argumentos (keyword args) para darles estilo con toda la potencia de CSS. +Utilizamos diferentes componentes como `center`, `vstack`, `input`, y `button` para construir el frontend. Los componentes pueden anidarse unos dentro de otros para crear diseños complejos. Además, puedes usar argumentos de tipo keyword para darles estilo con toda la potencia de CSS. -Reflex viene con [mas de 60+ componentes incorporados](https://reflex.dev/docs/library) para ayudarle a empezar. Estamos añadiendo activamente más componentes y es fácil [crear sus propios componentes](https://reflex.dev/docs/advanced-guide/wrapping-react). +Reflex viene con [mas de 60 componentes incorporados](https://reflex.dev/docs/library) para ayudarle a empezar. Estamos añadiendo activamente más componentes y es fácil [crear sus propios componentes](https://reflex.dev/docs/wrapping-react/overview/). -### **State** +### **Estado** -Reflex representa su UI en función de su estado (State). +Reflex representa su UI como una función de su estado (State). ```python class State(rx.State): - """The app state.""" + """El estado de la aplicación""" prompt = "" image_url = "" processing = False @@ -157,28 +175,30 @@ class State(rx.State): El estado (State) define todas las variables (llamadas vars) de una aplicación que pueden cambiar y las funciones que las modifican. -Aquí el estado (State) se compone de `prompt` e `image_url`. También están los booleanos `processing` y `complete` para poder indicar cuándo mostrar el progreso circular y la imagen. +Aquí el estado se compone de `prompt` e `image_url`. También están los booleanos `processing` y `complete` para indicar cuando se deshabilite el botón (durante la generación de la imagen) y cuando se muestre la imagen resultante. -### **Event Handlers** +### **Manejadores de Evento** ```python def get_image(self): - """Get the image from the prompt.""" + """Obtiene la imagen desde la consulta.""" if self.prompt == "": return rx.window_alert("Prompt Empty") self.processing, self.complete = True, False yield - response = openai.Image.create(prompt=self.prompt, n=1, size="1024x1024") - self.image_url = response["data"][0]["url"] + response = openai_client.images.generate( + prompt=self.prompt, n=1, size="1024x1024" + ) + self.image_url = response.data[0].url self.processing, self.complete = False, True ``` -Dentro del estado (State), definimos funciones llamadas "event handlers" que cambian los 'state vars'. Event handlers, son la manera que podemos modificar el 'state' en Reflex. Pueden ser activados en respuesta a las acciones del usuario, como hacer clic en un botón o escribir en un cuadro de texto. Estas acciones se llaman eventos 'events'. +Dentro del estado, definimos funciones llamadas manejadores de eventos que cambian las variables de estado. Los Manejadores de Evento son la manera que podemos modificar el estado en Reflex. Pueden ser activados en respuesta a las acciones del usuario, como hacer clic en un botón o escribir en un cuadro de texto. Estas acciones se llaman eventos. -Nuestra aplicación DALL·E. tiene un controlador de eventos "event handler", `get_image` que recibe esta imagen del OpenAI API. El uso de `yield` en medio de un controlador de eventos "event handler" hará que la UI se actualice. De lo contrario, la interfaz se actualizará al final del controlador de eventos "event handler". +Nuestra aplicación DALL·E tiene un manipulador de eventos, `get_image` que recibe esta imagen del OpenAI API. El uso de `yield` en medio de un manipulador de eventos hará que la UI se actualice. De lo contrario, la interfaz se actualizará al final del manejador de eventos. -### **Routing** +### **Enrutamiento** Por último, definimos nuestra app. @@ -198,36 +218,28 @@ Puedes crear una aplicación multipágina añadiendo más páginas.
-📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Biblioteca de Componentes](https://reflex.dev/docs/library)   |   🖼️ [Galería](https://reflex.dev/docs/gallery)   |   🛸 [Hospedaje](https://reflex.dev/docs/hosting/deploy)   +📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Librería de componentes](https://reflex.dev/docs/library)   |   🖼️ [Galería](https://reflex.dev/docs/gallery)   |   🛸 [Despliegue](https://reflex.dev/docs/hosting/deploy-quick-start)  
+## ✅ Estado +Reflex se lanzó en diciembre de 2022 con el nombre de Pynecone. +¡Desde febrero de 2024, nuestro servicio de alojamiento está en fase alfa! Durante este tiempo, cualquiera puede implementar sus aplicaciones de forma gratuita. Consulta nuestra [hoja de ruta](https://github.com/reflex-dev/reflex/issues/2727) para ver qué está planeado. -## ✅ Estatus +¡Reflex tiene nuevas versiones y características cada semana! Asegúrate de :star: marcar como favorito y :eyes: seguir este repositorio para mantenerte actualizado. -Reflex se lanzó en Diciembre de 2022 con el nombre Pynecone. - -A partir de julio de 2023, nos encontramos en la etapa de **Beta Pública**. - -- :white_check_mark: **Alpha Pública**: Cualquier persona puede instalar y usar Reflex. Puede haber problemas, pero estamos trabajando activamente para resolverlos. -- :large_orange_diamond: **Beta Pública**: Suficientemente estable para casos de uso no empresariales. -- **Beta de Hospedaje Público**: ¡_Opcionalmente_, despliega y hospeda tus aplicaciónes en Reflex! -- **Público**: Reflex está listo para producción. - -¡Reflex tiene nuevas versiones y características que se lanzan cada semana! Aseguraté de darnos una :star: estrella y :eyes: revisa este repositorio para mantenerte actualizado. - -## Contribuyendo +## Contribuciones ¡Aceptamos contribuciones de cualquier tamaño! A continuación encontrará algunas buenas formas de iniciarse en la comunidad Reflex. - **Únete a nuestro Discord**: Nuestro [Discord](https://discord.gg/T5WSbC2YtQ) es el mejor lugar para obtener ayuda en su proyecto Reflex y discutir cómo puedes contribuir. -- **Discusiones de GitHub**: Una excelente manera de hablar sobre las características que deseas agregar o las cosas que te resusltan confusas o necesitan aclaración. -- **GitHub Issues**: Las incidencias son una forma excelente de informar de errores. Además, puedes intentar resolver un problema exixtente y enviar un PR. +- **Discusiones de GitHub**: Una excelente manera de hablar sobre las características que deseas agregar o las cosas que te resultan confusas o necesitan aclaración. +- **GitHub Issues**: Las incidencias son una forma excelente de informar de errores. Además, puedes intentar resolver un problema existente y enviar un PR. -Buscamos colaboradores, sin importar su nivel o experiencia. +Buscamos colaboradores, sin importar su nivel o experiencia. Para contribuir consulta [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) ## Licencia From 4bda3eb233b6c8426224793c67ebc69649279794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Wed, 22 May 2024 01:21:46 +0200 Subject: [PATCH 025/496] add style for orientation=vertical in tabs (#3332) --- reflex/components/datadisplay/code.py | 2 +- reflex/components/datadisplay/code.pyi | 2 +- .../components/radix/primitives/accordion.py | 154 +++++++++--------- .../components/radix/primitives/accordion.pyi | 10 +- reflex/components/radix/primitives/form.py | 17 +- reflex/components/radix/primitives/form.pyi | 9 +- .../components/radix/primitives/progress.py | 49 +++--- .../components/radix/primitives/progress.pyi | 7 +- reflex/components/radix/primitives/slider.py | 103 ++++++------ reflex/components/radix/primitives/slider.pyi | 9 +- .../radix/themes/components/tabs.py | 46 ++++++ .../radix/themes/components/tabs.pyi | 7 + .../components/radix/themes/layout/center.py | 16 +- .../components/radix/themes/layout/center.pyi | 4 +- reflex/components/radix/themes/layout/list.py | 15 +- .../components/radix/themes/layout/list.pyi | 5 +- .../components/radix/themes/layout/spacer.py | 16 +- .../components/radix/themes/layout/spacer.pyi | 4 +- reflex/experimental/layout.py | 4 +- 19 files changed, 251 insertions(+), 228 deletions(-) diff --git a/reflex/components/datadisplay/code.py b/reflex/components/datadisplay/code.py index c59256b07..e6daf45f6 100644 --- a/reflex/components/datadisplay/code.py +++ b/reflex/components/datadisplay/code.py @@ -493,7 +493,7 @@ class CodeBlock(Component): else: return code_block - def add_style(self) -> Style | None: + def add_style(self): """Add style to the component.""" self.custom_style.update(self.style) diff --git a/reflex/components/datadisplay/code.pyi b/reflex/components/datadisplay/code.pyi index f3aeb8e18..901bf38e3 100644 --- a/reflex/components/datadisplay/code.pyi +++ b/reflex/components/datadisplay/code.pyi @@ -1111,6 +1111,6 @@ class CodeBlock(Component): The text component. """ ... - def add_style(self) -> Style | None: ... + def add_style(self): ... @staticmethod def convert_theme_name(theme) -> str: ... diff --git a/reflex/components/radix/primitives/accordion.py b/reflex/components/radix/primitives/accordion.py index 9cadfb35d..12eb8de39 100644 --- a/reflex/components/radix/primitives/accordion.py +++ b/reflex/components/radix/primitives/accordion.py @@ -59,7 +59,7 @@ class AccordionComponent(RadixPrimitiveComponent): # The variant of the component. variant: Var[LiteralAccordionVariant] - def add_style(self) -> Style | None: + def add_style(self): """Add style to the component.""" if self.color_scheme is not None: self.custom_attrs["data-accent-color"] = self.color_scheme @@ -250,43 +250,41 @@ class AccordionItem(AccordionComponent): return super().create(*children, value=value, **props, class_name=cls_name) - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ divider_style = f"var(--divider-px) solid {color('gray', 6, alpha=True)}" - return Style( - { - "overflow": "hidden", - "width": "100%", - "margin_top": "1px", + return { + "overflow": "hidden", + "width": "100%", + "margin_top": "1px", + "border_top": divider_style, + "&:first-child": { + "margin_top": 0, + "border_top": 0, + "border_top_left_radius": "var(--radius-4)", + "border_top_right_radius": "var(--radius-4)", + }, + "&:last-child": { + "border_bottom_left_radius": "var(--radius-4)", + "border_bottom_right_radius": "var(--radius-4)", + }, + "&:focus-within": { + "position": "relative", + "z_index": 1, + }, + _inherited_variant_selector("ghost", "&:first-child"): { + "border_radius": 0, "border_top": divider_style, - "&:first-child": { - "margin_top": 0, - "border_top": 0, - "border_top_left_radius": "var(--radius-4)", - "border_top_right_radius": "var(--radius-4)", - }, - "&:last-child": { - "border_bottom_left_radius": "var(--radius-4)", - "border_bottom_right_radius": "var(--radius-4)", - }, - "&:focus-within": { - "position": "relative", - "z_index": 1, - }, - _inherited_variant_selector("ghost", "&:first-child"): { - "border_radius": 0, - "border_top": divider_style, - }, - _inherited_variant_selector("ghost", "&:last-child"): { - "border_radius": 0, - "border_bottom": divider_style, - }, - } - ) + }, + _inherited_variant_selector("ghost", "&:last-child"): { + "border_radius": 0, + "border_bottom": divider_style, + }, + } class AccordionHeader(AccordionComponent): @@ -314,13 +312,13 @@ class AccordionHeader(AccordionComponent): return super().create(*children, class_name=cls_name, **props) - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style({"display": "flex"}) + return {"display": "flex"} class AccordionTrigger(AccordionComponent): @@ -348,44 +346,42 @@ class AccordionTrigger(AccordionComponent): return super().create(*children, class_name=cls_name, **props) - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style( - { - "color": color("accent", 11), - "font_size": "1.1em", - "line_height": 1, - "justify_content": "space-between", - "align_items": "center", - "flex": 1, - "display": "flex", - "padding": "var(--space-3) var(--space-4)", - "width": "100%", - "box_shadow": f"0 var(--divider-px) 0 {color('gray', 6, alpha=True)}", - "&[data-state='open'] > .AccordionChevron": { - "transform": "rotate(180deg)", - }, + return { + "color": color("accent", 11), + "font_size": "1.1em", + "line_height": 1, + "justify_content": "space-between", + "align_items": "center", + "flex": 1, + "display": "flex", + "padding": "var(--space-3) var(--space-4)", + "width": "100%", + "box_shadow": f"0 var(--divider-px) 0 {color('gray', 6, alpha=True)}", + "&[data-state='open'] > .AccordionChevron": { + "transform": "rotate(180deg)", + }, + "&:hover": { + "background_color": color("accent", 4), + }, + "& > .AccordionChevron": { + "transition": f"transform var(--animation-duration) var(--animation-easing)", + }, + _inherited_variant_selector("classic"): { + "color": "var(--accent-contrast)", "&:hover": { - "background_color": color("accent", 4), + "background_color": color("accent", 10), }, "& > .AccordionChevron": { - "transition": f"transform var(--animation-duration) var(--animation-easing)", - }, - _inherited_variant_selector("classic"): { "color": "var(--accent-contrast)", - "&:hover": { - "background_color": color("accent", 10), - }, - "& > .AccordionChevron": { - "color": "var(--accent-contrast)", - }, }, - } - ) + }, + } class AccordionIcon(Icon): @@ -470,7 +466,7 @@ to { """ ] - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: @@ -486,24 +482,22 @@ to { _var_is_string=True, ) - return Style( - { - "overflow": "hidden", - "color": color("accent", 11), - "padding_x": "var(--space-4)", - # Apply before and after content to avoid height animation jank. - "&:before, &:after": { - "content": "' '", - "display": "block", - "height": "var(--space-3)", - }, - "&[data-state='open']": {"animation": slideDown}, - "&[data-state='closed']": {"animation": slideUp}, - _inherited_variant_selector("classic"): { - "color": "var(--accent-contrast)", - }, - } - ) + return { + "overflow": "hidden", + "color": color("accent", 11), + "padding_x": "var(--space-4)", + # Apply before and after content to avoid height animation jank. + "&:before, &:after": { + "content": "' '", + "display": "block", + "height": "var(--space-3)", + }, + "&[data-state='open']": {"animation": slideDown}, + "&[data-state='closed']": {"animation": slideUp}, + _inherited_variant_selector("classic"): { + "color": "var(--accent-contrast)", + }, + } class Accordion(ComponentNamespace): diff --git a/reflex/components/radix/primitives/accordion.pyi b/reflex/components/radix/primitives/accordion.pyi index e7ae3ff19..a04073d2d 100644 --- a/reflex/components/radix/primitives/accordion.pyi +++ b/reflex/components/radix/primitives/accordion.pyi @@ -26,7 +26,7 @@ DEFAULT_ANIMATION_DURATION = 250 DEFAULT_ANIMATION_EASING = "cubic-bezier(0.87, 0, 0.13, 1)" class AccordionComponent(RadixPrimitiveComponent): - def add_style(self) -> Style | None: ... + def add_style(self): ... @overload @classmethod def create( # type: ignore @@ -520,7 +520,7 @@ class AccordionItem(AccordionComponent): The accordion item. """ ... - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... class AccordionHeader(AccordionComponent): @overload @@ -669,7 +669,7 @@ class AccordionHeader(AccordionComponent): The Accordion header Component. """ ... - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... class AccordionTrigger(AccordionComponent): @overload @@ -818,7 +818,7 @@ class AccordionTrigger(AccordionComponent): The Accordion trigger Component. """ ... - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... class AccordionIcon(Icon): @overload @@ -1047,7 +1047,7 @@ class AccordionContent(AccordionComponent): """ ... def add_custom_code(self) -> list[str]: ... - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... class Accordion(ComponentNamespace): content = staticmethod(AccordionContent.create) diff --git a/reflex/components/radix/primitives/form.py b/reflex/components/radix/primitives/form.py index 3369673da..1b37bd5f7 100644 --- a/reflex/components/radix/primitives/form.py +++ b/reflex/components/radix/primitives/form.py @@ -8,7 +8,6 @@ from reflex.components.component import ComponentNamespace from reflex.components.el.elements.forms import Form as HTMLForm from reflex.components.radix.themes.components.text_field import TextFieldRoot from reflex.constants.event import EventTriggers -from reflex.style import Style from reflex.vars import Var from .base import RadixPrimitiveComponentWithClassName @@ -38,13 +37,13 @@ class FormRoot(FormComponent, HTMLForm): EventTriggers.ON_CLEAR_SERVER_ERRORS: lambda: [], } - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style({"width": "100%"}) + return {"width": "100%"} class FormField(FormComponent): @@ -60,13 +59,13 @@ class FormField(FormComponent): # Flag to mark the form field as invalid, for server side validation. server_invalid: Var[bool] - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style({"display": "grid", "margin_bottom": "10px"}) + return {"display": "grid", "margin_bottom": "10px"} class FormLabel(FormComponent): @@ -76,13 +75,13 @@ class FormLabel(FormComponent): alias = "RadixFormLabel" - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style({"font_size": "15px", "font_weight": "500", "line_height": "35px"}) + return {"font_size": "15px", "font_weight": "500", "line_height": "35px"} class FormControl(FormComponent): @@ -149,13 +148,13 @@ class FormMessage(FormComponent): # Forces the message to be shown. This is useful when using server-side validation. force_match: Var[bool] - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style({"font_size": "13px", "opacity": "0.8", "color": "white"}) + return {"font_size": "13px", "opacity": "0.8", "color": "white"} class FormValidityState(FormComponent): diff --git a/reflex/components/radix/primitives/form.pyi b/reflex/components/radix/primitives/form.pyi index 8e1631825..182aaec36 100644 --- a/reflex/components/radix/primitives/form.pyi +++ b/reflex/components/radix/primitives/form.pyi @@ -12,7 +12,6 @@ from reflex.components.component import ComponentNamespace from reflex.components.el.elements.forms import Form as HTMLForm from reflex.components.radix.themes.components.text_field import TextFieldRoot from reflex.constants.event import EventTriggers -from reflex.style import Style from reflex.vars import Var from .base import RadixPrimitiveComponentWithClassName @@ -96,7 +95,7 @@ class FormComponent(RadixPrimitiveComponentWithClassName): class FormRoot(FormComponent, HTMLForm): def get_event_triggers(self) -> Dict[str, Any]: ... - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore @@ -275,7 +274,7 @@ class FormRoot(FormComponent, HTMLForm): ... class FormField(FormComponent): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore @@ -358,7 +357,7 @@ class FormField(FormComponent): ... class FormLabel(FormComponent): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore @@ -532,7 +531,7 @@ LiteralMatcher = Literal[ ] class FormMessage(FormComponent): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/components/radix/primitives/progress.py b/reflex/components/radix/primitives/progress.py index 313d6aeb6..976daf505 100644 --- a/reflex/components/radix/primitives/progress.py +++ b/reflex/components/radix/primitives/progress.py @@ -2,14 +2,13 @@ from __future__ import annotations -from typing import Optional +from typing import Any, Optional from reflex.components.component import Component, ComponentNamespace from reflex.components.core.colors import color from reflex.components.radix.primitives.accordion import DEFAULT_ANIMATION_DURATION from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius -from reflex.style import Style from reflex.vars import Var @@ -28,7 +27,7 @@ class ProgressRoot(ProgressComponent): # Override theme radius for progress bar: "none" | "small" | "medium" | "large" | "full" radius: Var[LiteralRadius] - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: @@ -37,17 +36,15 @@ class ProgressRoot(ProgressComponent): if self.radius is not None: self.custom_attrs["data-radius"] = self.radius - return Style( - { - "position": "relative", - "overflow": "hidden", - "background": color("gray", 3, alpha=True), - "border_radius": "max(var(--radius-2), var(--radius-full))", - "width": "100%", - "height": "20px", - "boxShadow": f"inset 0 0 0 1px {color('gray', 5, alpha=True)}", - } - ) + return { + "position": "relative", + "overflow": "hidden", + "background": color("gray", 3, alpha=True), + "border_radius": "max(var(--radius-2), var(--radius-full))", + "width": "100%", + "height": "20px", + "boxShadow": f"inset 0 0 0 1px {color('gray', 5, alpha=True)}", + } def _exclude_props(self) -> list[str]: return ["radius"] @@ -69,7 +66,7 @@ class ProgressIndicator(ProgressComponent): # The color scheme of the progress indicator. color_scheme: Var[LiteralAccentColor] - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: @@ -78,19 +75,17 @@ class ProgressIndicator(ProgressComponent): if self.color_scheme is not None: self.custom_attrs["data-accent-color"] = self.color_scheme - return Style( - { - "background_color": color("accent", 9), - "width": "100%", - "height": "100%", + return { + "background_color": color("accent", 9), + "width": "100%", + "height": "100%", + "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms linear", + "&[data_state='loading']": { "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms linear", - "&[data_state='loading']": { - "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms linear", - }, - "transform": f"translateX(calc(-100% + ({self.value} / {self.max} * 100%)))", # type: ignore - "boxShadow": "inset 0 0 0 1px var(--gray-a5)", - } - ) + }, + "transform": f"translateX(calc(-100% + ({self.value} / {self.max} * 100%)))", # type: ignore + "boxShadow": "inset 0 0 0 1px var(--gray-a5)", + } def _exclude_props(self) -> list[str]: return ["color_scheme"] diff --git a/reflex/components/radix/primitives/progress.pyi b/reflex/components/radix/primitives/progress.pyi index eb0149496..f6022652e 100644 --- a/reflex/components/radix/primitives/progress.pyi +++ b/reflex/components/radix/primitives/progress.pyi @@ -7,13 +7,12 @@ from typing import Any, Dict, Literal, Optional, Union, overload from reflex.vars import Var, BaseVar, ComputedVar from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style -from typing import Optional +from typing import Any, Optional from reflex.components.component import Component, ComponentNamespace from reflex.components.core.colors import color from reflex.components.radix.primitives.accordion import DEFAULT_ANIMATION_DURATION from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius -from reflex.style import Style from reflex.vars import Var class ProgressComponent(RadixPrimitiveComponentWithClassName): @@ -95,7 +94,7 @@ class ProgressComponent(RadixPrimitiveComponentWithClassName): ... class ProgressRoot(ProgressComponent): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore @@ -181,7 +180,7 @@ class ProgressRoot(ProgressComponent): ... class ProgressIndicator(ProgressComponent): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/components/radix/primitives/slider.py b/reflex/components/radix/primitives/slider.py index 2e0b1ef49..8368c7ed1 100644 --- a/reflex/components/radix/primitives/slider.py +++ b/reflex/components/radix/primitives/slider.py @@ -6,7 +6,6 @@ from typing import Any, Dict, List, Literal from reflex.components.component import Component, ComponentNamespace from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName -from reflex.style import Style from reflex.vars import Var LiteralSliderOrientation = Literal["horizontal", "vertical"] @@ -59,28 +58,26 @@ class SliderRoot(SliderComponent): "on_value_commit": lambda e0: [e0], # trigger when thumb is released } - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style( - { - "position": "relative", - "display": "flex", - "align_items": "center", - "user_select": "none", - "touch_action": "none", - "width": "200px", - "height": "20px", - "&[data-orientation='vertical']": { - "flex_direction": "column", - "width": "20px", - "height": "100px", - }, - } - ) + return { + "position": "relative", + "display": "flex", + "align_items": "center", + "user_select": "none", + "touch_action": "none", + "width": "200px", + "height": "20px", + "&[data-orientation='vertical']": { + "flex_direction": "column", + "width": "20px", + "height": "100px", + }, + } class SliderTrack(SliderComponent): @@ -89,22 +86,20 @@ class SliderTrack(SliderComponent): tag = "Track" alias = "RadixSliderTrack" - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style( - { - "position": "relative", - "flex_grow": "1", - "background_color": "black", - "border_radius": "9999px", - "height": "3px", - "&[data-orientation='vertical']": {"width": "3px"}, - } - ) + return { + "position": "relative", + "flex_grow": "1", + "background_color": "black", + "border_radius": "9999px", + "height": "3px", + "&[data-orientation='vertical']": {"width": "3px"}, + } class SliderRange(SliderComponent): @@ -113,20 +108,18 @@ class SliderRange(SliderComponent): tag = "Range" alias = "RadixSliderRange" - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style( - { - "position": "absolute", - "background_color": "white", - "height": "100%", - "&[data-orientation='vertical']": {"width": "100%"}, - } - ) + return { + "position": "absolute", + "background_color": "white", + "height": "100%", + "&[data-orientation='vertical']": {"width": "100%"}, + } class SliderThumb(SliderComponent): @@ -135,29 +128,27 @@ class SliderThumb(SliderComponent): tag = "Thumb" alias = "RadixSliderThumb" - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style( - { - "display": "block", - "width": "20px", - "height": "20px", - "background_color": "black", - "box_shadow": "0 2px 10px black", - "border_radius": "10px", - "&:hover": { - "background_color": "gray", - }, - "&:focus": { - "outline": "none", - "box_shadow": "0 0 0 4px gray", - }, - } - ) + return { + "display": "block", + "width": "20px", + "height": "20px", + "background_color": "black", + "box_shadow": "0 2px 10px black", + "border_radius": "10px", + "&:hover": { + "background_color": "gray", + }, + "&:focus": { + "outline": "none", + "box_shadow": "0 0 0 4px gray", + }, + } class Slider(ComponentNamespace): diff --git a/reflex/components/radix/primitives/slider.pyi b/reflex/components/radix/primitives/slider.pyi index 1f837d732..4ec99f73f 100644 --- a/reflex/components/radix/primitives/slider.pyi +++ b/reflex/components/radix/primitives/slider.pyi @@ -10,7 +10,6 @@ from reflex.style import Style from typing import Any, Dict, List, Literal from reflex.components.component import Component, ComponentNamespace from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName -from reflex.style import Style from reflex.vars import Var LiteralSliderOrientation = Literal["horizontal", "vertical"] @@ -96,7 +95,7 @@ class SliderComponent(RadixPrimitiveComponentWithClassName): class SliderRoot(SliderComponent): def get_event_triggers(self) -> Dict[str, Any]: ... - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore @@ -197,7 +196,7 @@ class SliderRoot(SliderComponent): ... class SliderTrack(SliderComponent): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore @@ -276,7 +275,7 @@ class SliderTrack(SliderComponent): ... class SliderRange(SliderComponent): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore @@ -355,7 +354,7 @@ class SliderRange(SliderComponent): ... class SliderThumb(SliderComponent): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/components/radix/themes/components/tabs.py b/reflex/components/radix/themes/components/tabs.py index 130cfd166..fb5bf936d 100644 --- a/reflex/components/radix/themes/components/tabs.py +++ b/reflex/components/radix/themes/components/tabs.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any, Dict, List, Literal from reflex.components.component import Component, ComponentNamespace +from reflex.components.core.colors import color from reflex.constants import EventTriggers from reflex.vars import Var @@ -12,6 +13,8 @@ from ..base import ( RadixThemesComponent, ) +vertical_orientation_css = "&[data-orientation='vertical']" + class TabsRoot(RadixThemesComponent): """Set of content sections to be displayed one at a time.""" @@ -41,6 +44,18 @@ class TabsRoot(RadixThemesComponent): EventTriggers.ON_CHANGE: lambda e0: [e0], } + def add_style(self) -> Dict[str, Any] | None: + """Add style for the component. + + Returns: + The style to add. + """ + return { + vertical_orientation_css: { + "display": "flex", + } + } + class TabsList(RadixThemesComponent): """Contains the triggers that sit alongside the active content.""" @@ -50,6 +65,19 @@ class TabsList(RadixThemesComponent): # Tabs size "1" - "2" size: Var[Literal["1", "2"]] + def add_style(self): + """Add style for the component. + + Returns: + The style to add. + """ + return { + vertical_orientation_css: { + "display": "block", + "box_shadow": f"inset -1px 0 0 0 {color('gray', 5, alpha=True)}", + }, + } + class TabsTrigger(RadixThemesComponent): """The button that activates its associated content.""" @@ -86,6 +114,14 @@ class TabsTrigger(RadixThemesComponent): def _exclude_props(self) -> list[str]: return ["color_scheme"] + def add_style(self) -> Dict[str, Any] | None: + """Add style for the component. + + Returns: + The style to add. + """ + return {vertical_orientation_css: {"width": "100%"}} + class TabsContent(RadixThemesComponent): """Contains the content associated with each trigger.""" @@ -95,6 +131,16 @@ class TabsContent(RadixThemesComponent): # The value of the tab. Must be unique for each tab. value: Var[str] + def add_style(self) -> dict[str, Any] | None: + """Add style for the component. + + Returns: + The style to add. + """ + return { + vertical_orientation_css: {"width": "100%", "margin": None}, + } + class Tabs(ComponentNamespace): """Set of content sections to be displayed one at a time.""" diff --git a/reflex/components/radix/themes/components/tabs.pyi b/reflex/components/radix/themes/components/tabs.pyi index a8243554f..02d3bbc93 100644 --- a/reflex/components/radix/themes/components/tabs.pyi +++ b/reflex/components/radix/themes/components/tabs.pyi @@ -9,12 +9,16 @@ from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style from typing import Any, Dict, List, Literal from reflex.components.component import Component, ComponentNamespace +from reflex.components.core.colors import color from reflex.constants import EventTriggers from reflex.vars import Var from ..base import LiteralAccentColor, RadixThemesComponent +vertical_orientation_css = "&[data-orientation='vertical']" + class TabsRoot(RadixThemesComponent): def get_event_triggers(self) -> Dict[str, Any]: ... + def add_style(self) -> Dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore @@ -108,6 +112,7 @@ class TabsRoot(RadixThemesComponent): ... class TabsList(RadixThemesComponent): + def add_style(self): ... @overload @classmethod def create( # type: ignore @@ -330,8 +335,10 @@ class TabsTrigger(RadixThemesComponent): The TabsTrigger Component. """ ... + def add_style(self) -> Dict[str, Any] | None: ... class TabsContent(RadixThemesComponent): + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/components/radix/themes/layout/center.py b/reflex/components/radix/themes/layout/center.py index 16441876a..6799f94e9 100644 --- a/reflex/components/radix/themes/layout/center.py +++ b/reflex/components/radix/themes/layout/center.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.style import Style +from typing import Any from .flex import Flex @@ -10,16 +10,14 @@ from .flex import Flex class Center(Flex): """A center component.""" - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style that center the content. Returns: The style of the component. """ - return Style( - { - "display": "flex", - "align_items": "center", - "justify_content": "center", - } - ) + return { + "display": "flex", + "align_items": "center", + "justify_content": "center", + } diff --git a/reflex/components/radix/themes/layout/center.pyi b/reflex/components/radix/themes/layout/center.pyi index 0fd45f940..9a05e6f99 100644 --- a/reflex/components/radix/themes/layout/center.pyi +++ b/reflex/components/radix/themes/layout/center.pyi @@ -7,11 +7,11 @@ from typing import Any, Dict, Literal, Optional, Union, overload from reflex.vars import Var, BaseVar, ComputedVar from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style -from reflex.style import Style +from typing import Any from .flex import Flex class Center(Flex): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/components/radix/themes/layout/list.py b/reflex/components/radix/themes/layout/list.py index 87b9cf012..e2e0c15f8 100644 --- a/reflex/components/radix/themes/layout/list.py +++ b/reflex/components/radix/themes/layout/list.py @@ -1,14 +1,13 @@ """List components.""" from __future__ import annotations -from typing import Iterable, Literal, Optional, Union +from typing import Any, Iterable, Literal, Optional, Union from reflex.components.component import Component, ComponentNamespace from reflex.components.core.foreach import Foreach from reflex.components.el.elements.typography import Li, Ol, Ul from reflex.components.lucide.icon import Icon from reflex.components.radix.themes.typography.text import Text -from reflex.style import Style from reflex.vars import Var LiteralListStyleTypeUnordered = Literal[ @@ -78,18 +77,16 @@ class BaseList(Component): style["gap"] = props["gap"] return super().create(*children, **props) - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style( - { - "direction": "column", - "list_style_position": "inside", - } - ) + return { + "direction": "column", + "list_style_position": "inside", + } class UnorderedList(BaseList, Ul): diff --git a/reflex/components/radix/themes/layout/list.pyi b/reflex/components/radix/themes/layout/list.pyi index a736f106c..6ff3a0a0f 100644 --- a/reflex/components/radix/themes/layout/list.pyi +++ b/reflex/components/radix/themes/layout/list.pyi @@ -7,13 +7,12 @@ from typing import Any, Dict, Literal, Optional, Union, overload from reflex.vars import Var, BaseVar, ComputedVar from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style -from typing import Iterable, Literal, Optional, Union +from typing import Any, Iterable, Literal, Optional, Union from reflex.components.component import Component, ComponentNamespace from reflex.components.core.foreach import Foreach from reflex.components.el.elements.typography import Li, Ol, Ul from reflex.components.lucide.icon import Icon from reflex.components.radix.themes.typography.text import Text -from reflex.style import Style from reflex.vars import Var LiteralListStyleTypeUnordered = Literal["none", "disc", "circle", "square"] @@ -157,7 +156,7 @@ class BaseList(Component): """ ... - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... class UnorderedList(BaseList, Ul): @overload diff --git a/reflex/components/radix/themes/layout/spacer.py b/reflex/components/radix/themes/layout/spacer.py index 6d7ab9aaf..3186552b1 100644 --- a/reflex/components/radix/themes/layout/spacer.py +++ b/reflex/components/radix/themes/layout/spacer.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.style import Style +from typing import Any from .flex import Flex @@ -10,16 +10,14 @@ from .flex import Flex class Spacer(Flex): """A spacer component.""" - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: The style of the component. """ - return Style( - { - "flex": 1, - "justify_self": "stretch", - "align_self": "stretch", - } - ) + return { + "flex": 1, + "justify_self": "stretch", + "align_self": "stretch", + } diff --git a/reflex/components/radix/themes/layout/spacer.pyi b/reflex/components/radix/themes/layout/spacer.pyi index 733501489..b991e9381 100644 --- a/reflex/components/radix/themes/layout/spacer.pyi +++ b/reflex/components/radix/themes/layout/spacer.pyi @@ -7,11 +7,11 @@ from typing import Any, Dict, Literal, Optional, Union, overload from reflex.vars import Var, BaseVar, ComputedVar from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style -from reflex.style import Style +from typing import Any from .flex import Flex class Spacer(Flex): - def add_style(self) -> Style | None: ... + def add_style(self) -> dict[str, Any] | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/experimental/layout.py b/reflex/experimental/layout.py index 5041b9356..033379158 100644 --- a/reflex/experimental/layout.py +++ b/reflex/experimental/layout.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from reflex import color, cond from reflex.components.base.fragment import Fragment from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf @@ -40,7 +42,7 @@ class Sidebar(Box, MemoizationLeaf): Box.create(width=props.get("width")), # spacer for layout ) - def add_style(self) -> Style | None: + def add_style(self) -> dict[str, Any] | None: """Add style to the component. Returns: From 3e5bf00ba206237f18d3969df8f6bd498482e7a7 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 21 May 2024 17:17:48 -0700 Subject: [PATCH 026/496] Bump to 0.5.1 (#3364) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fb3eab724..80e246089 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "reflex" -version = "0.5.0" +version = "0.5.1" description = "Web apps in pure Python." license = "Apache-2.0" authors = [ From 6976fe61452a7a6a0ad72c1e9228e2193a6a3f1a Mon Sep 17 00:00:00 2001 From: owlur <8864248@naver.com> Date: Wed, 22 May 2024 11:37:21 +0900 Subject: [PATCH 027/496] Update Korean README.md and synchronize code with the latest version (#3337) --- docs/kr/README.md | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/docs/kr/README.md b/docs/kr/README.md index 266992f9a..aaaa26b3f 100644 --- a/docs/kr/README.md +++ b/docs/kr/README.md @@ -70,10 +70,12 @@ http://localhost:3000 에서 앱이 실행 됩니다. import reflex as rx import openai -openai.api_key = "YOUR_API_KEY" +openai_client = openai.OpenAI() + class State(rx.State): """The app state.""" + prompt = "" image_url = "" processing = False @@ -86,33 +88,33 @@ class State(rx.State): self.processing, self.complete = True, False yield - response = openai.Image.create(prompt=self.prompt, n=1, size="1024x1024") - self.image_url = response["data"][0]["url"] + response = openai_client.images.generate( + prompt=self.prompt, n=1, size="1024x1024" + ) + self.image_url = response.data[0].url self.processing, self.complete = False, True - + def index(): return rx.center( rx.vstack( - rx.heading("DALL·E"), - rx.input(placeholder="Enter a prompt", on_blur=State.set_prompt), + rx.heading("DALL-E", font_size="1.5em"), + rx.input( + placeholder="Enter a prompt..", + on_blur=State.set_prompt, + width="25em", + ), rx.button( - "Generate Image", + "Generate Image", on_click=State.get_image, - is_loading=State.processing, - width="100%", + width="25em", + loading=State.processing ), rx.cond( State.complete, - rx.image( - src=State.image_url, - height="25em", - width="25em", - ) + rx.image(src=State.image_url, width="20em"), ), - padding="2em", - shadow="lg", - border_radius="lg", + align="center", ), width="100%", height="100vh", @@ -120,7 +122,7 @@ def index(): # Add state and page to the app. app = rx.App() -app.add_page(index, title="reflex:DALL·E") +app.add_page(index, title="Reflex:DALL-E") ``` ## 하나씩 살펴보겠습니다. @@ -160,7 +162,7 @@ class State(rx.State): state는 앱에서 변경될 수 있는 모든 변수(vars로 불림)와 이러한 변수를 변경하는 함수를 정의합니다. -여기서 state는 `prompt`와 `image_url`로 구성됩니다. 또한 `processing`과 `complete`라는 불리언 값이 있습니다. 이 값들은 원형 진행률과 이미지를 표시할 때를 나타냅니다. +여기서 state는 `prompt`와 `image_url`로 구성됩니다. 또한 `processing`과 `complete`라는 불리언 값이 있습니다. 이 값들은 이미지 생성 중 버튼을 비활성화할 때와, 결과 이미지를 표시할 때를 나타냅니다. ### **Event Handlers** @@ -172,8 +174,10 @@ def get_image(self): self.processing, self.complete = True, False yield - response = openai.Image.create(prompt=self.prompt, n=1, size="1024x1024") - self.image_url = response["data"][0]["url"] + response = openai_client.images.generate( + prompt=self.prompt, n=1, size="1024x1024" + ) + self.image_url = response.data[0].url self.processing, self.complete = False, True ``` From 0c0dde127f30a1e778c983cc2d8ea2158b4e89a0 Mon Sep 17 00:00:00 2001 From: Angelina Sheyko <84133312+Snaipergelka@users.noreply.github.com> Date: Thu, 23 May 2024 01:31:15 +0700 Subject: [PATCH 028/496] Added config for number of gunicorn workers (#3351) --- reflex/config.py | 3 +++ reflex/config.pyi | 2 ++ reflex/utils/exec.py | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/reflex/config.py b/reflex/config.py index 40c2ef8bf..769b94328 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -213,6 +213,9 @@ class Config(Base): # The worker class used in production mode gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker" + # Number of gunicorn workers from user + gunicorn_workers: Optional[int] = None + # Attributes that were explicitly set by the user. _non_default_attributes: Set[str] = pydantic.PrivateAttr(set()) diff --git a/reflex/config.pyi b/reflex/config.pyi index 57ce1123d..f7dfde770 100644 --- a/reflex/config.pyi +++ b/reflex/config.pyi @@ -70,6 +70,7 @@ class Config(Base): cp_web_url: str username: Optional[str] gunicorn_worker_class: str + gunicorn_workers: Optional[int] def __init__( self, @@ -97,6 +98,7 @@ class Config(Base): cp_web_url: Optional[str] = None, username: Optional[str] = None, gunicorn_worker_class: Optional[str] = None, + gunicorn_workers: Optional[int] = None, **kwargs ) -> None: ... @property diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index e4aefb8f1..91f235b7e 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -217,8 +217,12 @@ def run_backend_prod( """ from reflex.utils import processes - num_workers = processes.get_num_workers() config = get_config() + num_workers = ( + processes.get_num_workers() + if not config.gunicorn_workers + else config.gunicorn_workers + ) RUN_BACKEND_PROD = f"gunicorn --worker-class {config.gunicorn_worker_class} --preload --timeout {config.timeout} --log-level critical".split() RUN_BACKEND_PROD_WINDOWS = f"uvicorn --timeout-keep-alive {config.timeout}".split() app_module = f"reflex.app_module_for_backend:{constants.CompileVars.APP}" From 0b613d5d3b456eea532c56dacecc0b4868062b47 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Wed, 22 May 2024 20:35:42 +0200 Subject: [PATCH 029/496] do not check attribute type for var internals (#3357) --- reflex/vars.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/reflex/vars.py b/reflex/vars.py index be6aa7eb8..601caac5d 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -704,7 +704,11 @@ class Var: """ try: var_attribute = super().__getattribute__(name) - if not name.startswith("_"): + if ( + not name.startswith("_") + and name not in Var.__dict__ + and name not in BaseVar.__dict__ + ): # Check if the attribute should be accessed through the Var instead of # accessing one of the Var operations type_ = types.get_attribute_access_type( From 956a526b20d847486643b633e717ed78d76cb848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Wed, 22 May 2024 21:07:03 +0200 Subject: [PATCH 030/496] add support for lifespan tasks (#3312) * add support for lifespan tasks * allow passing args to lifespan task * add message to the cancel call * allow asynccontextmanager as lifespan tasks * Fix integration.utils.SessionStorage Previously the SessionStorage util was just looking in localStorage, but the tests didn't catch it because they were asserting the token was not None, rather than asserting it was truthy. Fixed here, because I'm using this structure in the new lifespan test. * If the lifespan task or context takes "app" parameter, pass the FastAPI instance. * test_lifespan: end to end test for register_lifespan_task * In py3.8, Task.cancel takes no args * test_lifespan: use polling to make the test more robust Fix CI failure * Do not allow task_args for better composability --------- Co-authored-by: Masen Furer --- integration/test_component_state.py | 3 +- integration/test_lifespan.py | 120 ++++++++++++++++++++++++++++ integration/test_navigation.py | 3 +- integration/utils.py | 20 +++-- reflex/app.py | 49 +++++++++++- 5 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 integration/test_lifespan.py diff --git a/integration/test_component_state.py b/integration/test_component_state.py index e903a1b74..77b8b3fa1 100644 --- a/integration/test_component_state.py +++ b/integration/test_component_state.py @@ -79,8 +79,7 @@ async def test_component_state_app(component_state_app: AppHarness): driver = component_state_app.frontend() ss = utils.SessionStorage(driver) - token = AppHarness._poll_for(lambda: ss.get("token") is not None) - assert token is not None + assert AppHarness._poll_for(lambda: ss.get("token") is not None), "token not found" count_a = driver.find_element(By.ID, "count-a") count_b = driver.find_element(By.ID, "count-b") diff --git a/integration/test_lifespan.py b/integration/test_lifespan.py new file mode 100644 index 000000000..cb384a511 --- /dev/null +++ b/integration/test_lifespan.py @@ -0,0 +1,120 @@ +"""Test cases for the FastAPI lifespan integration.""" +from typing import Generator + +import pytest +from selenium.webdriver.common.by import By + +from reflex.testing import AppHarness + +from .utils import SessionStorage + + +def LifespanApp(): + """App with lifespan tasks and context.""" + import asyncio + from contextlib import asynccontextmanager + + import reflex as rx + + lifespan_task_global = 0 + lifespan_context_global = 0 + + @asynccontextmanager + async def lifespan_context(app, inc: int = 1): + global lifespan_context_global + print(f"Lifespan context entered: {app}.") + lifespan_context_global += inc # pyright: ignore[reportUnboundVariable] + try: + yield + finally: + print("Lifespan context exited.") + lifespan_context_global += inc + + async def lifespan_task(inc: int = 1): + global lifespan_task_global + print("Lifespan global started.") + try: + while True: + lifespan_task_global += inc # pyright: ignore[reportUnboundVariable] + await asyncio.sleep(0.1) + except asyncio.CancelledError as ce: + print(f"Lifespan global cancelled: {ce}.") + lifespan_task_global = 0 + + class LifespanState(rx.State): + @rx.var + def task_global(self) -> int: + return lifespan_task_global + + @rx.var + def context_global(self) -> int: + return lifespan_context_global + + def tick(self, date): + pass + + def index(): + return rx.vstack( + rx.text(LifespanState.task_global, id="task_global"), + rx.text(LifespanState.context_global, id="context_global"), + rx.moment(interval=100, on_change=LifespanState.tick), + ) + + app = rx.App() + app.register_lifespan_task(lifespan_task) + app.register_lifespan_task(lifespan_context, inc=2) + app.add_page(index) + + +@pytest.fixture() +def lifespan_app(tmp_path) -> Generator[AppHarness, None, None]: + """Start LifespanApp app at tmp_path via AppHarness. + + Args: + tmp_path: pytest tmp_path fixture + + Yields: + running AppHarness instance + """ + with AppHarness.create( + root=tmp_path, + app_source=LifespanApp, # type: ignore + ) as harness: + yield harness + + +@pytest.mark.asyncio +async def test_lifespan(lifespan_app: AppHarness): + """Test the lifespan integration. + + Args: + lifespan_app: harness for LifespanApp app + """ + assert lifespan_app.app_module is not None, "app module is not found" + assert lifespan_app.app_instance is not None, "app is not running" + driver = lifespan_app.frontend() + + ss = SessionStorage(driver) + assert AppHarness._poll_for(lambda: ss.get("token") is not None), "token not found" + + context_global = driver.find_element(By.ID, "context_global") + task_global = driver.find_element(By.ID, "task_global") + + assert context_global.text == "2" + assert lifespan_app.app_module.lifespan_context_global == 2 # type: ignore + + original_task_global_text = task_global.text + original_task_global_value = int(original_task_global_text) + lifespan_app.poll_for_content(task_global, exp_not_equal=original_task_global_text) + assert lifespan_app.app_module.lifespan_task_global > original_task_global_value # type: ignore + assert int(task_global.text) > original_task_global_value + + # Kill the backend + assert lifespan_app.backend is not None + lifespan_app.backend.should_exit = True + if lifespan_app.backend_thread is not None: + lifespan_app.backend_thread.join() + + # Check that the lifespan tasks have been cancelled + assert lifespan_app.app_module.lifespan_task_global == 0 + assert lifespan_app.app_module.lifespan_context_global == 4 diff --git a/integration/test_navigation.py b/integration/test_navigation.py index 2c288552f..f5785a6c4 100644 --- a/integration/test_navigation.py +++ b/integration/test_navigation.py @@ -67,8 +67,7 @@ async def test_navigation_app(navigation_app: AppHarness): driver = navigation_app.frontend() ss = SessionStorage(driver) - token = AppHarness._poll_for(lambda: ss.get("token") is not None) - assert token is not None + assert AppHarness._poll_for(lambda: ss.get("token") is not None), "token not found" internal_link = driver.find_element(By.ID, "internal") diff --git a/integration/utils.py b/integration/utils.py index bcbd6c497..5a5dbae81 100644 --- a/integration/utils.py +++ b/integration/utils.py @@ -54,7 +54,9 @@ class LocalStorage: Returns: The number of items in local storage. """ - return int(self.driver.execute_script("return window.localStorage.length;")) + return int( + self.driver.execute_script(f"return window.{self.storage_key}.length;") + ) def items(self) -> dict[str, str]: """Get all items in local storage. @@ -63,7 +65,7 @@ class LocalStorage: A dict mapping keys to values. """ return self.driver.execute_script( - "var ls = window.localStorage, items = {}; " + f"var ls = window.{self.storage_key}, items = {{}}; " "for (var i = 0, k; i < ls.length; ++i) " " items[k = ls.key(i)] = ls.getItem(k); " "return items; " @@ -76,7 +78,7 @@ class LocalStorage: A list of keys. """ return self.driver.execute_script( - "var ls = window.localStorage, keys = []; " + f"var ls = window.{self.storage_key}, keys = []; " "for (var i = 0; i < ls.length; ++i) " " keys[i] = ls.key(i); " "return keys; " @@ -92,7 +94,7 @@ class LocalStorage: The value of the key. """ return self.driver.execute_script( - "return window.localStorage.getItem(arguments[0]);", key + f"return window.{self.storage_key}.getItem(arguments[0]);", key ) def set(self, key, value) -> None: @@ -103,7 +105,9 @@ class LocalStorage: value: The value to set the key to. """ self.driver.execute_script( - "window.localStorage.setItem(arguments[0], arguments[1]);", key, value + f"window.{self.storage_key}.setItem(arguments[0], arguments[1]);", + key, + value, ) def has(self, key) -> bool: @@ -123,11 +127,13 @@ class LocalStorage: Args: key: The key to remove. """ - self.driver.execute_script("window.localStorage.removeItem(arguments[0]);", key) + self.driver.execute_script( + f"window.{self.storage_key}.removeItem(arguments[0]);", key + ) def clear(self) -> None: """Clear all local storage.""" - self.driver.execute_script("window.localStorage.clear();") + self.driver.execute_script(f"window.{self.storage_key}.clear();") def __getitem__(self, key) -> str: """Get a key from local storage. diff --git a/reflex/app.py b/reflex/app.py index 72a09462a..9481204cb 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -7,10 +7,12 @@ import concurrent.futures import contextlib import copy import functools +import inspect import io import multiprocessing import os import platform +import sys from typing import ( Any, AsyncIterator, @@ -100,7 +102,50 @@ class OverlayFragment(Fragment): pass -class App(Base): +class LifespanMixin(Base): + """A Mixin that allow tasks to run during the whole app lifespan.""" + + # Lifespan tasks that are planned to run. + lifespan_tasks: Set[Union[asyncio.Task, Callable]] = set() + + @contextlib.asynccontextmanager + async def _run_lifespan_tasks(self, app: FastAPI): + running_tasks = [] + try: + async with contextlib.AsyncExitStack() as stack: + for task in self.lifespan_tasks: + if isinstance(task, asyncio.Task): + running_tasks.append(task) + else: + signature = inspect.signature(task) + if "app" in signature.parameters: + task = functools.partial(task, app=app) + _t = task() + if isinstance(_t, contextlib._AsyncGeneratorContextManager): + await stack.enter_async_context(_t) + elif isinstance(_t, Coroutine): + running_tasks.append(asyncio.create_task(_t)) + yield + finally: + cancel_kwargs = ( + {"msg": "lifespan_cleanup"} if sys.version_info >= (3, 9) else {} + ) + for task in running_tasks: + task.cancel(**cancel_kwargs) + + def register_lifespan_task(self, task: Callable | asyncio.Task, **task_kwargs): + """Register a task to run during the lifespan of the app. + + Args: + task: The task to register. + task_kwargs: The kwargs of the task. + """ + if task_kwargs: + task = functools.partial(task, **task_kwargs) # type: ignore + self.lifespan_tasks.add(task) # type: ignore + + +class App(LifespanMixin, Base): """The main Reflex app that encapsulates the backend and frontend. Every Reflex app needs an app defined in its main module. @@ -203,7 +248,7 @@ class App(Base): self.middleware.append(HydrateMiddleware()) # Set up the API. - self.api = FastAPI() + self.api = FastAPI(lifespan=self._run_lifespan_tasks) self._add_cors() self._add_default_endpoints() From ed39c27bfb7ef76a4d7536e3a84173b7f6ccec22 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 22 May 2024 16:12:33 -0700 Subject: [PATCH 031/496] [REF-2915] ComponentState subclasses are always treated as mixin (#3372) Always treat ComponentState subclasses as mixin, and explicitly pass `mixin=False` in `.create` classmethod when intentionally creating a substate. This allows a "base" ComponentState to have subclasses that also work as ComponentState themselves. Fix #3368 --- reflex/state.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index ba87029c8..e778946c0 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -757,7 +757,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): for base in cls.__bases__ if issubclass(base, BaseState) and base is not BaseState and not base._mixin ] - assert len(parent_states) < 2, "Only one parent state is allowed." + assert ( + len(parent_states) < 2 + ), f"Only one parent state is allowed {parent_states}." return parent_states[0] if len(parent_states) == 1 else None # type: ignore @classmethod @@ -1887,15 +1889,13 @@ class ComponentState(State, mixin=True): _per_component_state_instance_count: ClassVar[int] = 0 @classmethod - def __init_subclass__(cls, mixin: bool = False, **kwargs): + def __init_subclass__(cls, mixin: bool = True, **kwargs): """Overwrite mixin default to True. Args: mixin: Whether the subclass is a mixin and should not be initialized. **kwargs: The kwargs to pass to the pydantic init_subclass method. """ - if ComponentState in cls.__bases__: - mixin = True super().__init_subclass__(mixin=mixin, **kwargs) @classmethod @@ -1926,7 +1926,7 @@ class ComponentState(State, mixin=True): """ cls._per_component_state_instance_count += 1 state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}" - component_state = type(state_cls_name, (cls, State), {}) + component_state = type(state_cls_name, (cls, State), {}, mixin=False) component = component_state.get_component(*children, **props) component.State = component_state return component From d96f0514861d5f496de6a78383e584ced20a961e Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Thu, 23 May 2024 18:23:07 +0200 Subject: [PATCH 032/496] set config.deploy_url during AppHarness tests (#3359) --- integration/test_deploy_url.py | 98 ++++++++++++++++++++++++++++++++++ reflex/testing.py | 2 + 2 files changed, 100 insertions(+) create mode 100644 integration/test_deploy_url.py diff --git a/integration/test_deploy_url.py b/integration/test_deploy_url.py new file mode 100644 index 000000000..b0421cfb7 --- /dev/null +++ b/integration/test_deploy_url.py @@ -0,0 +1,98 @@ +"""Integration tests for deploy_url.""" + +from __future__ import annotations + +from typing import Generator + +import pytest +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.support.ui import WebDriverWait + +from reflex.testing import AppHarness + + +def DeployUrlSample() -> None: + """Sample app for testing config deploy_url is correct (in tests).""" + import reflex as rx + + class State(rx.State): + def goto_self(self): + return rx.redirect(rx.config.get_config().deploy_url) # type: ignore + + def index(): + return rx.fragment( + rx.button("GOTO SELF", on_click=State.goto_self, id="goto_self") + ) + + app = rx.App(state=rx.State) + app.add_page(index) + + +@pytest.fixture(scope="module") +def deploy_url_sample( + tmp_path_factory: pytest.TempPathFactory, +) -> Generator[AppHarness, None, None]: + """AppHarness fixture for testing deploy_url. + + Args: + tmp_path_factory: pytest fixture for creating temporary directories. + + Yields: + AppHarness: An AppHarness instance. + """ + with AppHarness.create( + root=tmp_path_factory.mktemp("deploy_url_sample"), + app_source=DeployUrlSample, # type: ignore + ) as harness: + yield harness + + +@pytest.fixture() +def driver(deploy_url_sample: AppHarness) -> Generator[WebDriver, None, None]: + """WebDriver fixture for testing deploy_url. + + Args: + deploy_url_sample: AppHarness fixture for testing deploy_url. + + Yields: + WebDriver: A WebDriver instance. + """ + assert deploy_url_sample.app_instance is not None, "app is not running" + driver = deploy_url_sample.frontend() + try: + yield driver + finally: + driver.quit() + + +def test_deploy_url(deploy_url_sample: AppHarness, driver: WebDriver) -> None: + """Test deploy_url is correct. + + Args: + deploy_url_sample: AppHarness fixture for testing deploy_url. + driver: WebDriver fixture for testing deploy_url. + """ + import reflex as rx + + deploy_url = rx.config.get_config().deploy_url + assert deploy_url is not None + assert deploy_url != "http://localhost:3000" + assert deploy_url == deploy_url_sample.frontend_url + driver.get(deploy_url) + assert driver.current_url == deploy_url + "/" + + +def test_deploy_url_in_app(deploy_url_sample: AppHarness, driver: WebDriver) -> None: + """Test deploy_url is correct in app. + + Args: + deploy_url_sample: AppHarness fixture for testing deploy_url. + driver: WebDriver fixture for testing deploy_url. + """ + driver.implicitly_wait(10) + driver.find_element(By.ID, "goto_self").click() + + WebDriverWait(driver, 10).until( + lambda driver: driver.current_url == f"{deploy_url_sample.frontend_url}/" + ) diff --git a/reflex/testing.py b/reflex/testing.py index aa8cbb893..ad0839632 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -325,6 +325,8 @@ class AppHarness: m = re.search(reflex.constants.Next.FRONTEND_LISTENING_REGEX, line) if m is not None: self.frontend_url = m.group(1) + config = reflex.config.get_config() + config.deploy_url = self.frontend_url break if self.frontend_url is None: raise RuntimeError("Frontend did not start") From 653f00b0462b5b4e631d7483522b0025505918b3 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 23 May 2024 15:54:39 -0700 Subject: [PATCH 033/496] [REF-2878] Map fontFamily to fontFamily and --default-font-family (#3380) When setting the font_family prop for a component, also set the radix token `--default-font-family` so that child radix components will inherit the font. --- reflex/style.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reflex/style.py b/reflex/style.py index 21a601dd0..814cf19fe 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -47,6 +47,8 @@ STYLE_PROP_SHORTHAND_MAPPING = { "marginY": ("marginTop", "marginBottom"), "bg": ("background",), "bgColor": ("backgroundColor",), + # Radix components derive their font from this CSS var, not inherited from body or class. + "fontFamily": ("fontFamily", "--default-font-family"), } From 51d3b2cb21b234f0c5d212102764f2a21cb32806 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 24 May 2024 09:44:09 -0700 Subject: [PATCH 034/496] url quote the str data passed to rx.download (#3381) --- reflex/event.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reflex/event.py b/reflex/event.py index 96a59fdc1..4a2b34df3 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -3,6 +3,7 @@ from __future__ import annotations import inspect +import urllib.parse from base64 import b64encode from typing import ( Any, @@ -665,7 +666,7 @@ def download( if isinstance(data, str): # Caller provided a plain text string to download. - url = "data:text/plain," + data + url = "data:text/plain," + urllib.parse.quote(data) elif isinstance(data, Var): # Need to check on the frontend if the Var already looks like a data: URI. is_data_url = data._replace( From 7c2056e96071f6e562365ff6d6dfdf6a8ba6a4fb Mon Sep 17 00:00:00 2001 From: abulvenz Date: Sat, 25 May 2024 00:16:26 +0000 Subject: [PATCH 035/496] feat: Optionally comparing fields in Var.contains, e.g. on rx.Base based types. (#3375) * feat: Optionally comparing fields, e.g. on rx.Base based types. * feat: Minimally invasive change. Leave the current implementation as is. Added test. * fix: Supporting old-school python versions. * fix: Adding masenf's suggestions to use var instead of string. --- reflex/vars.py | 25 +++++++++++++++++-------- tests/test_var.py | 3 +++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/reflex/vars.py b/reflex/vars.py index 601caac5d..287502b43 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -838,19 +838,19 @@ class Var: if invoke_fn: # invoke the function on left operand. operation_name = ( - f"{left_operand_full_name}.{fn}({right_operand_full_name})" # type: ignore - ) + f"{left_operand_full_name}.{fn}({right_operand_full_name})" + ) # type: ignore else: # pass the operands as arguments to the function. operation_name = ( - f"{left_operand_full_name} {op} {right_operand_full_name}" # type: ignore - ) + f"{left_operand_full_name} {op} {right_operand_full_name}" + ) # type: ignore operation_name = f"{fn}({operation_name})" else: # apply operator to operands (left operand right_operand) operation_name = ( - f"{left_operand_full_name} {op} {right_operand_full_name}" # type: ignore - ) + f"{left_operand_full_name} {op} {right_operand_full_name}" + ) # type: ignore operation_name = format.wrap(operation_name, "(") else: # apply operator to left operand ( left_operand) @@ -1353,11 +1353,12 @@ class Var: "'in' operator not supported for Var types, use Var.contains() instead." ) - def contains(self, other: Any) -> Var: + def contains(self, other: Any, field: Union[Var, None] = None) -> Var: """Check if a var contains the object `other`. Args: other: The object to check. + field: Optionally specify a field to check on both object and the other var. Raises: VarTypeError: If the var is not a valid type: dict, list, tuple or str. @@ -1393,8 +1394,16 @@ class Var: raise VarTypeError( f"'in ' requires string as left operand, not {other._var_type}" ) + + _var_name = None + if field is None: + _var_name = f"{self._var_name}.includes({other._var_full_name})" + else: + field = Var.create_safe(field, _var_is_string=isinstance(field, str)) + _var_name = f"{self._var_name}.some(e=>e[{field._var_name_unwrapped}]==={other._var_full_name})" + return self._replace( - _var_name=f"{self._var_name}.includes({other._var_full_name})", + _var_name=_var_name, _var_type=bool, _var_is_string=False, merge_var_data=other._var_data, diff --git a/tests/test_var.py b/tests/test_var.py index a58c49392..3a1fb08a8 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -454,6 +454,9 @@ def test_str_contains(var, expected): other_var = BaseVar(_var_name="other", _var_type=str) assert str(var.contains(other_state_var)) == f"{{{expected}.includes(state.other)}}" assert str(var.contains(other_var)) == f"{{{expected}.includes(other)}}" + assert ( + str(var.contains("1", "hello")) == f'{{{expected}.some(e=>e[`hello`]==="1")}}' + ) @pytest.mark.parametrize( From 6c6eaaa55f63c4cb8961a1ba0db350b745287449 Mon Sep 17 00:00:00 2001 From: abulvenz Date: Tue, 28 May 2024 16:39:25 +0000 Subject: [PATCH 036/496] External assets (#3220) --- .gitignore | 1 + reflex/constants/base.py | 2 ++ reflex/constants/base.pyi | 5 +++ reflex/experimental/__init__.py | 2 ++ reflex/experimental/assets.py | 56 +++++++++++++++++++++++++++++ reflex/experimental/client_state.py | 6 ++-- reflex/experimental/hooks.py | 14 ++++---- tests/experimental/custom_script.js | 1 + tests/experimental/test_assets.py | 36 +++++++++++++++++++ 9 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 reflex/experimental/assets.py create mode 100644 tests/experimental/custom_script.js create mode 100644 tests/experimental/test_assets.py diff --git a/.gitignore b/.gitignore index c6acfc099..a570ed353 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ **/.DS_Store **/*.pyc +assets/external/* dist/* examples/ .idea diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 733859ad4..94559c346 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -21,6 +21,8 @@ class Dirs(SimpleNamespace): WEB = ".web" # The name of the assets directory. APP_ASSETS = "assets" + # The name of the assets directory for external ressource (a subfolder of APP_ASSETS). + EXTERNAL_APP_ASSETS = "external" # The name of the utils file. UTILS = "utils" # The name of the output static directory. diff --git a/reflex/constants/base.pyi b/reflex/constants/base.pyi index 90804a080..2fbeafd79 100644 --- a/reflex/constants/base.pyi +++ b/reflex/constants/base.pyi @@ -15,10 +15,15 @@ from types import SimpleNamespace from platformdirs import PlatformDirs IS_WINDOWS = platform.system() == "Windows" +IS_WINDOWS_BUN_SUPPORTED_MACHINE = IS_WINDOWS and platform.machine() in [ + "AMD64", + "x86_64", +] class Dirs(SimpleNamespace): WEB = ".web" APP_ASSETS = "assets" + EXTERNAL_APP_ASSETS = "external" UTILS = "utils" STATIC = "_static" STATE_PATH = "/".join([UTILS, "state"]) diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index 6972fdfe0..b4ebc1086 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -8,6 +8,7 @@ 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 @@ -17,6 +18,7 @@ warn( ) _x = SimpleNamespace( + asset=asset, client_state=ClientStateVar.create, hooks=hooks, layout=layout, diff --git a/reflex/experimental/assets.py b/reflex/experimental/assets.py new file mode 100644 index 000000000..736390154 --- /dev/null +++ b/reflex/experimental/assets.py @@ -0,0 +1,56 @@ +"""Helper functions for adding assets to the app.""" +import inspect +from pathlib import Path +from typing import Optional + +from reflex import constants + + +def asset(relative_filename: str, subfolder: Optional[str] = None) -> str: + """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. + + Raises: + FileNotFoundError: If the file does not exist. + + Returns: + The relative URL to the copied asset. + """ + # Determine the file by which the asset is exposed. + calling_file = inspect.stack()[1].filename + module = inspect.getmodule(inspect.stack()[1][0]) + assert module is not None + caller_module_path = module.__name__.replace(".", "/") + + subfolder = f"{caller_module_path}/{subfolder}" if subfolder else caller_module_path + + src_file = Path(calling_file).parent / relative_filename + + assets = constants.Dirs.APP_ASSETS + external = constants.Dirs.EXTERNAL_APP_ASSETS + + if not src_file.exists(): + raise FileNotFoundError(f"File not found: {src_file}") + + # Create the asset folder in the currently compiling app. + asset_folder = Path.cwd() / assets / external / subfolder + asset_folder.mkdir(parents=True, exist_ok=True) + + dst_file = asset_folder / relative_filename + + if not dst_file.exists(): + dst_file.symlink_to(src_file) + + asset_url = f"/{external}/{subfolder}/{relative_filename}" + return asset_url diff --git a/reflex/experimental/client_state.py b/reflex/experimental/client_state.py index 93405b29f..9282c4721 100644 --- a/reflex/experimental/client_state.py +++ b/reflex/experimental/client_state.py @@ -2,7 +2,7 @@ import dataclasses import sys -from typing import Any, Callable, Optional, Type +from typing import Any, Callable, Optional, Type, Union from reflex import constants from reflex.event import EventChain, EventHandler, EventSpec, call_script @@ -171,7 +171,9 @@ class ClientStateVar(Var): ) ) - def retrieve(self, callback: EventHandler | Callable | None = None) -> EventSpec: + def retrieve( + self, callback: Union[EventHandler, Callable, None] = None + ) -> EventSpec: """Pass the value of the client state variable to a backend EventHandler. The event handler must `yield` or `return` the EventSpec to trigger the event. diff --git a/reflex/experimental/hooks.py b/reflex/experimental/hooks.py index 9040abd4d..5706b18d6 100644 --- a/reflex/experimental/hooks.py +++ b/reflex/experimental/hooks.py @@ -1,10 +1,12 @@ """Add standard Hooks wrapper for React.""" +from typing import Optional, Union + from reflex.utils.imports import ImportVar from reflex.vars import Var, VarData -def _add_react_import(v: Var | None, tags: str | list): +def _add_react_import(v: Optional[Var], tags: Union[str, list]): if v is None: return @@ -16,7 +18,7 @@ def _add_react_import(v: Var | None, tags: str | list): ) -def const(name, value) -> Var | None: +def const(name, value) -> Optional[Var]: """Create a constant Var. Args: @@ -31,7 +33,7 @@ def const(name, value) -> Var | None: return Var.create(f"const {name} = {value}") -def useCallback(func, deps) -> Var | None: +def useCallback(func, deps) -> Optional[Var]: """Create a useCallback hook with a function and dependencies. Args: @@ -49,7 +51,7 @@ def useCallback(func, deps) -> Var | None: return v -def useContext(context) -> Var | None: +def useContext(context) -> Optional[Var]: """Create a useContext hook with a context. Args: @@ -63,7 +65,7 @@ def useContext(context) -> Var | None: return v -def useRef(default) -> Var | None: +def useRef(default) -> Optional[Var]: """Create a useRef hook with a default value. Args: @@ -77,7 +79,7 @@ def useRef(default) -> Var | None: return v -def useState(var_name, default=None) -> Var | None: +def useState(var_name, default=None) -> Optional[Var]: """Create a useState hook with a variable name and setter name. Args: diff --git a/tests/experimental/custom_script.js b/tests/experimental/custom_script.js new file mode 100644 index 000000000..81bae3136 --- /dev/null +++ b/tests/experimental/custom_script.js @@ -0,0 +1 @@ +const test = "inside custom_script.js"; \ No newline at end of file diff --git a/tests/experimental/test_assets.py b/tests/experimental/test_assets.py new file mode 100644 index 000000000..8037bcc75 --- /dev/null +++ b/tests/experimental/test_assets.py @@ -0,0 +1,36 @@ +import shutil +from pathlib import Path + +import pytest + +import reflex as rx + + +def test_asset(): + # Test the asset function. + + # The asset function copies a file to the app's external assets directory. + asset = rx._x.asset("custom_script.js", "subfolder") + assert asset == "/external/test_assets/subfolder/custom_script.js" + result_file = Path( + Path.cwd(), "assets/external/test_assets/subfolder/custom_script.js" + ) + assert result_file.exists() + + # Running a second time should not raise an error. + asset = rx._x.asset("custom_script.js", "subfolder") + + # Test the asset function without a subfolder. + asset = rx._x.asset("custom_script.js") + assert asset == "/external/test_assets/custom_script.js" + result_file = Path(Path.cwd(), "assets/external/test_assets/custom_script.js") + assert result_file.exists() + + # clean up + shutil.rmtree(Path.cwd() / "assets/external") + + with pytest.raises(FileNotFoundError): + asset = rx._x.asset("non_existent_file.js") + + # Nothing is done to assets when file does not exist. + assert not Path(Path.cwd() / "assets/external").exists() From 33f71c6eefcb21ac371f30ceec0191039bf53476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Tue, 28 May 2024 19:53:26 +0200 Subject: [PATCH 037/496] add mapping between client_token and socket id (#3388) --- reflex/app.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/reflex/app.py b/reflex/app.py index 9481204cb..98ad1eacf 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1302,6 +1302,12 @@ class EventNamespace(AsyncNamespace): # The application object. app: App + # Keep a mapping between socket ID and client token. + token_to_sid: dict[str, str] = {} + + # Keep a mapping between client token and socket ID. + sid_to_token: dict[str, str] = {} + def __init__(self, namespace: str, app: App): """Initialize the event namespace. @@ -1327,7 +1333,9 @@ class EventNamespace(AsyncNamespace): Args: sid: The Socket.IO session id. """ - pass + disconnect_token = self.sid_to_token.pop(sid, None) + if disconnect_token: + self.token_to_sid.pop(disconnect_token, None) async def emit_update(self, update: StateUpdate, sid: str) -> None: """Emit an update to the client. @@ -1351,6 +1359,9 @@ class EventNamespace(AsyncNamespace): # Get the event. event = Event.parse_raw(data) + self.token_to_sid[event.token] = sid + self.sid_to_token[sid] = event.token + # Get the event environment. assert self.app.sio is not None environ = self.app.sio.get_environ(sid, self.namespace) From ac1c660bf017a0259a1c64d2962a16887839a71e Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 28 May 2024 12:21:28 -0700 Subject: [PATCH 038/496] Radix Themes + Tailwind Harmony (#3355) --- reflex/.templates/web/postcss.config.js | 1 + reflex/.templates/web/styles/tailwind.css | 5 ++- reflex/components/radix/themes/base.py | 46 +++++++++++++---------- reflex/components/radix/themes/base.pyi | 5 ++- reflex/constants/installer.py | 1 + 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/reflex/.templates/web/postcss.config.js b/reflex/.templates/web/postcss.config.js index 33ad091d2..1e7129835 100644 --- a/reflex/.templates/web/postcss.config.js +++ b/reflex/.templates/web/postcss.config.js @@ -1,5 +1,6 @@ module.exports = { plugins: { + "postcss-import": {}, tailwindcss: {}, autoprefixer: {}, }, diff --git a/reflex/.templates/web/styles/tailwind.css b/reflex/.templates/web/styles/tailwind.css index b5c61c956..e1c383749 100644 --- a/reflex/.templates/web/styles/tailwind.css +++ b/reflex/.templates/web/styles/tailwind.css @@ -1,3 +1,6 @@ -@tailwind base; +@import "tailwindcss/base"; + +@import "@radix-ui/themes/styles.css"; + @tailwind components; @tailwind utilities; diff --git a/reflex/components/radix/themes/base.py b/reflex/components/radix/themes/base.py index 559d10239..8347877e6 100644 --- a/reflex/components/radix/themes/base.py +++ b/reflex/components/radix/themes/base.py @@ -6,7 +6,8 @@ from typing import Any, Dict, Literal from reflex.components import Component from reflex.components.tags import Tag -from reflex.utils import imports +from reflex.config import get_config +from reflex.utils.imports import ImportVar from reflex.vars import Var LiteralAlign = Literal["start", "center", "end", "baseline", "stretch"] @@ -208,18 +209,23 @@ class Theme(RadixThemesComponent): children = [ThemePanel.create(), *children] return super().create(*children, **props) - def _get_imports(self) -> imports.ImportDict: - return imports.merge_imports( - super()._get_imports(), - { - "": [ - imports.ImportVar(tag="@radix-ui/themes/styles.css", install=False) - ], - "/utils/theme.js": [ - imports.ImportVar(tag="theme", is_default=True), - ], - }, - ) + def add_imports(self) -> dict[str, list[ImportVar] | ImportVar]: + """Add imports for the Theme component. + + Returns: + The import dict. + """ + _imports: dict[str, list[ImportVar] | ImportVar] = { + "/utils/theme.js": [ImportVar(tag="theme", is_default=True)], + } + if get_config().tailwind is None: + # When tailwind is disabled, import the radix-ui styles directly because they will + # not be included in the tailwind.css file. + _imports[""] = ImportVar( + tag="@radix-ui/themes/styles.css", + install=False, + ) + return _imports def _render(self, props: dict[str, Any] | None = None) -> Tag: tag = super()._render(props) @@ -243,13 +249,13 @@ class ThemePanel(RadixThemesComponent): # Whether the panel is open. Defaults to False. default_open: Var[bool] - def _get_imports(self) -> dict[str, list[imports.ImportVar]]: - return imports.merge_imports( - super()._get_imports(), - { - "react": [imports.ImportVar(tag="useEffect")], - }, - ) + def add_imports(self) -> dict[str, str]: + """Add imports for the ThemePanel component. + + Returns: + The import dict. + """ + return {"react": "useEffect"} def _get_hooks(self) -> str | None: # The panel freezes the tab if the user color preference differs from the diff --git a/reflex/components/radix/themes/base.pyi b/reflex/components/radix/themes/base.pyi index d7ee29801..34af32b55 100644 --- a/reflex/components/radix/themes/base.pyi +++ b/reflex/components/radix/themes/base.pyi @@ -10,7 +10,8 @@ from reflex.style import Style from typing import Any, Dict, Literal from reflex.components import Component from reflex.components.tags import Tag -from reflex.utils import imports +from reflex.config import get_config +from reflex.utils.imports import ImportVar from reflex.vars import Var LiteralAlign = Literal["start", "center", "end", "baseline", "stretch"] @@ -579,8 +580,10 @@ class Theme(RadixThemesComponent): A new component instance. """ ... + def add_imports(self) -> dict[str, list[ImportVar] | ImportVar]: ... class ThemePanel(RadixThemesComponent): + def add_imports(self) -> dict[str, str]: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 3cca265e6..9ee68d345 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -123,4 +123,5 @@ class PackageJson(SimpleNamespace): DEV_DEPENDENCIES = { "autoprefixer": "10.4.14", "postcss": "8.4.31", + "postcss-import": "16.1.0", } From 93de4070078481449aaa026d669a686831589fc1 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Tue, 28 May 2024 21:27:27 +0200 Subject: [PATCH 039/496] Explicit deps and interval for computed vars (#3231) --- integration/test_computed_vars.py | 210 ++++++++++++++++++++++++++++++ reflex/state.py | 16 +++ reflex/vars.py | 84 +++++++++++- reflex/vars.pyi | 16 +++ 4 files changed, 321 insertions(+), 5 deletions(-) create mode 100644 integration/test_computed_vars.py diff --git a/integration/test_computed_vars.py b/integration/test_computed_vars.py new file mode 100644 index 000000000..2523248a9 --- /dev/null +++ b/integration/test_computed_vars.py @@ -0,0 +1,210 @@ +"""Test computed vars.""" + +from __future__ import annotations + +import time +from typing import Generator + +import pytest +from selenium.webdriver.common.by import By + +from reflex.testing import DEFAULT_TIMEOUT, AppHarness, WebDriver + + +def ComputedVars(): + """Test app for computed vars.""" + import reflex as rx + + class State(rx.State): + count: int = 0 + + # cached var with dep on count + @rx.cached_var(interval=15) + def count1(self) -> int: + return self.count + + # same as above, different notation + @rx.var(interval=15, cache=True) + def count2(self) -> int: + return self.count + + # explicit disabled auto_deps + @rx.var(interval=15, cache=True, auto_deps=False) + def count3(self) -> int: + # this will not add deps, because auto_deps is False + print(self.count1) + print(self.count2) + + return self.count + + # explicit dependency on count1 var + @rx.cached_var(deps=[count1], auto_deps=False) + def depends_on_count1(self) -> int: + return self.count + + @rx.var(deps=[count3], auto_deps=False, cache=True) + def depends_on_count3(self) -> int: + return self.count + + def increment(self): + self.count += 1 + + def mark_dirty(self): + self._mark_dirty() + + def index() -> rx.Component: + return rx.center( + rx.vstack( + rx.input( + id="token", + value=State.router.session.client_token, + is_read_only=True, + ), + rx.button("Increment", on_click=State.increment, id="increment"), + rx.button("Do nothing", on_click=State.mark_dirty, id="mark_dirty"), + rx.text("count:"), + rx.text(State.count, id="count"), + rx.text("count1:"), + rx.text(State.count1, id="count1"), + rx.text("count2:"), + rx.text(State.count2, id="count2"), + rx.text("count3:"), + rx.text(State.count3, id="count3"), + rx.text("depends_on_count1:"), + rx.text( + State.depends_on_count1, + id="depends_on_count1", + ), + rx.text("depends_on_count3:"), + rx.text( + State.depends_on_count3, + id="depends_on_count3", + ), + ), + ) + + # raise Exception(State.count3._deps(objclass=State)) + app = rx.App() + app.add_page(index) + + +@pytest.fixture(scope="module") +def computed_vars( + tmp_path_factory, +) -> Generator[AppHarness, None, None]: + """Start ComputedVars app at tmp_path via AppHarness. + + Args: + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + running AppHarness instance + """ + with AppHarness.create( + root=tmp_path_factory.mktemp(f"computed_vars"), + app_source=ComputedVars, # type: ignore + ) as harness: + yield harness + + +@pytest.fixture +def driver(computed_vars: AppHarness) -> Generator[WebDriver, None, None]: + """Get an instance of the browser open to the computed_vars app. + + Args: + computed_vars: harness for ComputedVars app + + Yields: + WebDriver instance. + """ + assert computed_vars.app_instance is not None, "app is not running" + driver = computed_vars.frontend() + try: + yield driver + finally: + driver.quit() + + +@pytest.fixture() +def token(computed_vars: AppHarness, driver: WebDriver) -> str: + """Get a function that returns the active token. + + Args: + computed_vars: harness for ComputedVars app. + driver: WebDriver instance. + + Returns: + The token for the connected client + """ + assert computed_vars.app_instance is not None + token_input = driver.find_element(By.ID, "token") + assert token_input + + # wait for the backend connection to send the token + token = computed_vars.poll_for_value(token_input, timeout=DEFAULT_TIMEOUT * 2) + assert token is not None + + return token + + +def test_computed_vars( + computed_vars: AppHarness, + driver: WebDriver, + token: str, +): + """Test that computed vars are working as expected. + + Args: + computed_vars: harness for ComputedVars app. + driver: WebDriver instance. + token: The token for the connected client. + """ + assert computed_vars.app_instance is not None + + count = driver.find_element(By.ID, "count") + assert count + assert count.text == "0" + + count1 = driver.find_element(By.ID, "count1") + assert count1 + assert count1.text == "0" + + count2 = driver.find_element(By.ID, "count2") + assert count2 + assert count2.text == "0" + + count3 = driver.find_element(By.ID, "count3") + assert count3 + assert count3.text == "0" + + depends_on_count1 = driver.find_element(By.ID, "depends_on_count1") + assert depends_on_count1 + assert depends_on_count1.text == "0" + + depends_on_count3 = driver.find_element(By.ID, "depends_on_count3") + assert depends_on_count3 + assert depends_on_count3.text == "0" + + increment = driver.find_element(By.ID, "increment") + assert increment.is_enabled() + + mark_dirty = driver.find_element(By.ID, "mark_dirty") + assert mark_dirty.is_enabled() + + mark_dirty.click() + + increment.click() + assert computed_vars.poll_for_content(count, timeout=2, exp_not_equal="0") == "1" + assert computed_vars.poll_for_content(count1, timeout=2, exp_not_equal="0") == "1" + assert computed_vars.poll_for_content(count2, timeout=2, exp_not_equal="0") == "1" + + mark_dirty.click() + with pytest.raises(TimeoutError): + computed_vars.poll_for_content(count3, timeout=5, exp_not_equal="0") + + time.sleep(10) + assert count3.text == "0" + assert depends_on_count3.text == "0" + mark_dirty.click() + assert computed_vars.poll_for_content(count3, timeout=2, exp_not_equal="0") == "1" + assert depends_on_count3.text == "1" diff --git a/reflex/state.py b/reflex/state.py index e778946c0..e889fe42e 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1536,6 +1536,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if actual_var is not None: actual_var.mark_dirty(instance=self) + def _expired_computed_vars(self) -> set[str]: + """Determine ComputedVars that need to be recalculated based on the expiration time. + + Returns: + Set of computed vars to include in the delta. + """ + return set( + cvar + for cvar in self.computed_vars + if self.computed_vars[cvar].needs_update(instance=self) + ) + def _dirty_computed_vars(self, from_vars: set[str] | None = None) -> set[str]: """Determine ComputedVars that need to be recalculated based on the given vars. @@ -1588,6 +1600,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # and always dirty computed vars (cache=False) delta_vars = ( self.dirty_vars.intersection(self.base_vars) + .union(self.dirty_vars.intersection(self.computed_vars)) .union(self._dirty_computed_vars()) .union(self._always_dirty_computed_vars) ) @@ -1621,6 +1634,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): self.parent_state.dirty_substates.add(self.get_name()) self.parent_state._mark_dirty() + # Append expired computed vars to dirty_vars to trigger recalculation + self.dirty_vars.update(self._expired_computed_vars()) + # have to mark computed vars dirty to allow access to newly computed # values within the same ComputedVar function self._mark_dirty_computed_vars() diff --git a/reflex/vars.py b/reflex/vars.py index 287502b43..cf6f2eed6 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -4,6 +4,7 @@ from __future__ import annotations import contextlib import dataclasses +import datetime import dis import functools import inspect @@ -1873,13 +1874,26 @@ class ComputedVar(Var, property): # Whether to track dependencies and cache computed values _cache: bool = dataclasses.field(default=False) - _initial_value: Any | types.Unset = dataclasses.field(default_factory=types.Unset) + # The initial value of the computed var + _initial_value: Any | types.Unset = dataclasses.field(default=types.Unset()) + + # Explicit var dependencies to track + _static_deps: set[str] = dataclasses.field(default_factory=set) + + # Whether var dependencies should be auto-determined + _auto_deps: bool = dataclasses.field(default=True) + + # Interval at which the computed var should be updated + _update_interval: Optional[datetime.timedelta] = dataclasses.field(default=None) def __init__( self, fget: Callable[[BaseState], Any], initial_value: Any | types.Unset = types.Unset(), cache: bool = False, + deps: Optional[List[Union[str, Var]]] = None, + auto_deps: bool = True, + interval: Optional[Union[int, datetime.timedelta]] = None, **kwargs, ): """Initialize a ComputedVar. @@ -1888,10 +1902,22 @@ class ComputedVar(Var, property): fget: The getter function. initial_value: The initial value of the computed var. cache: Whether to cache the computed value. + deps: Explicit var dependencies to track. + auto_deps: Whether var dependencies should be auto-determined. + interval: Interval at which the computed var should be updated. **kwargs: additional attributes to set on the instance """ self._initial_value = initial_value self._cache = cache + if isinstance(interval, int): + interval = datetime.timedelta(seconds=interval) + self._update_interval = interval + if deps is None: + deps = [] + self._static_deps = { + dep._var_name if isinstance(dep, Var) else dep for dep in deps + } + self._auto_deps = auto_deps property.__init__(self, fget) kwargs["_var_name"] = kwargs.pop("_var_name", fget.__name__) kwargs["_var_type"] = kwargs.pop("_var_type", self._determine_var_type()) @@ -1912,6 +1938,9 @@ class ComputedVar(Var, property): fget=kwargs.get("fget", self.fget), initial_value=kwargs.get("initial_value", self._initial_value), cache=kwargs.get("cache", self._cache), + deps=kwargs.get("deps", self._static_deps), + auto_deps=kwargs.get("auto_deps", self._auto_deps), + interval=kwargs.get("interval", self._update_interval), _var_name=kwargs.get("_var_name", self._var_name), _var_type=kwargs.get("_var_type", self._var_type), _var_is_local=kwargs.get("_var_is_local", self._var_is_local), @@ -1932,7 +1961,32 @@ class ComputedVar(Var, property): """ return f"__cached_{self._var_name}" - def __get__(self, instance, owner): + @property + def _last_updated_attr(self) -> str: + """Get the attribute used to store the last updated timestamp. + + Returns: + An attribute name. + """ + return f"__last_updated_{self._var_name}" + + def needs_update(self, instance: BaseState) -> bool: + """Check if the computed var needs to be updated. + + Args: + instance: The state instance that the computed var is attached to. + + Returns: + True if the computed var needs to be updated, False otherwise. + """ + if self._update_interval is None: + return False + last_updated = getattr(instance, self._last_updated_attr, None) + if last_updated is None: + return True + return datetime.datetime.now() - last_updated > self._update_interval + + def __get__(self, instance: BaseState | None, owner): """Get the ComputedVar value. If the value is already cached on the instance, return the cached value. @@ -1948,10 +2002,13 @@ class ComputedVar(Var, property): return super().__get__(instance, owner) # handle caching - if not hasattr(instance, self._cache_attr): + if not hasattr(instance, self._cache_attr) or self.needs_update(instance): + # Set cache attr on state instance. setattr(instance, self._cache_attr, super().__get__(instance, owner)) # Ensure the computed var gets serialized to redis. instance._was_touched = True + # Set the last updated timestamp on the state instance. + setattr(instance, self._last_updated_attr, datetime.datetime.now()) return getattr(instance, self._cache_attr) def _deps( @@ -1978,7 +2035,9 @@ class ComputedVar(Var, property): VarValueError: if the function references the get_state, parent_state, or substates attributes (cannot track deps in a related state, only implicitly via parent state). """ - d = set() + if not self._auto_deps: + return self._static_deps + d = self._static_deps.copy() if obj is None: fget = property.__getattribute__(self, "fget") if fget is not None: @@ -2076,6 +2135,9 @@ def computed_var( fget: Callable[[BaseState], Any] | None = None, initial_value: Any | None = None, cache: bool = False, + deps: Optional[List[Union[str, Var]]] = None, + auto_deps: bool = True, + interval: Optional[Union[datetime.timedelta, int]] = None, **kwargs, ) -> ComputedVar | Callable[[Callable[[BaseState], Any]], ComputedVar]: """A ComputedVar decorator with or without kwargs. @@ -2084,19 +2146,31 @@ def computed_var( fget: The getter function. initial_value: The initial value of the computed var. cache: Whether to cache the computed value. + deps: Explicit var dependencies to track. + auto_deps: Whether var dependencies should be auto-determined. + interval: Interval at which the computed var should be updated. **kwargs: additional attributes to set on the instance Returns: A ComputedVar instance. + + Raises: + ValueError: If caching is disabled and an update interval is set. """ + if cache is False and interval is not None: + raise ValueError("Cannot set update interval without caching.") + if fget is not None: return ComputedVar(fget=fget, cache=cache) - def wrapper(fget): + def wrapper(fget: Callable[[BaseState], Any]) -> ComputedVar: return ComputedVar( fget=fget, initial_value=initial_value, cache=cache, + deps=deps, + auto_deps=auto_deps, + interval=interval, **kwargs, ) diff --git a/reflex/vars.pyi b/reflex/vars.pyi index 169e2d919..01b276342 100644 --- a/reflex/vars.pyi +++ b/reflex/vars.pyi @@ -2,6 +2,7 @@ from __future__ import annotations +import datetime from dataclasses import dataclass from _typeshed import Incomplete from reflex import constants as constants @@ -141,6 +142,7 @@ class ComputedVar(Var): def _deps(self, objclass: Type, obj: Optional[FunctionType] = ...) -> Set[str]: ... def _replace(self, merge_var_data=None, **kwargs: Any) -> ComputedVar: ... def mark_dirty(self, instance) -> None: ... + def needs_update(self, instance) -> bool: ... def _determine_var_type(self) -> Type: ... @overload def __init__( @@ -155,10 +157,24 @@ class ComputedVar(Var): def computed_var( fget: Callable[[BaseState], Any] | None = None, initial_value: Any | None = None, + cache: bool = False, + deps: Optional[List[Union[str, Var]]] = None, + auto_deps: bool = True, + interval: Optional[Union[datetime.timedelta, int]] = None, **kwargs, ) -> Callable[[Callable[[Any], Any]], ComputedVar]: ... @overload def computed_var(fget: Callable[[Any], Any]) -> ComputedVar: ... +@overload +def cached_var( + fget: Callable[[BaseState], Any] | None = None, + initial_value: Any | None = None, + deps: Optional[List[Union[str, Var]]] = None, + auto_deps: bool = True, + interval: Optional[Union[datetime.timedelta, int]] = None, + **kwargs, +) -> Callable[[Callable[[Any], Any]], ComputedVar]: ... +@overload def cached_var(fget: Callable[[Any], Any]) -> ComputedVar: ... class CallableVar(BaseVar): From 4b939caa7f2ce1f070ec4c944e8caa6d1c706efc Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Wed, 29 May 2024 09:28:41 -0700 Subject: [PATCH 040/496] [REF-2895]Benchmarks getting skipped on merge (#3369) --- .github/workflows/benchmarks.yml | 2 +- .github/workflows/integration_tests.yml | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 617519f75..4fbc937c2 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -23,7 +23,7 @@ env: jobs: reflex-web: - if: github.event.pull_request.merged == true +# if: github.event.pull_request.merged == true strategy: fail-fast: false matrix: diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index b83107504..0febb49e2 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -100,7 +100,7 @@ jobs: npm -v poetry run bash scripts/integration.sh ./reflex-examples/counter dev - name: Measure and upload .web size - if: ${{ env.DATABASE_URL && github.event.pull_request.merged == true }} + if: ${{ env.DATABASE_URL}} run: poetry run python scripts/benchmarks/benchmark_reflex_size.py --os "${{ matrix.os }}" --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" @@ -108,14 +108,12 @@ jobs: --branch-name "${{ github.head_ref || github.ref_name }}" --measurement-type "counter-app-dot-web" --path ./reflex-examples/counter/.web - name: Install hyperfine - if: github.event.pull_request.merged == true - run: cargo install --locked hyperfine + run: cargo install hyperfine - name: Benchmark imports - if: github.event.pull_request.merged == true working-directory: ./reflex-examples/counter run: hyperfine --warmup 3 "export POETRY_VIRTUALENVS_PATH=../../.venv; poetry run python counter/counter.py" --show-output --export-json "${{ env.OUTPUT_FILE }}" --shell bash - name: Upload Benchmarks - if : ${{ env.DATABASE_URL && github.event.pull_request.merged == true }} + if : ${{ env.DATABASE_URL }} run: poetry run python scripts/benchmarks/benchmark_imports.py --os "${{ matrix.os }}" --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" @@ -166,7 +164,7 @@ jobs: npm -v poetry run bash scripts/integration.sh ./reflex-web prod - name: Measure and upload .web size - if: ${{ env.DATABASE_URL && github.event.pull_request.merged == true }} + if: ${{ env.DATABASE_URL}} run: poetry run python scripts/benchmarks/benchmark_reflex_size.py --os "${{ matrix.os }}" --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" From c7064b9a240d2375157df838d1e7d87966fbe815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Wed, 29 May 2024 20:49:04 +0200 Subject: [PATCH 041/496] add config knob for react_strict_mode. default to True. also cleanup unused code (#3389) --- reflex/config.py | 3 + reflex/config.pyi | 112 ---------------------------------- reflex/constants/__init__.py | 2 - reflex/utils/prerequisites.py | 15 +---- 4 files changed, 4 insertions(+), 128 deletions(-) delete mode 100644 reflex/config.pyi diff --git a/reflex/config.py b/reflex/config.py index 769b94328..28d2e6a5f 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -202,6 +202,9 @@ class Config(Base): # Whether to enable or disable nextJS gzip compression. next_compression: bool = True + # Whether to use React strict mode in nextJS + react_strict_mode: bool = True + # Additional frontend packages to install. frontend_packages: List[str] = [] diff --git a/reflex/config.pyi b/reflex/config.pyi deleted file mode 100644 index f7dfde770..000000000 --- a/reflex/config.pyi +++ /dev/null @@ -1,112 +0,0 @@ -""" Generated with stubgen from mypy, then manually edited, do not regen.""" - -from reflex import constants as constants -from reflex.base import Base as Base -from reflex.utils import console as console -from typing import Any, Dict, List, Optional, overload - -class DBConfig(Base): - engine: str - username: Optional[str] - password: Optional[str] - host: Optional[str] - port: Optional[int] - database: str - - def __init__( - self, - database: str, - engine: str, - username: Optional[str] = None, - password: Optional[str] = None, - host: Optional[str] = None, - port: Optional[int] = None, - ): ... - @classmethod - def postgresql( - cls, - database: str, - username: str, - password: str | None = ..., - host: str | None = ..., - port: int | None = ..., - ) -> DBConfig: ... - @classmethod - def postgresql_psycopg2( - cls, - database: str, - username: str, - password: str | None = ..., - host: str | None = ..., - port: int | None = ..., - ) -> DBConfig: ... - @classmethod - def sqlite(cls, database: str) -> DBConfig: ... - def get_url(self) -> str: ... - -class Config(Base): - class Config: - validate_assignment: bool - app_name: str - loglevel: constants.LogLevel - frontend_port: int - frontend_path: str - backend_port: int - api_url: str - deploy_url: Optional[str] - backend_host: str - db_url: Optional[str] - redis_url: Optional[str] - telemetry_enabled: bool - bun_path: str - cors_allowed_origins: List[str] - tailwind: Optional[Dict[str, Any]] - timeout: int - next_compression: bool - event_namespace: Optional[str] - frontend_packages: List[str] - rxdeploy_url: Optional[str] - cp_backend_url: str - cp_web_url: str - username: Optional[str] - gunicorn_worker_class: str - gunicorn_workers: Optional[int] - - def __init__( - self, - *args, - app_name: str, - loglevel: Optional[constants.LogLevel] = None, - frontend_port: Optional[int] = None, - frontend_path: Optional[str] = None, - backend_port: Optional[int] = None, - api_url: Optional[str] = None, - deploy_url: Optional[str] = None, - backend_host: Optional[str] = None, - db_url: Optional[str] = None, - redis_url: Optional[str] = None, - telemetry_enabled: Optional[bool] = None, - bun_path: Optional[str] = None, - cors_allowed_origins: Optional[List[str]] = None, - tailwind: Optional[Dict[str, Any]] = None, - timeout: Optional[int] = None, - next_compression: Optional[bool] = None, - event_namespace: Optional[str] = None, - frontend_packages: Optional[List[str]] = None, - rxdeploy_url: Optional[str] = None, - cp_backend_url: Optional[str] = None, - cp_web_url: Optional[str] = None, - username: Optional[str] = None, - gunicorn_worker_class: Optional[str] = None, - gunicorn_workers: Optional[int] = None, - **kwargs - ) -> None: ... - @property - def module(self) -> str: ... - @staticmethod - def check_deprecated_values(**kwargs) -> None: ... - def update_from_env(self) -> None: ... - def get_event_namespace(self) -> str: ... - def _set_persistent(self, **kwargs) -> None: ... - -def get_config(reload: bool = ...) -> Config: ... diff --git a/reflex/constants/__init__.py b/reflex/constants/__init__.py index 1f3325a8a..c45a7f55d 100644 --- a/reflex/constants/__init__.py +++ b/reflex/constants/__init__.py @@ -35,7 +35,6 @@ from .compiler import ( ) from .config import ( ALEMBIC_CONFIG, - PRODUCTION_BACKEND_URL, Config, Expiration, GitIgnore, @@ -99,7 +98,6 @@ __ALL__ = [ Ping, POLLING_MAX_HTTP_BUFFER_SIZE, PYTEST_CURRENT_TEST, - PRODUCTION_BACKEND_URL, Reflex, RELOAD_CONFIG, RequirementsTxt, diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index f77e1d63c..8bdb4dde0 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -332,19 +332,6 @@ def parse_redis_url() -> str | dict | None: return dict(host=redis_url, port=int(redis_port), db=0) -def get_production_backend_url() -> str: - """Get the production backend URL. - - Returns: - The production backend URL. - """ - config = get_config() - return constants.PRODUCTION_BACKEND_URL.format( - username=config.username, - app_name=config.app_name, - ) - - def validate_app_name(app_name: str | None = None) -> str: """Validate the app name. @@ -625,7 +612,7 @@ def _update_next_config( next_config = { "basePath": config.frontend_path or "", "compress": config.next_compression, - "reactStrictMode": True, + "reactStrictMode": config.react_strict_mode, "trailingSlash": True, } if transpile_packages: From 7f054fda9df36ca5ce9d06f4e40de916d8f15dc4 Mon Sep 17 00:00:00 2001 From: Sagar Hedaoo Date: Thu, 30 May 2024 12:14:02 -0400 Subject: [PATCH 042/496] Updated in/readme.md with current readme (#3333) --- docs/in/README.md | 114 ++++++++++++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/docs/in/README.md b/docs/in/README.md index 378cfaaec..1e51b4909 100644 --- a/docs/in/README.md +++ b/docs/in/README.md @@ -1,25 +1,40 @@ ```diff Pynecone की तलाश हैं? आप सही रेपो में हैं। Pynecone का नाम Reflex में बदल दिया गया है। + + ``` +
-Reflex Logo -Reflex Logo +Reflex लोगो +Reflex लोगो
-### **✨ Python (पायथन) में परफॉर्मेंट, अनुकूलनयोग्य वेब ऐप्स। कुछ सेकंड्स में ही डिप्लॉय करें ✨** +### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨** + [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) ![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) +
---- -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) --- -## ⚙️ इंस्टॉलेशन +## [English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) + +# Reflex + +Reflex शुद्ध पायथन में पूर्ण-स्टैक वेब ऐप्स बनाने के लिए एक लाइब्रेरी है। + +मुख्य विशेषताएँ: + +- **शुद्ध पायथन** - अपने ऐप के फ्रंटएंड और बैकएंड को पायथन में लिखें, जावास्क्रिप्ट सीखने की जरूरत नहीं है। +- **पूर्ण लचीलापन** - Reflex के साथ शुरुआत करना आसान है, लेकिन यह जटिल ऐप्स के लिए भी स्केल कर सकता है। +- **तुरंत तैनाती** - बिल्डिंग के बाद, अपने ऐप को [एकल कमांड](https://reflex.dev/docs/hosting/deploy-quick-start/) के साथ तैनात करें या इसे अपने सर्वर पर होस्ट करें। + +Reflex के अंदर के कामकाज को जानने के लिए हमारे [आर्किटेक्चर पेज](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) को देखें। + +## ⚙️ इंस्टॉलेशन (Installation) एक टर्मिनल खोलें और चलाएं (Python 3.8+ की आवश्यकता है): @@ -27,11 +42,11 @@ Pynecone की तलाश हैं? आप सही रेपो में pip install reflex ``` -## 🥳 अपना पहला ऐप बनाएं +## 🥳 अपना पहला ऐप बनाएं (Create your first App) reflex को इंस्टॉल करने से ही reflex कमांड लाइन टूल भी इंस्टॉल हो जाता है। -सुनिश्चित करें कि इंस्टॉलेशन सफल थी, एक नया प्रोजेक्ट बनाकर इसे टेस्ट करें। ('my_app_name' की जगह अपने प्रोजेक्ट का नाम रखें): +सुनिश्चित करें कि इंस्टॉलेशन सफल थी, एक नया प्रोजेक्ट बनाकर इसे टेस्ट करें। ('my_app_name' की जगह अपने प्रोजेक्ट का नाम रखें): ```bash mkdir my_app_name @@ -51,14 +66,14 @@ reflex run अब आप my_app_name/my_app_name.py में source कोड को संशोधित कर सकते हैं। Reflex में तेज रिफ्रेश की सुविधा है, इसलिए जब आप अपनी कोड को सहेजते हैं, तो आप अपने बदलावों को तुरंत देख सकते हैं। -## 🫧 उदाहरण ऐप +## 🫧 उदाहरण ऐप (Example App) एक उदाहरण पर चलते हैं: DALL·E से एक इमेज उत्पन्न करने के लिए UI। सरलता के लिए, हम सिर्फ OpenAI API को बुलाते हैं, लेकिन आप इसे ML मॉडल से बदल सकते हैं locally।  
-A frontend wrapper for DALL·E, shown in the process of generating an image. +DALL·E के लिए एक फ्रंटएंड रैपर, छवि उत्पन्न करने की प्रक्रिया में दिखाया गया।
  @@ -69,10 +84,12 @@ reflex run import reflex as rx import openai -openai.api_key = "YOUR_API_KEY" +openai_client = openai.OpenAI() + class State(rx.State): """The app state.""" + prompt = "" image_url = "" processing = False @@ -85,33 +102,33 @@ class State(rx.State): self.processing, self.complete = True, False yield - response = openai.Image.create(prompt=self.prompt, n=1, size="1024x1024") - self.image_url = response["data"][0]["url"] + response = openai_client.images.generate( + prompt=self.prompt, n=1, size="1024x1024" + ) + self.image_url = response.data[0].url self.processing, self.complete = False, True - + def index(): return rx.center( rx.vstack( - rx.heading("DALL·E"), - rx.input(placeholder="Enter a prompt", on_blur=State.set_prompt), + rx.heading("DALL-E", font_size="1.5em"), + rx.input( + placeholder="Enter a prompt..", + on_blur=State.set_prompt, + width="25em", + ), rx.button( "Generate Image", on_click=State.get_image, - is_loading=State.processing, - width="100%", + width="25em", + loading=State.processing ), rx.cond( State.complete, - rx.image( - src=State.image_url, - height="25em", - width="25em", - ) + rx.image(src=State.image_url, width="20em"), ), - padding="2em", - shadow="lg", - border_radius="lg", + align="center", ), width="100%", height="100vh", @@ -119,10 +136,14 @@ def index(): # Add state and page to the app. app = rx.App() -app.add_page(index, title="reflex:DALL·E") +app.add_page(index, title="Reflex:DALL-E") ``` -## चलो इसे विस्तार से देखते हैं। +## इसे समझते हैं। + +
+DALL-E ऐप के बैकएंड और फ्रंटएंड भागों के बीच के अंतर की व्याख्या करता है। +
### **Reflex UI** @@ -156,7 +177,7 @@ class State(rx.State): स्टेट (state) ऐप में उन सभी वेरिएबल्स (vars) को परिभाषित करती है जो बदल सकती हैं और उन फ़ंक्शनों को जो उन्हें बदलते हैं। -यहां स्टेट (state) में `prompt` और `image_url` शामिल हैं। प्रगति और छवि दिखाने के लिए `processing` और `complete` बूलियन भी हैं। +यहां स्टेट (state) में `prompt` और `image_url` शामिल हैं। प्रगति और छवि दिखाने के लिए `processing` और `complete` बूलियन भी हैं। ### **इवेंट हैंडलर (Event Handlers)** @@ -168,8 +189,10 @@ def get_image(self): self.processing, self.complete = True, False yield - response = openai.Image.create(prompt=self.prompt, n=1, size="1024x1024") - self.image_url = response["data"][0]["url"] + response = openai_client.images.generate( + prompt=self.prompt, n=1, size="1024x1024" + ) + self.image_url = response.data[0].url self.processing, self.complete = False, True ``` @@ -197,33 +220,34 @@ app.add_page(index, title="DALL-E")
-📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Gallery](https://reflex.dev/docs/gallery)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy)   +📑 [दस्तावेज़](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [ब्लॉग](https://reflex.dev/blog)   |   📱 [कॉम्पोनेंट लाइब्रेरी](https://reflex.dev/docs/library)   |   🖼️ [गैलरी](https://reflex.dev/docs/gallery)   |   🛸 [तैनाती](https://reflex.dev/docs/hosting/deploy)  
## ✅ स्टेटस (Status) -रिफ्लेक्स को दिसंबर 2022 में पाइनकोन नाम से लॉन्च किया गया। +Reflex दिसंबर 2022 में Pynecone नाम से शुरू हुआ। -जुलाई 2023 तक, हम **Public Beta** (सार्वजनिक बीटा) चरण में हैं। +फरवरी 2024 तक, हमारी होस्टिंग सेवा अल्फा में है! इस समय कोई भी अपने ऐप्स को मुफ्त में तैनात कर सकता है। देखें हमारी [रोडमैप](https://github.com/reflex-dev/reflex/issues/2727) योजनाबद्ध चीज़ों को जानने के लिए। -- :white_check_mark: **Public Alpha** (सार्वजनिक अल्फा): कोई भी रिफ्लेक्स इंस्टॉल और उपयोग कर सकता है। कुछ इशू हो सकते हैं, लेकिन हम उन्हें सुलझाने के लिए सक्रिय रूप से काम कर रहे हैं। -- :large_orange_diamond: **Public Beta** (सार्वजनिक बीटा): गैर-उद्यम उपयोग-मामलों के लिए स्थिर। -- **Public Hosting Beta** (सार्वजनिक होस्टिंग बीटा): _Optionally_, अपने ऐप्स को रिफ्लेक्स पर डिप्लॉइ और होस्ट करें! -- **Public** (सार्वजनिक): रिफ्लेक्स उत्पादन के लिए तैयार है। - -रिफ्लेक्स में हर सप्ताह नई रिलीज़ और सुविधाएँ आ रही हैं! अपडेट रहने के लिए इस रिपॉजिटरी को :star: स्टार करें और समय-समय पर अवश्य देखें :eyes:। +Reflex में हर सप्ताह नए रिलीज़ और फीचर्स आ रहे हैं! सुनिश्चित करें कि ⭐ स्टार और 👀 वॉच इस रेपोजिटरी को अपडेट रहने के लिए। ## (योगदान) Contributing हम हर तरह के योगदान का स्वागत करते हैं! रिफ्लेक्स कम्यूनिटी में शुरुआत करने के कुछ अच्छे तरीके नीचे दिए गए हैं। -- **Join Our Discord** (डिस्कॉर्ड सर्वर से जुड़ें): Our [Discord](https://discord.gg/T5WSbC2YtQ) हमारा डिस्कॉर्ड रिफ्लेक्स प्रोजेक्ट पर सहायता प्राप्त करने और आप कैसे योगदान दे सकते हैं, इस पर चर्चा करने के लिए सबसे अच्छी जगह है। -- **GitHub Discussions** (गिटहब चर्चाएँ): उन सुविधाओं के बारे में बात करने का एक शानदार तरीका जिन्हें आप जोड़ना चाहते हैं या ऐसी चीज़ें जो भ्रमित करने वाली हैं/स्पष्टीकरण की आवश्यकता है। -- **GitHub Issues** (गिटहब समस्याएं): ये बग की रिपोर्ट करने का एक शानदार तरीका है। इसके अतिरिक्त, आप किसी मौजूदा समस्या को हल करने का प्रयास कर सकते हैं और एक पीआर सबमिट कर सकते हैं। +- **Join Our Discord** (डिस्कॉर्ड सर्वर से जुड़ें): Our [Discord](https://discord.gg/T5WSbC2YtQ) हमारा डिस्कॉर्ड रिफ्लेक्स प्रोजेक्ट पर सहायता प्राप्त करने और आप कैसे योगदान दे सकते हैं, इस पर चर्चा करने के लिए सबसे अच्छी जगह है। +- **GitHub Discussions** (गिटहब चर्चाएँ): उन सुविधाओं के बारे में बात करने का एक शानदार तरीका जिन्हें आप जोड़ना चाहते हैं या ऐसी चीज़ें जो भ्रमित करने वाली हैं/स्पष्टीकरण की आवश्यकता है। +- **GitHub Issues** (गिटहब समस्याएं): ये [बग](https://github.com/reflex-dev/reflex/issues) की रिपोर्ट करने का एक शानदार तरीका है। इसके अतिरिक्त, आप किसी मौजूदा समस्या को हल करने का प्रयास कर सकते हैं और एक पीआर सबमिट कर सकते हैं। -हम सक्रिय रूप से योगदानकर्ताओं की तलाश कर रहे हैं, चाहे आपका कौशल स्तर या अनुभव कुछ भी हो। +हम सक्रिय रूप से योगदानकर्ताओं की तलाश कर रहे हैं, चाहे आपका कौशल स्तर या अनुभव कुछ भी हो।योगदान करने के लिए [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) देखें। + +## हमारे सभी योगदानकर्ताओं का धन्यवाद: + + + + ## लाइसेंस (License) -रिफ्लेक्स ओपन-सोर्स है और [अपाचे लाइसेंस 2.0] (लाइसेंस) के तहत लाइसेंस प्राप्त है। +रिफ्लेक्स ओपन-सोर्स है और [अपाचे लाइसेंस 2.0](https://github.com/reflex-dev/reflex/blob/main/LICENSE) के तहत लाइसेंस प्राप्त है। From d99c40a7634e012c41d6e4460f5e54898cbed1e2 Mon Sep 17 00:00:00 2001 From: Kelechi Ebiri <56020538+TG199@users.noreply.github.com> Date: Thu, 30 May 2024 17:14:50 +0100 Subject: [PATCH 043/496] Add domain prop for the PolarRadiusAxis component (#3349) --- reflex/components/recharts/polar.py | 3 +++ reflex/components/recharts/polar.pyi | 2 ++ 2 files changed, 5 insertions(+) diff --git a/reflex/components/recharts/polar.py b/reflex/components/recharts/polar.py index 256695740..ade6f72c8 100644 --- a/reflex/components/recharts/polar.py +++ b/reflex/components/recharts/polar.py @@ -308,6 +308,9 @@ class PolarRadiusAxis(Recharts): # Valid children components _valid_children: List[str] = ["Label"] + # The domain of the polar radius axis, specifying the minimum and maximum values. + domain: List[int] = [0, 250] + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. diff --git a/reflex/components/recharts/polar.pyi b/reflex/components/recharts/polar.pyi index e2186eca0..a5fe60d99 100644 --- a/reflex/components/recharts/polar.pyi +++ b/reflex/components/recharts/polar.pyi @@ -492,6 +492,7 @@ class PolarRadiusAxis(Recharts): ], ] ] = None, + domain: Optional[List[int]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -533,6 +534,7 @@ class PolarRadiusAxis(Recharts): tick: The width or height of tick. tick_count: The count of ticks. scale: If 'auto' set, the scale funtion is linear scale. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold' + domain: The domain of the polar radius axis, specifying the minimum and maximum values. style: The style of the component. key: A unique key for the component. id: The id for the component. From 565231084091633e62059b7edab2f7aef341316d Mon Sep 17 00:00:00 2001 From: Tom Gotsman <64492814+tgberkeley@users.noreply.github.com> Date: Thu, 30 May 2024 12:23:56 -0700 Subject: [PATCH 044/496] update image for readme (#3395) * update image for readme * update based on Nikhil's comment --------- Co-authored-by: Tom Gotsman --- docs/images/dalle_colored_code_example.png | Bin 465957 -> 378844 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/dalle_colored_code_example.png b/docs/images/dalle_colored_code_example.png index c6f7689745195ddbd43679d47b4c8fe43af4a90c..18c8307c545ca31dbbaee3a572754430f596da6b 100644 GIT binary patch literal 378844 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