diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml index 39d648c8054..27182f0f65e 100644 --- a/.github/codeql-config.yml +++ b/.github/codeql-config.yml @@ -1,6 +1,6 @@ paths: - .github - reflex - - reflex/.templates + - packages paths-ignore: - "**/tests/**" diff --git a/.github/workflows/check_node_latest.yml b/.github/workflows/check_node_latest.yml index 3f7afe71980..452c360ed44 100644 --- a/.github/workflows/check_node_latest.yml +++ b/.github/workflows/check_node_latest.yml @@ -24,6 +24,9 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: 3.13 diff --git a/.github/workflows/check_outdated_dependencies.yml b/.github/workflows/check_outdated_dependencies.yml index 0e7c3ba1615..9e96870214a 100644 --- a/.github/workflows/check_outdated_dependencies.yml +++ b/.github/workflows/check_outdated_dependencies.yml @@ -14,8 +14,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 - + uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: 3.13 @@ -43,6 +45,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 + - uses: ./.github/actions/setup_build_env with: python-version: 3.13 diff --git a/.github/workflows/integration_app_harness.yml b/.github/workflows/integration_app_harness.yml index 606503d5ad9..a567141fa7f 100644 --- a/.github/workflows/integration_app_harness.yml +++ b/.github/workflows/integration_app_harness.yml @@ -45,6 +45,9 @@ jobs: - 6379:6379 steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 7c3340d911e..bec8b4ce448 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -43,11 +43,14 @@ jobs: matrix: # Show OS combos first in GUI os: [ubuntu-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.14"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: ${{ matrix.python-version }} @@ -111,6 +114,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: ${{ matrix.python-version }} @@ -147,6 +153,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: 3.14 @@ -179,6 +188,9 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 3f1bd391890..e6cc29dd98a 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -24,6 +24,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v6 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index d94bfdd7181..06ae79a0914 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -21,6 +21,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: 3.14 @@ -28,4 +31,6 @@ jobs: - uses: actions/checkout@v4 with: clean: false + fetch-tags: true + fetch-depth: 0 - run: uv run pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 14908f87bc0..043739b9bd2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,13 @@ name: Publish to PyPI on: + release: + types: [published] workflow_dispatch: + inputs: + tag: + description: "Release tag (e.g. v1.2.3 or reflex-lucide-v0.1.0)" + required: true jobs: publish: @@ -14,14 +20,39 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-tags: true + fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v7 + - name: Parse release tag + id: parse + run: | + TAG="${{ github.event.release.tag_name || inputs.tag }}" + # Tag format: v1.2.3 for reflex, reflex-lucide-v0.1.0 for sub-packages + if [[ "$TAG" =~ ^v([0-9].*)$ ]]; then + echo "package=reflex" >> "$GITHUB_OUTPUT" + echo "build_dir=." >> "$GITHUB_OUTPUT" + elif [[ "$TAG" =~ ^(.+)-v([0-9].*)$ ]]; then + PACKAGE="${BASH_REMATCH[1]}" + if [ ! -d "packages/$PACKAGE" ]; then + echo "Error: packages/$PACKAGE does not exist" + exit 1 + fi + echo "package=$PACKAGE" >> "$GITHUB_OUTPUT" + echo "build_dir=packages/$PACKAGE" >> "$GITHUB_OUTPUT" + else + echo "Error: Tag '$TAG' does not match expected format (v* or -v*)" + exit 1 + fi + - name: Build - run: uv build + run: uv build --directory "${{ steps.parse.outputs.build_dir }}" - name: Verify .pyi files in wheel + if: steps.parse.outputs.package == 'reflex' run: | if unzip -l dist/*.whl | grep '\.pyi$'; then echo "✓ .pyi files found in distribution" @@ -31,4 +62,4 @@ jobs: fi - name: Publish - run: uv publish + run: uv publish --directory "${{ steps.parse.outputs.build_dir }}" diff --git a/.github/workflows/reflex_init_in_docker_test.yml b/.github/workflows/reflex_init_in_docker_test.yml index 1b36786183b..7e33623fde6 100644 --- a/.github/workflows/reflex_init_in_docker_test.yml +++ b/.github/workflows/reflex_init_in_docker_test.yml @@ -24,6 +24,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - shell: bash run: | diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index dc8fdee03ba..5b3e339ea4a 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -47,6 +47,9 @@ jobs: - 6379:6379 steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: ${{ matrix.python-version }} @@ -69,12 +72,6 @@ jobs: export REFLEX_REDIS_URL=redis://localhost:6379 export REFLEX_OPLOCK_ENABLED=true uv run pytest tests/units --cov --no-cov-on-fail --cov-report= - # Change to explicitly install v1 when reflex-hosting-cli is compatible with v2 - - name: Run unit tests w/ pydantic v1 - run: | - export PYTHONUNBUFFERED=1 - uv pip install "pydantic~=1.10" - uv run pytest tests/units --cov --no-cov-on-fail --cov-report= - name: Generate coverage report run: uv run coverage html @@ -88,6 +85,9 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - uses: ./.github/actions/setup_build_env with: python-version: ${{ matrix.python-version }} @@ -97,8 +97,3 @@ jobs: run: | export PYTHONUNBUFFERED=1 uv run pytest tests/units --cov --no-cov-on-fail --cov-report= - - name: Run unit tests w/ pydantic v1 - run: | - export PYTHONUNBUFFERED=1 - uv pip install "pydantic~=1.10" - uv run pytest tests/units --cov --no-cov-on-fail --cov-report= diff --git a/docs/DEBUGGING.md b/DEBUGGING.md similarity index 100% rename from docs/DEBUGGING.md rename to DEBUGGING.md diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 00000000000..10ffd1f365c --- /dev/null +++ b/docs/__init__.py @@ -0,0 +1 @@ +"""Reflex documentation.""" diff --git a/docs/advanced_onboarding/code_structure.md b/docs/advanced_onboarding/code_structure.md new file mode 100644 index 00000000000..0e1ab6c2146 --- /dev/null +++ b/docs/advanced_onboarding/code_structure.md @@ -0,0 +1,362 @@ + +# Project Structure (Advanced) + +## App Module + +Reflex imports the main app module based on the `app_name` from the config, which **must define a module-level global named `app` as an instance of `rx.App`**. + +The main app module is responsible for importing all other modules that make up the app and defining `app = rx.App()`. + +**All other modules containing pages, state, and models MUST be imported by the main app module or package** for Reflex to include them in the compiled output. + +# Breaking the App into Smaller Pieces + +As applications scale, effective organization is crucial. This is achieved by breaking the application down into smaller, manageable modules and organizing them into logical packages that avoid circular dependencies. + +In the following documentation there will be an app with an `app_name` of `example_big_app`. The main module would be `example_big_app/example_big_app.py`. + +In the [Putting it all together](#putting-it-all-together) section there is a visual of the project folder structure to help follow along with the examples below. + +### Pages Package: `example_big_app/pages` + +All complex apps will have multiple pages, so it is recommended to create `example_big_app/pages` as a package. + +1. This package should contain one module per page in the app. +2. If a particular page depends on the state, the substate should be defined in the same module as the page. +3. The page-returning function should be decorated with `rx.page()` to have it added as a route in the app. + +```python +import reflex as rx + +from ..state import AuthState + + +class LoginState(AuthState): + @rx.event + def handle_submit(self, form_data): + self.logged_in = authenticate(form_data["username"], form_data["password"]) + + +def login_field(name: str, **input_props): + return rx.hstack( + rx.text(name.capitalize()), + rx.input(name=name, **input_props), + width="100%", + justify="between", + ) + + +@rx.page(route="/login") +def login(): + return rx.card( + rx.form( + rx.vstack( + login_field("username"), + login_field("password", type="password"), + rx.button("Login"), + width="100%", + justify="center", + ), + on_submit=LoginState.handle_submit, + ), + ) +``` + +### Templating: `example_big_app/template.py` + +Most applications maintain a consistent layout and structure across pages. Defining this common structure in a separate module facilitates easy sharing and reuse when constructing individual pages. + +**Best Practices** + +1. Factor out common frontend UI elements into a function that returns a component. +2. If a function accepts a function that returns a component, it can be used as a decorator as seen below. + +```python +from typing import Callable + +import reflex as rx + +from .components.menu import menu +from .components.navbar import navbar + + +def template(page: Callable[[], rx.Component]) -> rx.Component: + return rx.vstack( + navbar(), + rx.hstack( + menu(), + rx.container(page()), + ), + width="100%", + ) +``` + +The `@template` decorator should appear below the `@rx.page` decorator and above the page-returning function. See the [Posts Page](#a-post-page-example_big_apppagespostspy) code for an example. + +## State Management + +Most pages will use State in some capacity. You should avoid adding vars to a +shared state that will only be used in a single page. Instead, define a new +subclass of `rx.State` and keep it in the same module as the page. + +### Accessing other States + +As of Reflex 0.4.3, any event handler can get access to an instance of any other +substate via the `get_state` API. From a practical perspective, this means that +state can be split up into smaller pieces without requiring a complex +inheritance hierarchy to share access to other states. + +In previous releases, if an app wanted to store settings in `SettingsState` with +a page or component for modifying them, any other state with an event handler +that needed to access those settings would have to inherit from `SettingsState`, +even if the other state was mostly orthogonal. The other state would also now +always have to load the settings, even for event handlers that didn't need to +access them. + +A better strategy is to load the desired state on demand from only the event +handler which needs access to the substate. + +### A Settings Component: `example_big_app/components/settings.py` + +```python +import reflex as rx + + +class SettingsState(rx.State): + refresh_interval: int = 15 + auto_update: bool = True + prefer_plain_text: bool = True + posts_per_page: int = 20 + + +def settings_dialog(): + return rx.dialog(...) +``` + +### A Post Page: `example_big_app/pages/posts.py` + +This page loads the `SettingsState` to determine how many posts to display per page +and how often to refresh. + +```python +import reflex as rx + +from ..models import Post +from ..template import template +from ..components.settings import SettingsState + + +class PostsState(rx.State): + refresh_tick: int + page: int + posts: list[Post] + + @rx.event + async def on_load(self): + settings = await self.get_state(SettingsState) + if settings.auto_update: + self.refresh_tick = settings.refresh_interval * 1000 + else: + self.refresh_tick = 0 + + @rx.event + async def tick(self, _): + settings = await self.get_state(SettingsState) + with rx.session() as session: + q = Post.select().offset(self.page * settings.posts_per_page).limit(settings.posts_per_page) + self.posts = q.all() + + @rx.event + def go_to_previous(self): + if self.page > 0: + self.page = self.page - 1 + + @rx.event + def go_to_next(self): + if self.posts: + self.page = self.page + 1 + + +@rx.page(route="/posts", on_load=PostsState.on_load) +@template +def posts(): + return rx.vstack( + rx.foreach(PostsState.posts, post_view), + rx.hstack( + rx.button("< Prev", on_click=PostsState.go_to_previous), + rx.button("Next >", on_click=PostsState.go_to_next), + justify="between", + ), + rx.moment(interval=PostsState.refresh_tick, on_change=PostsState.tick, display="none"), + width="100%", + ) +``` + +### Common State: `example_big_app/state.py` + +_Common_ states and substates that are shared by multiple pages or components +should be implemented in a separate module to avoid circular imports. This +module should not import other modules in the app. + +## Component Reusability + +The primary mechanism for reusing components in Reflex is to define a function that returns +the component, then simply call it where that functionality is needed. + +Component functions typically should not take any State classes as arguments, but prefer +to import the needed state and access the vars on the class directly. + +### Memoize Functions for Improved Performance + +In a large app, if a component has many subcomponents or is used in a large number of places, it can improve compile and runtime performance to memoize the function with the `@lru_cache` decorator. + +To memoize the `foo` component to avoid re-creating it many times simply add `@lru_cache` to the function definition, and the component will only be created once per unique set of arguments. + +```python +from functools import lru_cache + +import reflex as rx + +class State(rx.State): + v: str = "foo" + + +@lru_cache +def foo(): + return rx.text(State.v) + + +def index(): + return rx.flex( + rx.button("Change", on_click=State.set_v(rx.cond(State.v != "bar", "bar", "foo"))), + *[ + foo() + for _ in range(100) + ], + direction="row", + wrap="wrap", + ) +``` + +### example_big_app/components + +This package contains reusable parts of the app, for example headers, footers, +and menus. If a particular component requires state, the substate may be defined +in the same module for locality. Any substate defined in a component module +should only contain fields and event handlers pertaining to that individual +component. + +### External Components + +Reflex 0.4.3 introduced support for the [`reflex component` CLI commands](/docs/custom-components/overview), which makes it easy +to bundle up common functionality to publish on PyPI as a standalone Python package +that can be installed and used in any Reflex app. + +When wrapping npm components or other self-contained bits of functionality, it can be helpful +to move this complexity outside the app itself for easier maintenance and reuse in other apps. + +## Database Models: `example_big_app/models.py` + +It is recommended to implement all database models in a single file to make it easier to define relationships and understand the entire schema. + +However, if the schema is very large, it might make sense to have a `models` package with individual models defined in their own modules. + +At any rate, defining the models separately allows any page or component to import and use them without circular imports. + +## Top-level Package: `example_big_app/__init__.py` + +This is a great place to import all state, models, and pages that should be part of the app. +Typically, components and helpers do not need to imported, because they will be imported by +pages that use them (or they would be unused). + +```python +from . import state, models +from .pages import index, login, post, product, profile, schedule + +__all__ = [ + "state", + "models", + "index", + "login", + "post", + "product", + "profile", + "schedule", +] +``` + +If any pages are not imported here, they will not be compiled as part of the app. + +## example_big_app/example_big_app.py + +This is the main app module. Since everything else is defined in other modules, this file becomes very simple. + +```python +import reflex as rx + +app = rx.App() +``` + +## File Management + +There are two categories of non-code assets (media, fonts, stylesheets, +documents) typically used in a Reflex app. + +### assets + +The `assets` directory is used for **static** files that should be accessible +relative to the root of the frontend (default port 3000). When an app is deployed in +production mode, changes to the assets directory will NOT be available at runtime! + +When referencing an asset, always use a leading forward slash, so the +asset can be resolved regardless of the page route where it may appear. + +### uploaded_files + +If an app needs to make files available dynamically at runtime, it is +recommended to set the target directory via `REFLEX_UPLOADED_FILES_DIR` +environment variable (default `./uploaded_files`), write files relative to the +path returned by `rx.get_upload_dir()`, and create working links via +`rx.get_upload_url(relative_path)`. + +Uploaded files are served from the backend (default port 8000) via +`/_upload/` + +## Putting it all together + +Based on the previous discussion, the recommended project layout look like this. + +```text +example-big-app/ +├─ assets/ +├─ example_big_app/ +│ ├─ components/ +│ │ ├─ __init__.py +│ │ ├─ auth.py +│ │ ├─ footer.py +│ │ ├─ menu.py +│ │ ├─ navbar.py +│ ├─ pages/ +│ │ ├─ __init__.py +│ │ ├─ index.py +│ │ ├─ login.py +│ │ ├─ posts.py +│ │ ├─ product.py +│ │ ├─ profile.py +│ │ ├─ schedule.py +│ ├─ __init__.py +│ ├─ example_big_app.py +│ ├─ models.py +│ ├─ state.py +│ ├─ template.py +├─ uploaded_files/ +├─ requirements.txt +├─ rxconfig.py +``` + +## Key Takeaways + +- Like any other Python project, **split up the app into modules and packages** to keep the codebase organized and manageable. +- Using smaller modules and packages makes it easier to **reuse components and state** across the app + without introducing circular dependencies. +- Create **individual functions** to encapsulate units of functionality and **reuse them** where needed. diff --git a/docs/advanced_onboarding/configuration.md b/docs/advanced_onboarding/configuration.md new file mode 100644 index 00000000000..09e54e94169 --- /dev/null +++ b/docs/advanced_onboarding/configuration.md @@ -0,0 +1,57 @@ + +# Configuration + +Reflex apps can be configured using a configuration file, environment variables, and command line arguments. + +## Configuration File + +Running `reflex init` will create an `rxconfig.py` file in your root directory. +You can pass keyword arguments to the `Config` class to configure your app. + +For example: + +```python +# rxconfig.py +import reflex as rx + +config = rx.Config( + app_name="my_app_name", + # Connect to your own database. + db_url="postgresql://user:password@localhost:5432/my_db", + # Change the frontend port. + frontend_port=3001, +) +``` + +See the [config reference](https://reflex.dev/docs/api-reference/config/) for all the parameters available. + +## Environment Variables + +You can override the configuration file by setting environment variables. +For example, to override the `frontend_port` setting, you can set the `FRONTEND_PORT` environment variable. + +```bash +FRONTEND_PORT=3001 reflex run +``` + +## Command Line Arguments + +Finally, you can override the configuration file and environment variables by passing command line arguments to `reflex run`. + +```bash +reflex run --frontend-port 3001 +``` + +See the [CLI reference](/docs/api-reference/cli) for all the arguments available. + +## Customizable App Data Directory + +The `REFLEX_DIR` environment variable can be set, which allows users to set the location where Reflex writes helper tools like Bun and NodeJS. + +By default we use Platform specific directories: + +On windows, `C:/Users//AppData/Local/reflex` is used. + +On macOS, `~/Library/Application Support/reflex` is used. + +On linux, `~/.local/share/reflex` is used. diff --git a/docs/advanced_onboarding/how-reflex-works.md b/docs/advanced_onboarding/how-reflex-works.md new file mode 100644 index 00000000000..f401f9d896f --- /dev/null +++ b/docs/advanced_onboarding/how-reflex-works.md @@ -0,0 +1,236 @@ +# How Reflex Works + +We'll use the following basic app that displays Github profile images as an example to explain the different parts of the architecture. + +```python demo exec +import requests +import reflex as rx + +class GithubState(rx.State): + url: str = "https://github.com/reflex-dev" + profile_image: str = "https://avatars.githubusercontent.com/u/104714959" + + @rx.event + def set_profile(self, username: str): + if username == "": + return + try: + github_data = requests.get(f"https://api.github.com/users/{username}").json() + except: + return + self.url = github_data["url"] + self.profile_image = github_data["avatar_url"] + +def index(): + return rx.hstack( + rx.link( + rx.avatar(src=GithubState.profile_image), + href=GithubState.url, + ), + rx.input( + placeholder="Your Github username", + on_blur=GithubState.set_profile, + ), + ) +``` + +## The Reflex Architecture + +Full-stack web apps are made up of a frontend and a backend. The frontend is the user interface, and is served as a web page that runs on the user's browser. The backend handles the logic and state management (such as databases and APIs), and is run on a server. + +In traditional web development, these are usually two separate apps, and are often written in different frameworks or languages. For example, you may combine a Flask backend with a React frontend. With this approach, you have to maintain two separate apps and end up writing a lot of boilerplate code to connect the frontend and backend. + +We wanted to simplify this process in Reflex by defining both the frontend and backend in a single codebase, while using Python for everything. Developers should only worry about their app's logic and not about the low-level implementation details. + +### TLDR + +Under the hood, Reflex apps compile down to a [React](https://react.dev) frontend app and a [FastAPI](https://github.com/tiangolo/fastapi) backend app. Only the UI is compiled to Javascript; all the app logic and state management stays in Python and is run on the server. Reflex uses [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) to send events from the frontend to the backend, and to send state updates from the backend to the frontend. + +The diagram below provides a detailed overview of how a Reflex app works. We'll go through each part in more detail in the following sections. + +```python exec +from reflex_image_zoom import image_zoom +``` + +```python eval +image_zoom(rx.image(src="https://web.reflex-assets.dev/other/architecture.webp")) +``` + +```python eval +rx.box(height="1em") +``` + +## Frontend + +We wanted Reflex apps to look and feel like a traditional web app to the end user, while still being easy to build and maintain for the developer. To do this, we built on top of mature and popular web technologies. + +When you `reflex run` your app, Reflex compiles the frontend down to a single-page [Next.js](https://nextjs.org) app and serves it on a port (by default `3000`) that you can access in your browser. + +The frontend's job is to reflect the app's state, and send events to the backend when the user interacts with the UI. No actual logic is run on the frontend. + +### Components + +Reflex frontends are built using components that can be composed together to create complex UIs. Instead of using a templating language that mixes HTML and Python, we just use Python functions to define the UI. + +```python +def index(): + return rx.hstack( + rx.link( + rx.avatar(src=GithubState.profile_image), + href=GithubState.url, + ), + rx.input( + placeholder="Your Github username", + on_blur=GithubState.set_profile, + ), + ) +``` + +In our example app, we have components such as `rx.hstack`, `rx.avatar`, and `rx.input`. These components can have different **props** that affect their appearance and functionality - for example the `rx.input` component has a `placeholder` prop to display the default text. + +We can make our components respond to user interactions with events such as `on_blur`, which we will discuss more below. + +Under the hood, these components compile down to React components. For example, the above code compiles down to the following React code: + +```jsx + + + + + + +``` + +Many of our core components are based on [Radix](https://radix-ui.com/), a popular React component library. We also have many other components for graphing, datatables, and more. + +We chose React because it is a popular library with a huge ecosystem. Our goal isn't to recreate the web ecosystem, but to make it accessible to Python developers. + +This also lets our users bring their own components if we don't have a component they need. Users can [wrap their own React components](/docs/wrapping-react/overview) and then [publish them](/docs/custom-components/overview) for others to use. Over time we will build out our [third party component ecosystem](/docs/custom-components/overview) so that users can easily find and use components that others have built. + +### Styling + +We wanted to make sure Reflex apps look good out of the box, while still giving developers full control over the appearance of their app. + +We have a core [theming system](/docs/styling/theming) that lets you set high level styling options such as dark mode and accent color throughout your app to give it a unified look and feel. + +Beyond this, Reflex components can be styled using the full power of CSS. We leverage the [Emotion](https://emotion.sh/docs/introduction) library to allow "CSS-in-Python" styling, so you can pass any CSS prop as a keyword argument to a component. This includes [responsive props](/docs/styling/responsive) by passing a list of values. + +## Backend + +Now let's look at how we added interactivity to our apps. + +In Reflex only the frontend compiles to Javascript and runs on the user's browser, while all the state and logic stays in Python and is run on the server. When you `reflex run`, we start a FastAPI server (by default on port `8000`) that the frontend connects to through a websocket. + +All the state and logic are defined within a `State` class. + +```python +class GithubState(rx.State): + url: str = "https://github.com/reflex-dev" + profile_image: str = "https://avatars.githubusercontent.com/u/104714959" + + def set_profile(self, username: str): + if username == "": + return + github_data = requests.get(f"https://api.github.com/users/\{username}").json() + self.url = github_data["url"] + self.profile_image = github_data["avatar_url"] +``` + +The state is made up of **vars** and **event handlers**. + +Vars are any values in your app that can change over time. They are defined as class attributes on your `State` class, and may be any Python type that can be serialized to JSON. In our example, `url` and `profile_image` are vars. + +Event handlers are methods in your `State` class that are called when the user interacts with the UI. They are the only way that we can modify the vars in Reflex, and can be called in response to user actions, such as clicking a button or typing in a text box. In our example, `set_profile` is an event handler that updates the `url` and `profile_image` vars. + +Since event handlers are run on the backend, you can use any Python library within them. In our example, we use the `requests` library to make an API call to Github to get the user's profile image. + +## Event Processing + +Now we get into the interesting part - how we handle events and state updates. + +Normally when writing web apps, you have to write a lot of boilerplate code to connect the frontend and backend. With Reflex, you don't have to worry about that - we handle the communication between the frontend and backend for you. Developers just have to write their event handler logic, and when the vars are updated the UI is automatically updated. + +You can refer to the diagram above for a visual representation of the process. Let's walk through it with our Github profile image example. + +### Event Triggers + +The user can interact with the UI in many ways, such as clicking a button, typing in a text box, or hovering over an element. In Reflex, we call these **event triggers**. + +```python +rx.input( + placeholder="Your Github username", + on_blur=GithubState.set_profile, +) +``` + +In our example we bind the `on_blur` event trigger to the `set_profile` event handler. This means that when the user types in the input field and then clicks away, the `set_profile` event handler is called. + +### Event Queue + +On the frontend, we maintain an event queue of all pending events. An event consists of three major pieces of data: + +- **client token**: Each client (browser tab) has a unique token to identify it. This let's the backend know which state to update. +- **event handler**: The event handler to run on the state. +- **arguments**: The arguments to pass to the event handler. + +Let's assume I type my username "picklelo" into the input. In this example, our event would look something like this: + +```json +{ + "client_token": "abc123", + "event_handler": "GithubState.set_profile", + "arguments": ["picklelo"] +} +``` + +On the frontend, we maintain an event queue of all pending events. + +When an event is triggered, it is added to the queue. We have a `processing` flag to make sure only one event is processed at a time. This ensures that the state is always consistent and there aren't any race conditions with two event handlers modifying the state at the same time. + +```md alert info +# There are exceptions to this, such as [background events](/docs/events/background_events) which allow you to run events in the background without blocking the UI. +``` + +Once the event is ready to be processed, it is sent to the backend through a WebSocket connection. + +### State Manager + +Once the event is received, it is processed on the backend. + +Reflex uses a **state manager** which maintains a mapping between client tokens and their state. By default, the state manager is just an in-memory dictionary, but it can be extended to use a database or cache. In production we use Redis as our state manager. + +### Event Handling + +Once we have the user's state, the next step is to run the event handler with the arguments. + +```python + def set_profile(self, username: str): + if username == "": + return + github_data = requests.get(f"https://api.github.com/users/\{username}").json() + self.url = github_data["url"] + self.profile_image = github_data["avatar_url"] +``` + +In our example, the `set_profile` event handler is run on the user's state. This makes an API call to Github to get the user's profile image, and then updates the state's `url` and `profile_image` vars. + +### State Updates + +Every time an event handler returns (or [yields](/docs/events/yield_events)), we save the state in the state manager and send the **state updates** to the frontend to update the UI. + +To maintain performance as your state grows, internally Reflex keeps track of vars that were updated during the event handler (**dirty vars**). When the event handler is done processing, we find all the dirty vars and create a state update to send to the frontend. + +In our case, the state update may look something like this: + +```json +{ + "url": "https://github.com/picklelo", + "profile_image": "https://avatars.githubusercontent.com/u/104714959" +} +``` + +We store the new state in our state manager, and then send the state update to the frontend. The frontend then updates the UI to reflect the new state. In our example, the new Github profile image is displayed. diff --git a/docs/api-reference/browser_javascript.md b/docs/api-reference/browser_javascript.md new file mode 100644 index 00000000000..892ffd2c064 --- /dev/null +++ b/docs/api-reference/browser_javascript.md @@ -0,0 +1,222 @@ +```python exec +import asyncio +from typing import Any +import reflex as rx +``` + +# Browser Javascript + +Reflex compiles your frontend code, defined as python functions, into a Javascript web application +that runs in the user's browser. There are instances where you may need to supply custom javascript +code to interop with Web APIs, use certain third-party libraries, or wrap low-level functionality +that is not exposed via Reflex's Python API. + +```md alert +# Avoid Custom Javascript + +Custom Javascript code in your Reflex app presents a maintenance challenge, as it will be harder to debug and may be unstable across Reflex versions. + +Prefer to use the Python API whenever possible and file an issue if you need additional functionality that is not currently provided. +``` + +## Executing Script + +There are four ways to execute custom Javascript code into your Reflex app: + +- `rx.script` - Injects the script via `next/script` for efficient loading of inline and external Javascript code. Described further in the [component library](/docs/library/other/script). + - These components can be directly included in the body of a page, or they may + be passed to `rx.App(head_components=[rx.script(...)])` to be included in + the `` tag of all pages. +- `rx.call_script` - An event handler that evaluates arbitrary Javascript code, + and optionally returns the result to another event handler. + +These previous two methods can work in tandem to load external scripts and then +call functions defined within them in response to user events. + +The following two methods are geared towards wrapping components and are +described with examples in the [Wrapping React](/docs/wrapping-react/overview) +section. + +- `_get_hooks` and `_get_custom_code` in an `rx.Component` subclass +- `Var.create` with `_var_is_local=False` + +## Inline Scripts + +The `rx.script` component is the recommended way to load inline Javascript for greater control over +frontend behavior. + +The functions and variables in the script can be accessed from backend event +handlers or frontend event triggers via the `rx.call_script` interface. + +```python demo exec +class SoundEffectState(rx.State): + @rx.event(background=True) + async def delayed_play(self): + await asyncio.sleep(1) + return rx.call_script("playFromStart(button_sfx)") + + +def sound_effect_demo(): + return rx.hstack( + rx.script(""" + var button_sfx = new Audio("/vintage-button-sound-effect.mp3") + function playFromStart (sfx) {sfx.load(); sfx.play()}"""), + rx.button("Play Immediately", on_click=rx.call_script("playFromStart(button_sfx)")), + rx.button("Play Later", on_click=SoundEffectState.delayed_play), + ) +``` + +## External Scripts + +External scripts can be loaded either from the `assets` directory, or from CDN URL, and then controlled +via `rx.call_script`. + +```python demo +rx.vstack( + rx.script( + src="https://cdn.jsdelivr.net/gh/scottschiller/snowstorm@snowstorm_20131208/snowstorm-min.js", + ), + rx.script(""" + window.addEventListener('load', function() { + if (typeof snowStorm !== 'undefined') { + snowStorm.autoStart = false; + snowStorm.snowColor = '#111'; + } + }); + """), + rx.button("Start Duststorm", on_click=rx.call_script("snowStorm.start()")), + rx.button("Toggle Duststorm", on_click=rx.call_script("snowStorm.toggleSnow()")), +) +``` + +## Accessing Client Side Values + +The `rx.call_script` function accepts a `callback` parameter that expects an +Event Handler with one argument which will receive the result of evaluating the +Javascript code. This can be used to access client-side values such as the +`window.location` or current scroll location, or any previously defined value. + +```python demo exec +class WindowState(rx.State): + location: dict[str, str] = {} + scroll_position: dict[str, int] = {} + + def update_location(self, location): + self.location = location + + def update_scroll_position(self, scroll_position): + self.scroll_position = { + "x": scroll_position[0], + "y": scroll_position[1], + } + + @rx.event + def get_client_values(self): + return [ + rx.call_script( + "window.location", + callback=WindowState.update_location + ), + rx.call_script( + "[window.scrollX, window.scrollY]", + callback=WindowState.update_scroll_position, + ), + ] + + +def window_state_demo(): + return rx.vstack( + rx.button("Update Values", on_click=WindowState.get_client_values), + rx.text(f"Scroll Position: {WindowState.scroll_position.to_string()}"), + rx.text("window.location:"), + rx.text_area(value=WindowState.location.to_string(), is_read_only=True), + on_mount=WindowState.get_client_values, + ) +``` + +```md alert +# Allowed Callback Values + +The `callback` parameter may be an `EventHandler` with one argument, or a lambda with one argument that returns an `EventHandler`. +If the callback is None, then no event is triggered. +``` + +## Using React Hooks + +To use React Hooks directly in a Reflex app, you must subclass `rx.Component`, +typically `rx.Fragment` is used when the hook functionality has no visual +element. The hook code is returned by the `add_hooks` method, which is expected +to return a `list[str]` containing Javascript code which will be inserted into the +page component (i.e the render function itself). + +For supporting code that must be defined outside of the component render +function, use `_get_custom_code`. + +The following example uses `useEffect` to register global hotkeys on the +`document` object, and then triggers an event when a specific key is pressed. + +```python demo exec +import dataclasses + +from reflex.utils import imports + +@dataclasses.dataclass +class KeyEvent: + """Interface of Javascript KeyboardEvent""" + key: str = "" + +def key_event_spec(ev: rx.Var[KeyEvent]) -> tuple[rx.Var[str]]: + # Takes the event object and returns the key pressed to send to the state + return (ev.key,) + +class GlobalHotkeyState(rx.State): + key: str = "" + + @rx.event + def update_key(self, key): + self.key = key + + +class GlobalHotkeyWatcher(rx.Fragment): + """A component that listens for key events globally.""" + + # The event handler that will be called + on_key_down: rx.EventHandler[key_event_spec] + + def add_imports(self) -> imports.ImportDict: + """Add the imports for the component.""" + return { + "react": [imports.ImportVar(tag="useEffect")], + } + + def add_hooks(self) -> list[str | rx.Var]: + """Add the hooks for the component.""" + return [ + """ + useEffect(() => { + const handle_key = %s; + document.addEventListener("keydown", handle_key, false); + return () => { + document.removeEventListener("keydown", handle_key, false); + } + }) + """ + % str(rx.Var.create(self.event_triggers["on_key_down"])) + ] + +def global_key_demo(): + return rx.vstack( + GlobalHotkeyWatcher.create( + keys=["a", "s", "d", "w"], + on_key_down=lambda key: rx.cond( + rx.Var.create(["a", "s", "d", "w"]).contains(key), + GlobalHotkeyState.update_key(key), + rx.console_log(key) + ) + ), + rx.text("Press a, s, d or w to trigger an event"), + rx.heading(f"Last watched key pressed: {GlobalHotkeyState.key}"), + ) +``` + +This snippet can also be imported through pip: [reflex-global-hotkey](https://pypi.org/project/reflex-global-hotkey/). diff --git a/docs/api-reference/browser_storage.md b/docs/api-reference/browser_storage.md new file mode 100644 index 00000000000..04374f9a4aa --- /dev/null +++ b/docs/api-reference/browser_storage.md @@ -0,0 +1,336 @@ +# Browser Storage + +## rx.Cookie + +Represents a state Var that is stored as a cookie in the browser. Currently only supports string values. + +Parameters + +- `name` : The name of the cookie on the client side. +- `path`: The cookie path. Use `/` to make the cookie accessible on all pages. +- `max_age` : Relative max age of the cookie in seconds from when the client receives it. +- `domain`: Domain for the cookie (e.g., `sub.domain.com` or `.allsubdomains.com`). +- `secure`: If the cookie is only accessible through HTTPS. +- `same_site`: Whether the cookie is sent with third-party requests. Can be one of (`True`, `False`, `None`, `lax`, `strict`). + +```python +class CookieState(rx.State): + c1: str = rx.Cookie() + c2: str = rx.Cookie('c2 default') + + # cookies with custom settings + c3: str = rx.Cookie(max_age=2) # expires after 2 second + c4: str = rx.Cookie(same_site='strict') + c5: str = rx.Cookie(path='/foo/') # only accessible on `/foo/` + c6: str = rx.Cookie(name='c6-custom-name') +``` + +```md alert warning +# **The default value of a Cookie is never set in the browser!** + +The Cookie value is only set when the Var is assigned. If you need to set a +default value, you can assign a value to the cookie in an `on_load` event +handler. +``` + +## Accessing Cookies + +Cookies are accessed like any other Var in the state. If another state needs access +to the value of a cookie, the state should be a substate of the state that defines +the cookie. Alternatively the `get_state` API can be used to access the other state. + +For rendering cookies in the frontend, import the state that defines the cookie and +reference it directly. + +```md alert warning +# **Two separate states should _avoid_ defining `rx.Cookie` with the same name.** + +Although it is technically possible, the cookie options may differ, leading to +unexpected results. + +Additionally, updating the cookie value in one state will not automatically +update the value in the other state without a page refresh or navigation event. +``` + +## rx.remove_cookies + +Remove a cookie from the client's browser. + +Parameters: + +- `key`: The name of cookie to remove. + +```python +rx.button( + 'Remove cookie', on_click=rx.remove_cookie('key') +) +``` + +This event can also be returned from an event handler: + +```python +class CookieState(rx.State): + ... + def logout(self): + return rx.remove_cookie('auth_token') +``` + +## rx.LocalStorage + +Represents a state Var that is stored in localStorage in the browser. Currently only supports string values. + +Parameters + +- `name`: The name of the storage key on the client side. +- `sync`: Boolean indicates if the state should be kept in sync across tabs of the same browser. + +```python +class LocalStorageState(rx.State): + # local storage with default settings + l1: str = rx.LocalStorage() + + # local storage with custom settings + l2: str = rx.LocalStorage("l2 default") + l3: str = rx.LocalStorage(name="l3") + + # local storage that automatically updates in other states across tabs + l4: str = rx.LocalStorage(sync=True) +``` + +### Syncing Vars + +Because LocalStorage applies to the entire browser, all LocalStorage Vars are +automatically shared across tabs. + +The `sync` parameter controls whether an update in one tab should be actively +propagated to other tabs without requiring a navigation or page refresh event. + +## rx.remove_local_storage + +Remove a local storage item from the client's browser. + +Parameters + +- `key`: The key to remove from local storage. + +```python +rx.button( + 'Remove Local Storage', + on_click=rx.remove_local_storage('key'), +) +``` + +This event can also be returned from an event handler: + +```python +class LocalStorageState(rx.State): + ... + def logout(self): + return rx.remove_local_storage('local_storage_state.l1') +``` + +## rx.clear_local_storage() + +Clear all local storage items from the client's browser. This may affect other +apps running in the same domain or libraries within your app that use local +storage. + +```python +rx.button( + 'Clear all Local Storage', + on_click=rx.clear_local_storage(), +) +``` + +## rx.SessionStorage + +Represents a state Var that is stored in sessionStorage in the browser. Similar to localStorage, but the data is cleared when the page session ends (when the browser/tab is closed). Currently only supports string values. + +Parameters + +- `name`: The name of the storage key on the client side. + +```python +class SessionStorageState(rx.State): + # session storage with default settings + s1: str = rx.SessionStorage() + + # session storage with custom settings + s2: str = rx.SessionStorage("s2 default") + s3: str = rx.SessionStorage(name="s3") +``` + +### Session Persistence + +SessionStorage data is cleared when the page session ends. A page session lasts as long as the browser is open and survives page refreshes and restores, but is cleared when the tab or browser is closed. + +Unlike LocalStorage, SessionStorage is isolated to the tab/window in which it was created, so it's not shared with other tabs/windows of the same origin. + +## rx.remove_session_storage + +Remove a session storage item from the client's browser. + +Parameters + +- `key`: The key to remove from session storage. + +```python +rx.button( + 'Remove Session Storage', + on_click=rx.remove_session_storage('key'), +) +``` + +This event can also be returned from an event handler: + +```python +class SessionStorageState(rx.State): + ... + def logout(self): + return rx.remove_session_storage('session_storage_state.s1') +``` + +## rx.clear_session_storage() + +Clear all session storage items from the client's browser. This may affect other +apps running in the same domain or libraries within your app that use session +storage. + +```python +rx.button( + 'Clear all Session Storage', + on_click=rx.clear_session_storage(), +) +``` + +# Serialization Strategies + +If a non-trivial data structure should be stored in a `Cookie`, `LocalStorage`, or `SessionStorage` var it needs to be serialized before and after storing it. It is recommended to use a pydantic class for the data which provides simple serialization helpers and works recursively in complex object structures. + +```python demo exec +import reflex as rx +import pydantic + + +class AppSettings(pydantic.BaseModel): + theme: str = 'light' + sidebar_visible: bool = True + update_frequency: int = 60 + error_messages: list[str] = pydantic.Field(default_factory=list) + + +class ComplexLocalStorageState(rx.State): + data_raw: str = rx.LocalStorage("{}") + data: AppSettings = AppSettings() + settings_open: bool = False + + @rx.event + def save_settings(self): + self.data_raw = self.data.model_dump_json() + self.settings_open = False + + @rx.event + def open_settings(self): + self.data = AppSettings.model_validate_json(self.data_raw) + self.settings_open = True + + @rx.event + def set_field(self, field, value): + setattr(self.data, field, value) + + +def app_settings(): + return rx.form.root( + rx.foreach( + ComplexLocalStorageState.data.error_messages, + rx.text, + ), + rx.form.field( + rx.flex( + rx.form.label( + "Theme", + rx.input( + value=ComplexLocalStorageState.data.theme, + on_change=lambda v: ComplexLocalStorageState.set_field( + "theme", v + ), + ), + ), + rx.form.label( + "Sidebar Visible", + rx.switch( + checked=ComplexLocalStorageState.data.sidebar_visible, + on_change=lambda v: ComplexLocalStorageState.set_field( + "sidebar_visible", + v, + ), + ), + ), + rx.form.label( + "Update Frequency (seconds)", + rx.input( + value=ComplexLocalStorageState.data.update_frequency, + on_change=lambda v: ComplexLocalStorageState.set_field( + "update_frequency", + v, + ), + ), + ), + rx.dialog.close(rx.button("Save", type="submit")), + gap=2, + direction="column", + ) + ), + on_submit=lambda _: ComplexLocalStorageState.save_settings(), + ) + +def app_settings_example(): + return rx.dialog.root( + rx.dialog.trigger( + rx.button("App Settings", on_click=ComplexLocalStorageState.open_settings), + ), + rx.dialog.content( + rx.dialog.title("App Settings"), + app_settings(), + ), + ) +``` + +# Comparison of Storage Types + +Here's a comparison of the different client-side storage options in Reflex: + +| Feature | rx.Cookie | rx.LocalStorage | rx.SessionStorage | +| ------------------- | --------------------------------- | --------------------------- | --------------------------- | +| Persistence | Until cookie expires | Until explicitly deleted | Until browser/tab is closed | +| Storage Limit | ~4KB | ~5MB | ~5MB | +| Sent with Requests | Yes | No | No | +| Accessibility | Server & Client | Client Only | Client Only | +| Expiration | Configurable | Never | End of session | +| Scope | Configurable (domain, path) | Origin (domain) | Tab/Window | +| Syncing Across Tabs | No | Yes (with sync=True) | No | +| Use Case | Authentication, Server-side state | User preferences, App state | Temporary session data | + +# When to Use Each Storage Type + +## Use rx.Cookie When: + +- You need the data to be accessible on the server side (cookies are sent with HTTP requests) +- You're handling user authentication +- You need fine-grained control over expiration and scope +- You need to limit the data to specific paths in your app + +## Use rx.LocalStorage When: + +- You need to store larger amounts of data (up to ~5MB) +- You want the data to persist indefinitely (until explicitly deleted) +- You need to share data between different tabs/windows of your app +- You want to store user preferences that should be remembered across browser sessions + +## Use rx.SessionStorage When: + +- You need temporary data that should be cleared when the browser/tab is closed +- You want to isolate data to a specific tab/window +- You're storing sensitive information that shouldn't persist after the session ends +- You're implementing per-session features like form data, shopping carts, or multi-step processes +- You want to persist data for a state after Redis expiration (for server-side state that needs to survive longer than Redis TTL) diff --git a/docs/api-reference/cli.md b/docs/api-reference/cli.md new file mode 100644 index 00000000000..76da441c036 --- /dev/null +++ b/docs/api-reference/cli.md @@ -0,0 +1,128 @@ +# CLI + +The `reflex` command line interface (CLI) is a tool for creating and managing Reflex apps. + +To see a list of all available commands, run `reflex --help`. + +```bash +$ reflex --help + +Usage: reflex [OPTIONS] COMMAND [ARGS]... + + Reflex CLI to create, run, and deploy apps. + +Options: + --version Show the version and exit. + --help Show this message and exit. + +Commands: + cloud The Hosting CLI. + component CLI for creating custom components. + db Subcommands for managing the database schema. + deploy Deploy the app to the Reflex hosting service. + export Export the app to a zip file. + init Initialize a new Reflex app in the current directory. + login Authenticate with experimental Reflex hosting service. + logout Log out of access to Reflex hosting service. + rename Rename the app in the current directory. + run Run the app in the current directory. + script Subcommands for running helper scripts. +``` + +## Init + +The `reflex init` command creates a new Reflex app in the current directory. +If an `rxconfig.py` file already exists already, it will re-initialize the app with the latest template. + +```bash +$ reflex init --help +Usage: reflex init [OPTIONS] + + Initialize a new Reflex app in the current directory. + +Options: + --name APP_NAME The name of the app to initialize. + --template [demo|sidebar|blank] + The template to initialize the app with. + --loglevel [debug|info|warning|error|critical] + The log level to use. [default: + LogLevel.INFO] + --help Show this message and exit. +``` + +## Run + +The `reflex run` command runs the app in the current directory. + +By default it runs your app in development mode. +This means that the app will automatically reload when you make changes to the code. +You can also run in production mode which will create an optimized build of your app. + +You can configure the mode, as well as other options through flags. + +```bash +$ reflex run --help +Usage: reflex run [OPTIONS] + + Run the app in the current directory. + +Options: + --env [dev|prod] The environment to run the app in. + [default: Env.DEV] + --frontend-only Execute only frontend. + --backend-only Execute only backend. + --frontend-port TEXT Specify a different frontend port. + [default: 3000] + --backend-port TEXT Specify a different backend port. [default: + 8000] + --backend-host TEXT Specify the backend host. [default: + 0.0.0.0] + --loglevel [debug|info|warning|error|critical] + The log level to use. [default: + LogLevel.INFO] + --help Show this message and exit. +``` + +## Export + +You can export your app's frontend and backend to zip files using the `reflex export` command. + +The frontend is a compiled NextJS app, which can be deployed to a static hosting service like Github Pages or Vercel. +However this is just a static build, so you will need to deploy the backend separately. +See the self-hosting guide for more information. + +## Rename + +The `reflex rename` command allows you to rename your Reflex app. This updates the app name in the configuration files. + +```bash +$ reflex rename --help +Usage: reflex rename [OPTIONS] NEW_NAME + + Rename the app in the current directory. + +Options: + --loglevel [debug|default|info|warning|error|critical] + The log level to use. + --help Show this message and exit. +``` + +## Cloud + +The `reflex cloud` command provides access to the Reflex Cloud hosting service. It includes subcommands for managing apps, projects, secrets, and more. + +For detailed documentation on Reflex Cloud and deployment, see the [Cloud Quick Start Guide](https://reflex.dev/docs/hosting/deploy-quick-start/). + +## Script + +The `reflex script` command provides access to helper scripts for Reflex development. + +```bash +$ reflex script --help +Usage: reflex script [OPTIONS] COMMAND [ARGS]... + + Subcommands for running helper scripts. + +Options: + --help Show this message and exit. +``` diff --git a/docs/api-reference/event_triggers.md b/docs/api-reference/event_triggers.md new file mode 100644 index 00000000000..3e75905c3f6 --- /dev/null +++ b/docs/api-reference/event_triggers.md @@ -0,0 +1,389 @@ +```python exec +from datetime import datetime + +import reflex as rx + +from pcweb.templates.docpage import docdemo, h1_comp, text_comp + +SYNTHETIC_EVENTS = [ + { + "name": "on_focus", + "description": "The on_focus event handler is called when the element (or some element inside of it) receives focus. For example, it’s called when the user clicks on a text input.", + "state": """class FocusState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self, text): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.input(value=FocusState.text, on_focus=FocusState.change_text)""", + }, + { + "name": "on_blur", + "description": "The on_blur event handler is called when focus has left the element (or left some element inside of it). For example, it’s called when the user clicks outside of a focused text input.", + "state": """class BlurState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self, text): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.input(value=BlurState.text, on_blur=BlurState.change_text)""", + }, + { + "name": "on_change", + "description": "The on_change event handler is called when the value of an element has changed. For example, it’s called when the user types into a text input each keystroke triggers the on change.", + "state": """class ChangeState(rx.State): + checked: bool = False + + @rx.event + def set_checked(self): + self.checked = not self.checked + +""", + "example": """rx.switch(on_change=ChangeState.set_checked)""", + }, + { + "name": "on_click", + "description": "The on_click event handler is called when the user clicks on an element. For example, it’s called when the user clicks on a button.", + "state": """class ClickState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(ClickState.text, on_click=ClickState.change_text)""", + }, + { + "name": "on_context_menu", + "description": "The on_context_menu event handler is called when the user right-clicks on an element. For example, it’s called when the user right-clicks on a button.", + "state": """class ContextState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(ContextState.text, on_context_menu=ContextState.change_text)""", + }, + { + "name": "on_double_click", + "description": "The on_double_click event handler is called when the user double-clicks on an element. For example, it’s called when the user double-clicks on a button.", + "state": """class DoubleClickState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(DoubleClickState.text, on_double_click=DoubleClickState.change_text)""", + }, + { + "name": "on_mount", + "description": "The on_mount event handler is called after the component is rendered on the page. It is similar to a page on_load event, although it does not necessarily fire when navigating between pages. This event is particularly useful for initializing data, making API calls, or setting up component-specific state when a component first appears.", + "state": """class MountState(rx.State): + events: list[str] = [] + data: list[dict] = [] + loading: bool = False + + @rx.event + def on_mount(self): + self.events = self.events[-4:] + ["on_mount @ " + str(datetime.now())] + + @rx.event + async def load_data(self): + # Common pattern: Set loading state, yield to update UI, then fetch data + self.loading = True + yield + # Simulate API call + import asyncio + await asyncio.sleep(1) + self.data = [dict(id=1, name="Item 1"), dict(id=2, name="Item 2")] + self.loading = False +""", + "example": """rx.vstack( + rx.heading("Component Lifecycle Demo"), + rx.foreach(MountState.events, rx.text), + rx.cond( + MountState.loading, + rx.spinner(), + rx.foreach( + MountState.data, + lambda item: rx.text(f"ID: {item['id']} - {item['name']}") + ) + ), + on_mount=MountState.on_mount, +)""", + }, + { + "name": "on_unmount", + "description": "The on_unmount event handler is called after removing the component from the page. However, on_unmount will only be called for internal navigation, not when following external links or refreshing the page. This event is useful for cleaning up resources, saving state, or performing cleanup operations before a component is removed from the DOM.", + "state": """class UnmountState(rx.State): + events: list[str] = [] + resource_id: str = "resource-12345" + status: str = "Resource active" + + @rx.event + def on_unmount(self): + self.events = self.events[-4:] + ["on_unmount @ " + str(datetime.now())] + # Common pattern: Clean up resources when component is removed + self.status = f"Resource {self.resource_id} cleaned up" + + @rx.event + def initialize_resource(self): + self.status = f"Resource {self.resource_id} initialized" +""", + "example": """rx.vstack( + rx.heading("Unmount Demo"), + rx.foreach(UnmountState.events, rx.text), + rx.text(UnmountState.status), + rx.link( + rx.button("Navigate Away (Triggers Unmount)"), + href="/docs", + ), + on_mount=UnmountState.initialize_resource, + on_unmount=UnmountState.on_unmount, +)""", + }, + { + "name": "on_mouse_up", + "description": "The on_mouse_up event handler is called when the user releases a mouse button on an element. For example, it’s called when the user releases the left mouse button on a button.", + "state": """class MouseUpState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseUpState.text, on_mouse_up=MouseUpState.change_text)""", + }, + { + "name": "on_mouse_down", + "description": "The on_mouse_down event handler is called when the user presses a mouse button on an element. For example, it’s called when the user presses the left mouse button on a button.", + "state": """class MouseDown(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseDown.text, on_mouse_down=MouseDown.change_text)""", + }, + { + "name": "on_mouse_enter", + "description": "The on_mouse_enter event handler is called when the user’s mouse enters an element. For example, it’s called when the user’s mouse enters a button.", + "state": """class MouseEnter(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseEnter.text, on_mouse_enter=MouseEnter.change_text)""", + }, + { + "name": "on_mouse_leave", + "description": "The on_mouse_leave event handler is called when the user’s mouse leaves an element. For example, it’s called when the user’s mouse leaves a button.", + "state": """class MouseLeave(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseLeave.text, on_mouse_leave=MouseLeave.change_text)""", + }, + { + "name": "on_mouse_move", + "description": "The on_mouse_move event handler is called when the user moves the mouse over an element. For example, it’s called when the user moves the mouse over a button.", + "state": """class MouseMove(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseMove.text, on_mouse_move=MouseMove.change_text)""", + }, + { + "name": "on_mouse_out", + "description": "The on_mouse_out event handler is called when the user’s mouse leaves an element. For example, it’s called when the user’s mouse leaves a button.", + "state": """class MouseOut(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseOut.text, on_mouse_out=MouseOut.change_text)""", + }, + { + "name": "on_mouse_over", + "description": "The on_mouse_over event handler is called when the user’s mouse enters an element. For example, it’s called when the user’s mouse enters a button.", + "state": """class MouseOver(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseOver.text, on_mouse_over=MouseOver.change_text)""", + }, + { + "name": "on_scroll", + "description": "The on_scroll event handler is called when the user scrolls the page. For example, it’s called when the user scrolls the page down.", + "state": """class ScrollState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.vstack( + rx.text("Scroll to make the text below change."), + rx.text(ScrollState.text), + rx.text("Scroll to make the text above change."), + on_scroll=ScrollState.change_text, + overflow = "auto", + height = "3em", + width = "100%", + )""", + }, +] +for i in SYNTHETIC_EVENTS: + exec(i["state"]) + +def component_grid(): + events = [] + for event in SYNTHETIC_EVENTS: + events.append( + rx.vstack( + h1_comp(text=event["name"]), + text_comp(text=event["description"]), + docdemo( + event["example"], state=event["state"], comp=eval(event["example"]) + ), + align_items="left", + ) + ) + + return rx.box(*events) +``` + +# Event Triggers + +Components can modify the state based on user events such as clicking a button or entering text in a field. +These events are triggered by event triggers. + +Event triggers are component specific and are listed in the documentation for each component. + +## Component Lifecycle Events + +Reflex components have lifecycle events like `on_mount` and `on_unmount` that allow you to execute code at specific points in a component's existence. These events are crucial for initializing data, cleaning up resources, and creating dynamic user interfaces. + +### When Lifecycle Events Are Activated + +- **on_mount**: This event is triggered immediately after a component is rendered and attached to the DOM. It fires: + - When a page containing the component is first loaded + - When a component is conditionally rendered (appears after being hidden) + - When navigating to a page containing the component using internal navigation + - It does NOT fire when the page is refreshed or when following external links + +- **on_unmount**: This event is triggered just before a component is removed from the DOM. It fires: + - When navigating away from a page containing the component using internal navigation + - When a component is conditionally removed from the DOM (e.g., via a condition that hides it) + - It does NOT fire when refreshing the page, closing the browser tab, or following external links + +## Page Load Events + +In addition to component lifecycle events, Reflex also provides page-level events like `on_load` that are triggered when a page loads. The `on_load` event is useful for: + +- Fetching data when a page first loads +- Checking authentication status +- Initializing page-specific state +- Setting default values for cookies or browser storage + +You can specify an event handler to run when the page loads using the `on_load` parameter in the `@rx.page` decorator or `app.add_page()` method: + +```python +class State(rx.State): + data: dict = dict() + + @rx.event + def get_data(self): + # Fetch data when the page loads + self.data = fetch_data() + +@rx.page(on_load=State.get_data) +def index(): + return rx.text('Data loaded on page load') +``` + +This is particularly useful for authentication checks: + +```python +class State(rx.State): + authenticated: bool = False + + @rx.event + def check_auth(self): + # Check if user is authenticated + self.authenticated = check_auth() + if not self.authenticated: + return rx.redirect('/login') + +@rx.page(on_load=State.check_auth) +def protected_page(): + return rx.text('Protected content') +``` + +For more details on page load events, see the [page load events documentation](/docs/events/page_load_events). + +# Event Reference + +```python eval +rx.box( + rx.divider(), + component_grid(), +) +``` diff --git a/docs/api-reference/plugins.md b/docs/api-reference/plugins.md new file mode 100644 index 00000000000..ff0643a5e2f --- /dev/null +++ b/docs/api-reference/plugins.md @@ -0,0 +1,241 @@ +```python exec +import reflex as rx +``` + +# Plugins + +Reflex supports a plugin system that allows you to extend the framework's functionality during the compilation process. Plugins can add frontend dependencies, modify build configurations, generate static assets, and perform custom tasks before compilation. + +## Configuring Plugins + +Plugins are configured in your `rxconfig.py` file using the `plugins` parameter: + +```python +import reflex as rx + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.SitemapPlugin(), + rx.plugins.TailwindV4Plugin(), + ], +) +``` + +## Built-in Plugins + +Reflex comes with several built-in plugins that provide common functionality. + +### SitemapPlugin + +The `SitemapPlugin` automatically generates a sitemap.xml file for your application, which helps search engines discover and index your pages. + +```python +import reflex as rx + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.SitemapPlugin(), + ], +) +``` + +The sitemap plugin automatically includes all your app's routes. For dynamic routes or custom configuration, you can add sitemap metadata to individual pages: + +```python +@rx.page(route="/blog/[slug]", context={"sitemap": {"changefreq": "weekly", "priority": 0.8}}) +def blog_post(): + return rx.text("Blog post content") + +@rx.page(route="/about", context={"sitemap": {"changefreq": "monthly", "priority": 0.5}}) +def about(): + return rx.text("About page") +``` + +The sitemap configuration supports the following options: + +- `loc`: Custom URL for the page (required for dynamic routes) +- `lastmod`: Last modification date (datetime object) +- `changefreq`: How frequently the page changes (`"always"`, `"hourly"`, `"daily"`, `"weekly"`, `"monthly"`, `"yearly"`, `"never"`) +- `priority`: Priority of this URL relative to other URLs (0.0 to 1.0) + +### TailwindV4Plugin + +The `TailwindV4Plugin` provides support for Tailwind CSS v4, which is the recommended version for new projects and includes performance improvements and new features. + +```python +import reflex as rx + +# Basic configuration +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV4Plugin(), + ], +) +``` + +You can customize the Tailwind configuration by passing a config dictionary: + +```python +import reflex as rx + +tailwind_config = { + "theme": { + "extend": { + "colors": { + "brand": { + "50": "#eff6ff", + "500": "#3b82f6", + "900": "#1e3a8a", + } + } + } + }, + "plugins": ["@tailwindcss/typography"], +} + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV4Plugin(tailwind_config), + ], +) +``` + +### TailwindV3Plugin + +The `TailwindV3Plugin` integrates Tailwind CSS v3 into your Reflex application. While still supported, TailwindV4Plugin is recommended for new projects. + +```python +import reflex as rx + +# Basic configuration +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV3Plugin(), + ], +) +``` + +You can customize the Tailwind configuration by passing a config dictionary: + +```python +import reflex as rx + +tailwind_config = { + "theme": { + "extend": { + "colors": { + "primary": "#3b82f6", + "secondary": "#64748b", + } + } + }, + "plugins": ["@tailwindcss/typography", "@tailwindcss/forms"], +} + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV3Plugin(tailwind_config), + ], +) +``` + +## Plugin Management + +### Default Plugins + +Some plugins are enabled by default. Currently, the `SitemapPlugin` is enabled automatically. If you want to disable a default plugin, use the `disable_plugins` parameter: + +```python +import reflex as rx + +config = rx.Config( + app_name="my_app", + disable_plugins=["reflex.plugins.sitemap.SitemapPlugin"], +) +``` + +### Plugin Order + +Plugins are executed in the order they appear in the `plugins` list. This can be important if plugins have dependencies on each other or modify the same files. + +```python +import reflex as rx + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV4Plugin(), # Runs first + rx.plugins.SitemapPlugin(), # Runs second + ], +) +``` + +## Plugin Architecture + +All plugins inherit from the base `Plugin` class and can implement several lifecycle methods: + +```python +class Plugin: + def get_frontend_development_dependencies(self, **context) -> list[str]: + """Get NPM packages required by the plugin for development.""" + return [] + + def get_frontend_dependencies(self, **context) -> list[str]: + """Get NPM packages required by the plugin.""" + return [] + + def get_static_assets(self, **context) -> Sequence[tuple[Path, str | bytes]]: + """Get static assets required by the plugin.""" + return [] + + def get_stylesheet_paths(self, **context) -> Sequence[str]: + """Get paths to stylesheets required by the plugin.""" + return [] + + def pre_compile(self, **context) -> None: + """Called before compilation to perform custom tasks.""" + pass +``` + +### Creating Custom Plugins + +You can create custom plugins by inheriting from the base `Plugin` class: + +```python +from reflex.plugins.base import Plugin +from pathlib import Path + +class CustomPlugin(Plugin): + def get_frontend_dependencies(self, **context): + return ["my-custom-package@1.0.0"] + + def pre_compile(self, **context): + # Custom logic before compilation + print("Running custom plugin logic...") + + # Add a custom task + context["add_save_task"](self.create_custom_file) + + def create_custom_file(self): + return "public/custom.txt", "Custom content" +``` + +Then use it in your configuration: + +```python +import reflex as rx +from my_plugins import CustomPlugin + +config = rx.Config( + app_name="my_app", + plugins=[ + CustomPlugin(), + ], +) +``` diff --git a/docs/api-reference/special_events.md b/docs/api-reference/special_events.md new file mode 100644 index 00000000000..675f465ca70 --- /dev/null +++ b/docs/api-reference/special_events.md @@ -0,0 +1,123 @@ +```python exec +import reflex as rx +``` + +# Special Events + +Reflex includes a set of built-in special events that can be utilized as event triggers +or returned from event handlers in your applications. These events enhance interactivity and user experience. +Below are the special events available in Reflex, along with explanations of their functionality: + +## rx.console_log + +Perform a console.log in the browser's console. + +```python demo +rx.button('Log', on_click=rx.console_log('Hello World!')) +``` + +When triggered, this event logs a specified message to the browser's developer console. +It's useful for debugging and monitoring the behavior of your application. + +## rx.scroll_to + +scroll to an element in the page + +```python demo +rx.button( + "Scroll to download button", + on_click=rx.scroll_to("download button") + +) +``` + +When this is triggered, it scrolls to an element passed by id as parameter. Click on button to scroll to download button (rx.download section) at the bottom of the page + +## rx.redirect + +Redirect the user to a new path within the application. + +### Parameters + +- `path`: The destination path or URL to which the user should be redirected. +- `external`: If set to True, the redirection will open in a new tab. Defaults to `False`. + +```python demo +rx.vstack( + rx.button("open in tab", on_click=rx.redirect("/docs/api-reference/special-events")), + rx.button("open in new tab", on_click=rx.redirect('https://github.com/reflex-dev/reflex/', is_external=True)) +) +``` + +When this event is triggered, it navigates the user to a different page or location within your Reflex application. +By default, the redirection occurs in the same tab. However, if you set the external parameter to True, the redirection +will open in a new tab or window, providing a seamless user experience. + +This event can also be run from an event handler in State. It is necessary to `return` the `rx.redirect()`. + +```python demo exec +class RedirectExampleState(rx.State): + """The app state.""" + + @rx.event + def change_page(self): + return rx.redirect('https://github.com/reflex-dev/reflex/', is_external=True) + +def redirect_example(): + return rx.vstack( + rx.button("Change page in State", on_click=RedirectExampleState.change_page), + ) +``` + +## rx.set_clipboard + +Set the specified text content to the clipboard. + +```python demo +rx.button('Copy "Hello World" to clipboard',on_click=rx.set_clipboard('Hello World'),) +``` + +This event allows you to copy a given text or content to the user's clipboard. +It's handy when you want to provide a "Copy to Clipboard" feature in your application, +allowing users to easily copy information to paste elsewhere. + +## rx.set_value + +Set the value of a specified reference element. + +```python demo +rx.hstack( + rx.input(id='input1'), + rx.button( + 'Erase', on_click=rx.set_value('input1', '') + ), +) +``` + +With this event, you can modify the value of a particular HTML element, typically an input field or another form element. + +## rx.window_alert + +Create a window alert in the browser. + +```python demo +rx.button('Alert', on_click=rx.window_alert('Hello World!')) +``` + +## rx.download + +Download a file at a given path. + +Parameters: + +- `url`: The URL of the file to be downloaded. +- `data`: The data to be downloaded. Should be `str` or `bytes`, `data:` URI, `PIL.Image`, or any state Var (to be converted to JSON). +- `filename`: The desired filename of the downloaded file. + +```md alert +# `url` and `data` args are mutually exclusive, and at least one of them must be provided. +``` + +```python demo +rx.button("Download", on_click=rx.download(url="/reflex_banner.webp", filename="different_name_logo.webp"), id="download button") +``` diff --git a/docs/api-reference/utils.md b/docs/api-reference/utils.md new file mode 100644 index 00000000000..c0424ab6f36 --- /dev/null +++ b/docs/api-reference/utils.md @@ -0,0 +1,169 @@ +```python exec +import reflex as rx +``` + +# Utility Functions + +Reflex provides utility functions to help with common tasks in your applications. + +## run_in_thread + +The `run_in_thread` function allows you to run a **non-async** function in a separate thread, which is useful for preventing long-running operations from blocking the UI event queue. + +```python +async def run_in_thread(func: Callable) -> Any +``` + +### Parameters + +- `func`: The non-async function to run in a separate thread. + +### Returns + +- The return value of the function. + +### Raises + +- `ValueError`: If the function is an async function. + +### Usage + +```python demo exec id=run_in_thread_demo +import asyncio +import dataclasses +import time +import reflex as rx + + +def quick_blocking_function(): + time.sleep(0.5) + return "Quick task completed successfully!" + + +def slow_blocking_function(): + time.sleep(3.0) + return "This should never be returned due to timeout!" + + +@dataclasses.dataclass +class TaskInfo: + result: str = "No result yet" + status: str = "Idle" + + +class RunInThreadState(rx.State): + tasks: list[TaskInfo] = [] + + @rx.event(background=True) + async def run_quick_task(self): + """Run a quick task that completes within the timeout.""" + async with self: + task_ix = len(self.tasks) + self.tasks.append(TaskInfo(status="Running quick task...")) + task_info = self.tasks[task_ix] + + try: + result = await rx.run_in_thread(quick_blocking_function) + async with self: + task_info.result = result + task_info.status = "Complete" + except Exception as e: + async with self: + task_info.result = f"Error: {str(e)}" + task_info.status = "Failed" + + @rx.event(background=True) + async def run_slow_task(self): + """Run a slow task that exceeds the timeout.""" + async with self: + task_ix = len(self.tasks) + self.tasks.append(TaskInfo(status="Running slow task...")) + task_info = self.tasks[task_ix] + + try: + # Run with a timeout of 1 second (not enough time) + result = await asyncio.wait_for( + rx.run_in_thread(slow_blocking_function), + timeout=1.0, + ) + async with self: + task_info.result = result + task_info.status = "Complete" + except asyncio.TimeoutError: + async with self: + # Warning: even though we stopped waiting for the task, + # it may still be running in thread + task_info.result = "Task timed out after 1 second!" + task_info.status = "Timeout" + except Exception as e: + async with self: + task_info.result = f"Error: {str(e)}" + task_info.status = "Failed" + + +def run_in_thread_example(): + return rx.vstack( + rx.heading("run_in_thread Example", size="3"), + rx.hstack( + rx.button( + "Run Quick Task", + on_click=RunInThreadState.run_quick_task, + color_scheme="green", + ), + rx.button( + "Run Slow Task (exceeds timeout)", + on_click=RunInThreadState.run_slow_task, + color_scheme="red", + ), + ), + rx.vstack( + rx.foreach( + RunInThreadState.tasks.reverse()[:10], + lambda task: rx.hstack( + rx.text(task.status), + rx.spacer(), + rx.text(task.result), + ), + ), + align="start", + width="100%", + ), + width="100%", + align_items="start", + spacing="4", + ) +``` + +### When to Use run_in_thread + +Use `run_in_thread` when you need to: + +1. Execute CPU-bound operations that would otherwise block the event loop +2. Call synchronous libraries that don't have async equivalents +3. Prevent long-running operations from blocking UI responsiveness + +### Example: Processing a Large File + +```python +import reflex as rx +import time + +class FileProcessingState(rx.State): + progress: str = "Ready" + + @rx.event(background=True) + async def process_large_file(self): + async with self: + self.progress = "Processing file..." + + def process_file(): + # Simulate processing a large file + time.sleep(5) + return "File processed successfully!" + + # Save the result to a local variable to avoid blocking the event loop. + result = await rx.run_in_thread(process_file) + async with self: + # Then assign the local result to the state while holding the lock. + self.progress = result +``` diff --git a/docs/api-reference/var_system.md b/docs/api-reference/var_system.md new file mode 100644 index 00000000000..10c2b84de2b --- /dev/null +++ b/docs/api-reference/var_system.md @@ -0,0 +1,82 @@ +# Reflex's Var System + +## Motivation + +Reflex supports some basic operations in state variables on the frontend. +Reflex automatically converts variable operations from Python into a JavaScript equivalent. + +Here's an example of a Reflex conditional in Python that returns "Pass" if the threshold is equal to or greater than 50 and "Fail" otherwise: + +```py +rx.cond( + State.threshold >= 50, + "Pass", + "Fail", +) +``` + +The conditional to roughly the following in Javascript: + +```js +state.threshold >= 50 ? "Pass" : "Fail"; +``` + +## Overview + +Simply put, a `Var` in Reflex represents a Javascript expression. +If the type is known, it can be any of the following: + +- `NumberVar` represents an expression that evaluates to a Javascript `number`. `NumberVar` can support both integers and floating point values +- `BooleanVar` represents a boolean expression. For example: `false`, `3 > 2`. +- `StringVar` represents an expression that evaluates to a string. For example: `'hello'`, `(2).toString()`. +- `ArrayVar` represents an expression that evaluates to an array object. For example: `[1, 2, 3]`, `'words'.split()`. +- `ObjectVar` represents an expression that evaluates to an object. For example: `\{a: 2, b: 3}`, `\{deeply: \{nested: \{value: false}}}`. +- `NoneVar` represent null values. These can be either `undefined` or `null`. + +## Creating Vars + +State fields are converted to `Var` by default. Additionally, you can create a `Var` from Python values using `rx.Var.create()`: + +```py +rx.Var.create(4) # NumberVar +rx.Var.create("hello") # StringVar +rx.Var.create([1, 2, 3]) # ArrayVar +``` + +If you want to explicitly create a `Var` from a raw Javascript string, you can instantiate `rx.Var` directly: + +```py +rx.Var("2", _var_type=int).guess_type() # NumberVar +``` + +In the example above, `.guess_type()` will attempt to downcast from a generic `Var` type into `NumberVar`. +For this example, calling the function `.to(int)` can also be used in place of `.guess_type()`. + +## Operations + +The `Var` system also supports some other basic operations. +For example, `NumberVar` supports basic arithmetic operations like `+` and `-`, as in Python. +It also supports comparisons that return a `BooleanVar`. + +Custom `Var` operations can also be defined: + +```py +from reflex.vars import var_operation, var_operation_return, ArrayVar, NumberVar + +@var_operation +def multiply_array_values(a: ArrayVar): + return var_operation_return( + js_expression=f"\{a}.reduce((p, c) => p * c, 1)", + var_type=int, + ) + +def factorial(value: NumberVar): + return rx.cond( + value <= 1, + 1, + multiply_array_values(rx.Var.range(1, value+1)) + ) +``` + +Use `js_expression` to pass explicit JavaScript expressions; in the `multiply_array_values` example, we pass in a JavaScript expression that calculates the product of all elements in an array called `a` by using the reduce method to multiply each element with the accumulated result, starting from an initial value of 1. +Later, we leverage `rx.cond` in the' factorial' function, we instantiate an array using the `range` function, and pass this array to `multiply_array_values`. diff --git a/docs/api-routes/overview.md b/docs/api-routes/overview.md new file mode 100644 index 00000000000..30e5ef31f7a --- /dev/null +++ b/docs/api-routes/overview.md @@ -0,0 +1,152 @@ +```python exec +import reflex as rx +``` + +# API Transformer + +In addition to your frontend app, Reflex uses a FastAPI backend to serve your app. The API transformer feature allows you to transform or extend the ASGI app that serves your Reflex application. + +## Overview + +The API transformer provides a way to: + +1. Integrate existing FastAPI or Starlette applications with your Reflex app +2. Apply middleware or transformations to the ASGI app +3. Extend your Reflex app with additional API endpoints + +This is useful for creating a backend API that can be used for purposes beyond your Reflex app, or for integrating Reflex with existing backend services. + +## Using API Transformer + +You can set the `api_transformer` parameter when initializing your Reflex app: + +```python +import reflex as rx +from fastapi import FastAPI, Depends +from fastapi.security import OAuth2PasswordBearer + +# Create a FastAPI app +fastapi_app = FastAPI(title="My API") + +# Add routes to the FastAPI app +@fastapi_app.get("/api/items") +async def get_items(): + return dict(items=["Item1", "Item2", "Item3"]) + +# Create a Reflex app with the FastAPI app as the API transformer +app = rx.App(api_transformer=fastapi_app) +``` + +## Types of API Transformers + +The `api_transformer` parameter can accept: + +1. A Starlette or FastAPI instance +2. A callable that takes an ASGIApp and returns an ASGIApp +3. A sequence of the above + +### Using a FastAPI or Starlette Instance + +When you provide a FastAPI or Starlette instance as the API transformer, Reflex will mount its internal API to your app, allowing you to define additional routes: + +```python +import reflex as rx +from fastapi import FastAPI, Depends +from fastapi.security import OAuth2PasswordBearer + +# Create a FastAPI app with authentication +fastapi_app = FastAPI(title="Secure API") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Add a protected route +@fastapi_app.get("/api/protected") +async def protected_route(token: str = Depends(oauth2_scheme)): + return dict(message="This is a protected endpoint") + +# Create a token endpoint +@fastapi_app.post("/token") +async def login(username: str, password: str): + # In a real app, you would validate credentials + if username == "user" and password == "password": + return dict(access_token="example_token", token_type="bearer") + return dict(error="Invalid credentials") + +# Create a Reflex app with the FastAPI app as the API transformer +app = rx.App(api_transformer=fastapi_app) +``` + +### Using a Callable Transformer + +You can also provide a callable that transforms the ASGI app: + +```python +import reflex as rx +from starlette.middleware.cors import CORSMiddleware + +# Create a transformer function that returns a transformed ASGI app +def add_cors_middleware(app): + # Wrap the app with CORS middleware and return the wrapped app + return CORSMiddleware( + app=app, + allow_origins=["https://example.com"], + allow_methods=["*"], + allow_headers=["*"], + ) + +# Create a Reflex app with the transformer +app = rx.App(api_transformer=add_cors_middleware) +``` + +### Using Multiple Transformers + +You can apply multiple transformers by providing a sequence: + +```python +import reflex as rx +from fastapi import FastAPI +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware + +# Create a FastAPI app +fastapi_app = FastAPI(title="My API") + +# Add routes to the FastAPI app +@fastapi_app.get("/api/items") +async def get_items(): + return dict(items=["Item1", "Item2", "Item3"]) + +# Create a transformer function +def add_logging_middleware(app): + # This is a simple example middleware that logs requests + async def middleware(scope, receive, send): + # Log the request path + path = scope["path"] + print("Request:", path) + await app(scope, receive, send) + return middleware + +# Create a Reflex app with multiple transformers +app = rx.App(api_transformer=[fastapi_app, add_logging_middleware]) +``` + +## Reserved Routes + +Some routes on the backend are reserved for the runtime of Reflex, and should not be overridden unless you know what you are doing. + +### Ping + +`localhost:8000/ping/`: You can use this route to check the health of the backend. + +The expected return is `"pong"`. + +### Event + +`localhost:8000/_event`: the frontend will use this route to notify the backend that an event occurred. + +```md alert error +# Overriding this route will break the event communication +``` + +### Upload + +`localhost:8000/_upload`: This route is used for the upload of file when using `rx.upload()`. diff --git a/docs/assets/overview.md b/docs/assets/overview.md new file mode 100644 index 00000000000..d864bbf892d --- /dev/null +++ b/docs/assets/overview.md @@ -0,0 +1,92 @@ +```python exec +import reflex as rx +``` + +# Assets + +Static files such as images and stylesheets can be placed in `assets/` folder of the project. These files can be referenced within your app. + +```md alert +# Assets are copied during the build process. + +Any files placed within the `assets/` folder at runtime will not be available to the app +when running in production mode. The `assets/` folder should only be used for static files. +``` + +## Referencing Assets + +There are two ways to reference assets in your Reflex app: + +### 1. Direct Path Reference + +To reference an image in the `assets/` folder, pass the relative path as a prop. + +For example, you can store your logo in your assets folder: + +```bash +assets +└── Reflex.svg +``` + +Then you can display it using a `rx.image` component: + +```python demo +rx.image(src="https://web.reflex-assets.dev/other/Reflex.svg", width="5em") +``` + +```md alert +# Always prefix the asset path with a forward slash `/` to reference the asset from the root of the project, or it may not display correctly on non-root pages. +``` + +### 2. Using rx.asset Function + +The `rx.asset` function provides a more flexible way to reference assets in your app. It supports both local assets (in the app's `assets/` directory) and shared assets (placed next to your Python files). + +#### Local Assets + +Local assets are stored in the app's `assets/` directory and are referenced using `rx.asset`: + +```python demo +rx.image(src=rx.asset("Reflex.svg"), width="5em") +``` + +#### Shared Assets + +Shared assets are placed next to your Python file and are linked to the app's external assets directory. This is useful for creating reusable components with their own assets: + +```python box +# my_component.py +import reflex as rx + +# my_script.js is located in the same directory as this Python file +def my_component(): + return rx.box( + rx.script(src=rx.asset("my_script.js", shared=True)), + "Component with custom script" + ) +``` + +You can also specify a subfolder for shared assets: + +```python box +# my_component.py +import reflex as rx + +# image.png is located in a subfolder next to this Python file +def my_component_with_image(): + return rx.image( + src=rx.asset("image.png", shared=True, subfolder="images") + ) +``` + +```md alert +# Shared assets are linked to your app via symlinks. + +When using `shared=True`, the asset is symlinked from its original location to your app's external assets directory. This allows you to keep assets alongside their related code. +``` + +## Favicon + +The favicon is the small icon that appears in the browser tab. + +You can add a `favicon.ico` file to the `assets/` folder to change the favicon. diff --git a/docs/assets/upload_and_download_files.md b/docs/assets/upload_and_download_files.md new file mode 100644 index 00000000000..685309f8ab7 --- /dev/null +++ b/docs/assets/upload_and_download_files.md @@ -0,0 +1,147 @@ +```python exec +import reflex as rx +``` + +# Files + +In addition to any assets you ship with your app, many web app will often need to receive or send files, whether you want to share media, allow user to import their data, or export some backend data. + +In this section, we will cover all you need to know for manipulating files in Reflex. + +## Assets vs Upload Directory + +Before diving into file uploads and downloads, it's important to understand the difference between assets and the upload directory in Reflex: + +```python eval +# Simple table comparing assets vs upload directory +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Feature"), + rx.table.column_header_cell("Assets"), + rx.table.column_header_cell("Upload Directory"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell(rx.text("Purpose", font_weight="bold")), + rx.table.cell(rx.text("Static files included with your app (images, stylesheets, scripts)")), + rx.table.cell(rx.text("Dynamic files uploaded by users during runtime")), + ), + rx.table.row( + rx.table.cell(rx.text("Location", font_weight="bold")), + rx.table.cell(rx.hstack( + rx.code("assets/", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}), + rx.text(" folder or next to Python files (shared assets)"), + spacing="2", + )), + rx.table.cell(rx.hstack( + rx.code("uploaded_files/", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}), + rx.text(" directory (configurable)"), + spacing="2", + )), + ), + rx.table.row( + rx.table.cell(rx.text("Access Method", font_weight="bold")), + rx.table.cell(rx.hstack( + rx.code("rx.asset()", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}), + rx.text(" or direct path reference"), + spacing="2", + )), + rx.table.cell(rx.code("rx.get_upload_url()", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)})), + ), + rx.table.row( + rx.table.cell(rx.text("When to Use", font_weight="bold")), + rx.table.cell(rx.text("For files that are part of your application's codebase")), + rx.table.cell(rx.text("For files that users upload or generate through your application")), + ), + rx.table.row( + rx.table.cell(rx.text("Availability", font_weight="bold")), + rx.table.cell(rx.text("Available at compile time")), + rx.table.cell(rx.text("Available at runtime")), + ), + ), + width="100%", +) +``` + +For more information about assets, see the [Assets Overview](/docs/assets/overview/). + +## Download + +If you want to let the users of your app download files from your server to their computer, Reflex offer you two way. + +### With a regular link + +For some basic usage, simply providing the path to your resource in a `rx.link` will work, and clicking the link will download or display the resource. + +```python demo +rx.link("Download", href="/reflex_banner.webp") +``` + +### With `rx.download` event + +Using the `rx.download` event will always prompt the browser to download the file, even if it could be displayed in the browser. + +The `rx.download` event also allows the download to be triggered from another backend event handler. + +```python demo +rx.button( + "Download", + on_click=rx.download(url="/reflex_banner.webp"), +) +``` + +`rx.download` lets you specify a name for the file that will be downloaded, if you want it to be different from the name on the server side. + +```python demo +rx.button( + "Download and Rename", + on_click=rx.download( + url="/reflex_banner.webp", + filename="different_name_logo.png" + ), +) +``` + +If the data to download is not already available at a known URL, pass the `data` directly to the `rx.download` event from the backend. + +```python demo exec +import random + +class DownloadState(rx.State): + @rx.event + def download_random_data(self): + return rx.download( + data=",".join([str(random.randint(0, 100)) for _ in range(10)]), + filename="random_numbers.csv" + ) + +def download_random_data_button(): + return rx.button( + "Download random numbers", + on_click=DownloadState.download_random_data + ) +``` + +The `data` arg accepts `str` or `bytes` data, a `data:` URI, `PIL.Image`, or any state Var. If the Var is not already a string, it will be converted to a string using `JSON.stringify`. This allows complex state structures to be offered as JSON downloads. + +Reference page for `rx.download` [here](/docs/api-reference/special_events#rx.download). + +## Upload + +Uploading files to your server let your users interact with your app in a different way than just filling forms to provide data. + +The component `rx.upload` let your users upload files on the server. + +Here is a basic example of how it is used: + +```python +def index(): + return rx.fragment( + rx.upload(rx.text("Upload files"), rx.icon(tag="upload")), + rx.button(on_submit=State.) + ) +``` + +For detailed information, see the reference page of the component [here](/docs/library/forms/upload). diff --git a/docs/authentication/authentication_overview.md b/docs/authentication/authentication_overview.md new file mode 100644 index 00000000000..68d97ce78b5 --- /dev/null +++ b/docs/authentication/authentication_overview.md @@ -0,0 +1,25 @@ + +# Authentication Overview + +Many apps require authentication to manage users. There are a few different ways to accomplish this in Reflex: + +We have solutions that currently exist outside of the core framework: + +1. Local Auth: Uses your own database: https://github.com/masenf/reflex-local-auth +2. Google Auth: Uses sign in with Google: https://github.com/masenf/reflex-google-auth +3. Captcha: Generates tests that humans can pass but automated systems cannot: https://github.com/masenf/reflex-google-recaptcha-v2 +4. Magic Link Auth: A passwordless login method that sends a unique, one-time-use URL to a user's email: https://github.com/masenf/reflex-magic-link-auth +5. Clerk Auth: A community member wrapped this component and hooked it up in this app: https://github.com/TimChild/reflex-clerk-api +6. Descope Auth: Enables authentication with Descope, supporting passwordless, social login, SSO, and MFA: https://github.com/descope-sample-apps/reflex-descope-auth + +If you're using the AI Builder, you can also use the built-in [Authentication Integrations](/docs/ai-builder/integrations/overview) which include Azure Auth, Google Auth, Okta Auth, and Descope. + +## Guidance for Implementing Authentication + +- Store sensitive user tokens and information in [backend-only vars](/docs/vars/base_vars#backend-only-vars). +- Validate user session and permissions for each event handler that performs an authenticated action and all computed vars or loader events that access private data. +- All content that is statically rendered in the frontend (for example, data hardcoded or loaded at compile time in the UI) will be publicly available, even if the page redirects to a login or uses `rx.cond` to hide content. +- Only data that originates from state can be truly private and protected. +- When using cookies or local storage, a signed JWT can detect and invalidate any local tampering. + +More auth documentation on the way. Check back soon! diff --git a/docs/client_storage/overview.md b/docs/client_storage/overview.md new file mode 100644 index 00000000000..7016080a1d9 --- /dev/null +++ b/docs/client_storage/overview.md @@ -0,0 +1,45 @@ +```python exec +import reflex as rx +``` + +# Client-storage + +You can use the browser's local storage to persist state between sessions. +This allows user preferences, authentication cookies, other bits of information +to be stored on the client and accessed from different browser tabs. + +A client-side storage var looks and acts like a normal `str` var, except the +default value is either `rx.Cookie` or `rx.LocalStorage` depending on where the +value should be stored. The key name will be based on the var name, but this +can be overridden by passing `name="my_custom_name"` as a keyword argument. + +For more information see [Browser Storage](/docs/api-reference/browser-storage/). + +Try entering some values in the text boxes below and then load the page in a separate +tab or check the storage section of browser devtools to see the values saved in the browser. + +```python demo exec +class ClientStorageState(rx.State): + my_cookie: str = rx.Cookie("") + my_local_storage: str = rx.LocalStorage("") + custom_cookie: str = rx.Cookie(name="CustomNamedCookie", max_age=3600) + + @rx.event + def set_my_cookie(self, value: str): + self.my_cookie = value + + @rx.event + def set_my_local_storage(self, value: str): + self.my_local_storage = value + + @rx.event + def set_custom_cookie(self, value: str): + self.custom_cookie = value + +def client_storage_example(): + return rx.vstack( + rx.hstack(rx.text("my_cookie"), rx.input(value=ClientStorageState.my_cookie, on_change=ClientStorageState.set_my_cookie)), + rx.hstack(rx.text("my_local_storage"), rx.input(value=ClientStorageState.my_local_storage, on_change=ClientStorageState.set_my_local_storage)), + rx.hstack(rx.text("custom_cookie"), rx.input(value=ClientStorageState.custom_cookie, on_change=ClientStorageState.set_custom_cookie)), + ) +``` diff --git a/docs/components/conditional_rendering.md b/docs/components/conditional_rendering.md new file mode 100644 index 00000000000..3ce2101c05d --- /dev/null +++ b/docs/components/conditional_rendering.md @@ -0,0 +1,127 @@ +```python exec +import reflex as rx + +``` + +# Conditional Rendering + +Recall from the [basics](/docs/getting_started/basics) that we cannot use Python `if/else` statements when referencing state vars in Reflex. Instead, use the `rx.cond` component to conditionally render components or set props based on the value of a state var. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=6040&end=6463 +# Video: Conditional Rendering +``` + +```md alert +# Check out the API reference for [cond docs](/docs/library/dynamic-rendering/cond). +``` + +```python eval +rx.box(height="2em") +``` + +Below is a simple example showing how to toggle between two text components by checking the value of the state var `show`. + +```python demo exec +class CondSimpleState(rx.State): + show: bool = True + + @rx.event + def change(self): + self.show = not (self.show) + + +def cond_simple_example(): + return rx.vstack( + rx.button("Toggle", on_click=CondSimpleState.change), + rx.cond( + CondSimpleState.show, + rx.text("Text 1", color="blue"), + rx.text("Text 2", color="red"), + ), + ) +``` + +If `show` is `True` then the first component is rendered (in this case the blue text). Otherwise the second component is rendered (in this case the red text). + +## Conditional Props + +You can also set props conditionally using `rx.cond`. In this example, we set the `color` prop of a text component based on the value of the state var `show`. + +```python demo exec +class PropCondState(rx.State): + + value: int + + @rx.event + def set_end(self, value: list[int | float]): + self.value = value[0] + + +def cond_prop(): + return rx.slider( + default_value=[50], + on_value_commit=PropCondState.set_end, + color_scheme=rx.cond(PropCondState.value > 50, "green", "pink"), + width="100%", + ) +``` + +## Var Operations + +You can use [var operations](/docs/vars/var-operations) with the `cond` component for more complex conditions. See the full [cond reference](/docs/library/dynamic-rendering/cond) for more details. + +## Multiple Conditional Statements + +The [`rx.match`](/docs/library/dynamic-rendering/match) component in Reflex provides a powerful alternative to`rx.cond` for handling multiple conditional statements and structural pattern matching. This component allows you to handle multiple conditions and their associated components in a cleaner and more readable way compared to nested `rx.cond` structures. + +```python demo exec +from typing import List + +import reflex as rx + + +class MatchState(rx.State): + cat_breed: str = "" + animal_options: List[str] = [ + "persian", + "siamese", + "maine coon", + "ragdoll", + "pug", + "corgi", + ] + + @rx.event + def set_cat_breed(self, breed: str): + self.cat_breed = breed + + +def match_demo(): + return rx.flex( + rx.match( + MatchState.cat_breed, + ("persian", rx.text("Persian cat selected.")), + ("siamese", rx.text("Siamese cat selected.")), + ( + "maine coon", + rx.text("Maine Coon cat selected."), + ), + ("ragdoll", rx.text("Ragdoll cat selected.")), + rx.text("Unknown cat breed selected."), + ), + rx.select( + [ + "persian", + "siamese", + "maine coon", + "ragdoll", + "pug", + "corgi", + ], + value=MatchState.cat_breed, + on_change=MatchState.set_cat_breed, + ), + direction="column", + gap="2", + ) +``` diff --git a/docs/components/html_to_reflex.md b/docs/components/html_to_reflex.md new file mode 100644 index 00000000000..42dcd0f5f60 --- /dev/null +++ b/docs/components/html_to_reflex.md @@ -0,0 +1,16 @@ +# Convert HTML to Reflex + +To convert HTML, CSS, or any design into Reflex code, use our AI-powered build tool at [Reflex Build](https://build.reflex.dev). + +Simply paste your HTML, CSS, or describe what you want to build, and our AI will generate the corresponding Reflex code for you. + +## How to use Reflex Build + +1. Go to [Reflex Build](https://build.reflex.dev) +2. Paste your HTML/CSS code or describe your design +3. The AI will automatically generate Reflex code +4. Copy the generated code into your Reflex application + +## Convert Figma file to Reflex + +Check out this [Notion doc](https://www.notion.so/reflex-dev/Convert-HTML-to-Reflex-fe22d0641dcd4d5c91c8404ca41c7e77) for a walk through on how to convert a Figma file into Reflex code. diff --git a/docs/components/props.md b/docs/components/props.md new file mode 100644 index 00000000000..03779db0339 --- /dev/null +++ b/docs/components/props.md @@ -0,0 +1,93 @@ +```python exec +import reflex as rx +``` + +# Props + +Props modify the behavior and appearance of a component. They are passed in as keyword arguments to a component. + +## Component Props + +There are props that are shared between all components, but each component can also define its own props. + +For example, the `rx.image` component has a `src` prop that specifies the URL of the image to display and an `alt` prop that specifies the alternate text for the image. + +```python demo +rx.image( + src="https://web.reflex-assets.dev/other/logo.jpg", + alt="Reflex Logo", +) +``` + +Check the docs for the component you are using to see what props are available and how they affect the component (see the `rx.image` [reference](/docs/library/media/image#api-reference) page for example). + +## Common Props + +Components support many standard HTML properties as props. For example: the HTML [id]({"https://www.w3schools.com/html/html_id.asp"}) property is exposed directly as the prop `id`. The HTML [className]({"https://www.w3schools.com/jsref/prop_html_classname.asp"}) property is exposed as the prop `class_name` (note the Pythonic snake_casing!). + +```python demo +rx.box( + "Hello World", + id="box-id", + class_name=["class-name-1", "class-name-2",], +) +``` + +In the example above, the `class_name` prop of the `rx.box` component is assigned a list of class names. This means the `rx.box` component will be styled with the CSS classes `class-name-1` and `class-name-2`. + +## Style Props + +In addition to component-specific props, most built-in components support a full range of style props. You can use any [CSS property](https://www.w3schools.com/cssref/index.php) to style a component. + +```python demo +rx.button( + "Fancy Button", + border_radius="1em", + box_shadow="rgba(151, 65, 252, 0.8) 0 15px 30px -10px", + background_image="linear-gradient(144deg,#AF40FF,#5B42F3 50%,#00DDEB)", + box_sizing="border-box", + color="white", + opacity= 1, +) +``` + +See the [styling docs](/docs/styling/overview) to learn more about customizing the appearance of your app. + +## Binding Props to State + +```md alert warning +# Optional: Learn all about [State](/docs/state/overview) first. +``` + +Reflex apps define [State](/docs/state/overview) classes that hold variables that can change over time. + +State may be modified in response to things like user input like clicking a button, or in response to events like loading a page. + +State vars can be bound to component props, so that the UI always reflects the current state of the app. + +Try clicking the badge below to change its color. + +```python demo exec +class PropExampleState(rx.State): + text: str = "Hello World" + color: str = "red" + + @rx.event + def flip_color(self): + if self.color == "red": + self.color = "blue" + else: + self.color = "red" + + +def index(): + return rx.button( + PropExampleState.text, + color_scheme=PropExampleState.color, + on_click=PropExampleState.flip_color, + ) +``` + +In this example, the `color_scheme` prop is bound to the `color` state var. + +When the `flip_color` event handler is called, the `color` var is updated, and the `color_scheme` prop is updated to match. diff --git a/docs/components/rendering_iterables.md b/docs/components/rendering_iterables.md new file mode 100644 index 00000000000..bdcd195536b --- /dev/null +++ b/docs/components/rendering_iterables.md @@ -0,0 +1,301 @@ +```python exec +import reflex as rx + +``` + +# Rendering Iterables + +Recall again from the [basics](/docs/getting_started/basics) that we cannot use Python `for` loops when referencing state vars in Reflex. Instead, use the `rx.foreach` component to render components from a collection of data. + +For dynamic content that should automatically scroll to show the newest items, consider using the [auto scroll](/docs/library/dynamic-rendering/auto_scroll) component together with `rx.foreach`. + +```python demo exec +class IterState(rx.State): + color: list[str] = [ + "red", + "green", + "blue", + ] + + +def colored_box(color: str): + return rx.button(color, background_color=color) + + +def dynamic_buttons(): + return rx.vstack( + rx.foreach(IterState.color, colored_box), + ) + +``` + +Here's the same example using a lambda function. + +```python +def dynamic_buttons(): + return rx.vstack( + rx.foreach(IterState.color, lambda color: colored_box(color)), + ) +``` + +You can also use lambda functions directly with components without defining a separate function. + +```python +def dynamic_buttons(): + return rx.vstack( + rx.foreach(IterState.color, lambda color: rx.button(color, background_color=color)), + ) +``` + +In this first simple example we iterate through a `list` of colors and render a dynamic number of buttons. + +The first argument of the `rx.foreach` function is the state var that you want to iterate through. The second argument is a function that takes in an item from the data and returns a component. In this case, the `colored_box` function takes in a color and returns a button with that color. + +## For vs Foreach + +```md definition +# Regular For Loop + +- Use when iterating over constants. + +# Foreach + +- Use when iterating over state vars. +``` + +The above example could have been written using a regular Python `for` loop, since the data is constant. + +```python demo exec +colors = ["red", "green", "blue"] +def dynamic_buttons_for(): + return rx.vstack( + [colored_box(color) for color in colors], + ) +``` + +However, as soon as you need the data to be dynamic, you must use `rx.foreach`. + +```python demo exec +class DynamicIterState(rx.State): + color: list[str] = [ + "red", + "green", + "blue", + ] + + def add_color(self, form_data): + self.color.append(form_data["color"]) + +def dynamic_buttons_foreach(): + return rx.vstack( + rx.foreach(DynamicIterState.color, colored_box), + rx.form( + rx.input(name="color", placeholder="Add a color"), + rx.button("Add"), + on_submit=DynamicIterState.add_color, + ) + ) +``` + +## Render Function + +The function to render each item can be defined either as a separate function or as a lambda function. In the example below, we define the function `colored_box` separately and pass it to the `rx.foreach` function. + +```python demo exec +class IterState2(rx.State): + color: list[str] = [ + "red", + "green", + "blue", + ] + +def colored_box(color: rx.Var[str]): + return rx.button(color, background_color=color) + +def dynamic_buttons2(): + return rx.vstack( + rx.foreach(IterState2.color, colored_box), + ) + +``` + +Notice that the type annotation for the `color` parameter in the `colored_box` function is `rx.Var[str]` (rather than just `str`). This is because the `rx.foreach` function passes the item as a `Var` object, which is a wrapper around the actual value. This is what allows us to compile the frontend without knowing the actual value of the state var (which is only known at runtime). + +## Enumerating Iterables + +The function can also take an index as a second argument, meaning that we can enumerate through data as shown in the example below. + +```python demo exec +class IterIndexState(rx.State): + color: list[str] = [ + "red", + "green", + "blue", + ] + + +def create_button(color: rx.Var[str], index: int): + return rx.box( + rx.button(f"{index + 1}. {color}"), + padding_y="0.5em", + ) + +def enumerate_foreach(): + return rx.vstack( + rx.foreach( + IterIndexState.color, + create_button + ), + ) +``` + +Here's the same example using a lambda function. + +```python +def enumerate_foreach(): + return rx.vstack( + rx.foreach( + IterIndexState.color, + lambda color, index: create_button(color, index) + ), + ) +``` + +## Iterating Dictionaries + +We can iterate through a `dict` using a `foreach`. When the dict is passed through to the function that renders each item, it is presented as a list of key-value pairs `[("sky", "blue"), ("balloon", "red"), ("grass", "green")]`. + +```python demo exec +class SimpleDictIterState(rx.State): + color_chart: dict[str, str] = { + "sky": "blue", + "balloon": "red", + "grass": "green", + } + + +def display_color(color: list): + # color is presented as a list key-value pairs [("sky", "blue"), ("balloon", "red"), ("grass", "green")] + return rx.box(rx.text(color[0]), bg=color[1], padding_x="1.5em") + + +def dict_foreach(): + return rx.grid( + rx.foreach( + SimpleDictIterState.color_chart, + display_color, + ), + columns="3", + ) + +``` + +```md alert warning +# Dict Type Annotation. + +It is essential to provide the correct full type annotation for the dictionary in the state definition (e.g., `dict[str, str]` instead of `dict`) to ensure `rx.foreach` works as expected. Proper typing allows Reflex to infer and validate the structure of the data during rendering. +``` + +## Nested examples + +`rx.foreach` can be used with nested state vars. Here we use nested `foreach` components to render the nested state vars. The `rx.foreach(project["technologies"], get_badge)` inside of the `project_item` function, renders the `dict` values which are of type `list`. The `rx.box(rx.foreach(NestedStateFE.projects, project_item))` inside of the `projects_example` function renders each `dict` inside of the overall state var `projects`. + +```python demo exec +class NestedStateFE(rx.State): + projects: list[dict[str, list]] = [ + { + "technologies": ["Next.js", "Prisma", "Tailwind", "Google Cloud", "Docker", "MySQL"] + }, + { + "technologies": ["Python", "Flask", "Google Cloud", "Docker"] + } + ] + +def get_badge(technology: rx.Var[str]) -> rx.Component: + return rx.badge(technology, variant="soft", color_scheme="green") + +def project_item(project: rx.Var[dict[str, list]]) -> rx.Component: + return rx.box( + rx.hstack( + rx.foreach(project["technologies"], get_badge) + ), + ) + +def projects_example() -> rx.Component: + return rx.box(rx.foreach(NestedStateFE.projects, project_item)) +``` + +If you want an example where not all of the values in the dict are the same type then check out the example on [var operations using foreach](/docs/vars/var-operations). + +Here is a further example of how to use `foreach` with a nested data structure. + +```python demo exec +class NestedDictIterState(rx.State): + color_chart: dict[str, list[str]] = { + "purple": ["red", "blue"], + "orange": ["yellow", "red"], + "green": ["blue", "yellow"], + } + + +def display_colors(color: rx.Var[tuple[str, list[str]]]): + return rx.vstack( + rx.text(color[0], color=color[0]), + rx.hstack( + rx.foreach( + color[1], + lambda x: rx.box( + rx.text(x, color="black"), bg=x + ), + ) + ), + ) + + +def nested_dict_foreach(): + return rx.grid( + rx.foreach( + NestedDictIterState.color_chart, + display_colors, + ), + columns="3", + ) + +``` + +## Foreach with Cond + +We can also use `foreach` with the `cond` component. + +In this example we define the function `render_item`. This function takes in an `item`, uses the `cond` to check if the item `is_packed`. If it is packed it returns the `item_name` with a `✔` next to it, and if not then it just returns the `item_name`. We use the `foreach` to iterate over all of the items in the `to_do_list` using the `render_item` function. + +```python demo exec +import dataclasses + +@dataclasses.dataclass +class ToDoListItem: + item_name: str + is_packed: bool + +class ForeachCondState(rx.State): + to_do_list: list[ToDoListItem] = [ + ToDoListItem(item_name="Space suit", is_packed=True), + ToDoListItem(item_name="Helmet", is_packed=True), + ToDoListItem(item_name="Back Pack", is_packed=False), + ] + + +def render_item(item: rx.Var[ToDoListItem]): + return rx.cond( + item.is_packed, + rx.list.item(item.item_name + ' ✔'), + rx.list.item(item.item_name), + ) + +def packing_list(): + return rx.vstack( + rx.text("Sammy's Packing List"), + rx.list(rx.foreach(ForeachCondState.to_do_list, render_item)), + ) + +``` diff --git a/docs/custom-components/command-reference.md b/docs/custom-components/command-reference.md new file mode 100644 index 00000000000..989312feb64 --- /dev/null +++ b/docs/custom-components/command-reference.md @@ -0,0 +1,154 @@ + +# Command Reference + +The custom component commands are under `reflex component` subcommand. To see the list of available commands, run `reflex component --help`. To see the manual on a specific command, run `reflex component --help`, for example, `reflex component init --help`. + +```bash +reflex component --help +``` + +```text +Usage: reflex component [OPTIONS] COMMAND [ARGS]... + + Subcommands for creating and publishing Custom Components. + +Options: + --help Show this message and exit. + +Commands: + init Initialize a custom component. + build Build a custom component. + share Collect more details on the published package for gallery. +``` + +## reflex component init + +Below is an example of running the `init` command. + +```bash +reflex component init +``` + +```text +reflex component init +─────────────────────────────────────── Initializing reflex-google-auth project ─────────────────────────────────────── +Info: Populating pyproject.toml with package name: reflex-google-auth +Info: Initializing the component directory: custom_components/reflex_google_auth +Info: Creating app for testing: google_auth_demo +──────────────────────────────────────────── Initializing google_auth_demo ──────────────────────────────────────────── +[07:58:16] Initializing the app directory. console.py:85 + Initializing the web directory. console.py:85 +Success: Initialized google_auth_demo +─────────────────────────────────── Installing reflex-google-auth in editable mode. ─────────────────────────────────── +Info: Package reflex-google-auth installed! +Custom component initialized successfully! +─────────────────────────────────────────────────── Project Summary ─────────────────────────────────────────────────── +[ README.md ]: Package description. Please add usage examples. +[ pyproject.toml ]: Project configuration file. Please fill in details such as your name, email, homepage URL. +[ custom_components/ ]: Custom component code template. Start by editing it with your component implementation. +[ google_auth_demo/ ]: Demo App. Add more code to this app and test. +``` + +The `init` command uses the current enclosing folder name to construct a python package name, typically in the kebab case. For example, if running init in folder `google_auth`, the package name will be `reflex-google-auth`. The added prefix reduces the chance of name collision on PyPI (the Python Package Index), and it indicates that the package is a Reflex custom component. The user can override the package name by providing the `--package-name` option. + +The `init` command creates a set of files and folders prefilled with the package name and other details. During the init, the `custom_component` folder is installed locally in editable mode, so a developer can incrementally develop and test with ease. The changes in component implementation is automatically reflected where it is used. Below is the folder structure after the `init` command. + +```text +google_auth/ +├── pyproject.toml +├── README.md +├── custom_components/ +│ └── reflex_google_auth/ +│ ├── google_auth.py +│ └── __init__.py +└── google_auth_demo/ + └── assets/ + google_auth_demo/ + requirements.txt + rxconfig.py +``` + +### pyproject.toml + +The `pyproject.toml` is required for the package to build and be published. It is prefilled with information such as the package name, version (`0.0.1`), author name and email, homepage URL. By default the **Apache-2.0** license is used, the same as Reflex. If any of this information requires update, the user can edit the file by hand. + +### README + +The `README.md` file is created with installation instructions, e.g. `pip install reflex-google-auth`, and a brief description of the package. Typically the `README.md` contains usage examples. On PyPI, the `README.md` is rendered as part of the package page. + +### Custom Components Folder + +The `custom_components` folder is where the actual implementation is. Do not worry about this folder name: there is no need to change it. It is where `pyproject.toml` specifies the source of the python package is. The published package contains the contents inside it, excluding this folder. + +`reflex_google_auth` is the top folder for importable code. The `reflex_google_auth/__init__.py` imports everything from the `reflex_google_auth/google_auth.py`. For the user of the package, the import looks like `from reflex_google_auth import ABC, XYZ`. + +`reflex_google_auth/google_auth.py` is prefilled with code example and instructions from the [wrapping react guide](/docs/wrapping-react/overview). + +### Demo App Folder + +A demo app is generated inside `google_auth_demo` folder with import statements and example usage of the component. This is a regular Reflex app. Go into this directory and start using any reflex commands for testing. + +### Help Manual + +The help manual is shown when adding the `--help` option to the command. + +```bash +reflex component init --help +``` + +```text +Usage: reflex component init [OPTIONS] + + Initialize a custom component. + + Args: library_name: The name of the library. install: Whether to + install package from this local custom component in editable mode. + loglevel: The log level to use. + + Raises: Exit: If the pyproject.toml already exists. + +Options: + --library-name TEXT The name of your library. On PyPI, package + will be published as `reflex-{library- + name}`. + --install / --no-install Whether to install package from this local + custom component in editable mode. + [default: install] + --loglevel [debug|info|warning|error|critical] + The log level to use. [default: + LogLevel.INFO] + --help Show this message and exit. +``` + +## reflex component publish + +To publish to a package index, a user is required to already have an account with them. As of **0.7.5**, Reflex does not handle the publishing process for you. You can do so manually by first running `reflex component build` followed by `twine upload` or `uv publish` or your choice of a publishing utility. + +You can then share your build on our website with `reflex component share`. + +## reflex component build + +It is not required to run the `build` command separately before publishing. The `publish` command will build the package if it is not already built. The `build` command is provided for the user's convenience. + +The `build` command generates the `.tar.gz` and `.whl` distribution files to be uploaded to the desired package index, for example, PyPI. This command must be run at the top level of the project where the `pyproject.toml` file is. As a result of a successful build, there is a new `dist` folder with the distribution files. + +```bash +reflex component build --help +``` + +```text +Usage: reflex component build [OPTIONS] + + Build a custom component. Must be run from the project root directory where + the pyproject.toml is. + + Args: loglevel: The log level to use. + + Raises: Exit: If the build fails. + +Options: + --loglevel [debug|info|warning|error|critical] + The log level to use. [default: + LogLevel.INFO] + --help Show this message and exit. +``` diff --git a/docs/custom-components/overview.md b/docs/custom-components/overview.md new file mode 100644 index 00000000000..0c303d2c4b8 --- /dev/null +++ b/docs/custom-components/overview.md @@ -0,0 +1,72 @@ +# Custom Components Overview + +```python exec +import reflex as rx +``` + +Reflex users create many components of their own: ready to use high level components, or nicely wrapped React components. With **Custom Components**, the community can easily share these components now. + +Release **0.4.3** introduces a series of `reflex component` commands that help developers wrap react components, test, and publish them as python packages. As shown in the image below, there are already a few custom components published on PyPI, such as `reflex-spline`, `reflex-webcam`. + +Check out the custom components gallery [here](/docs/custom-components/overview). + +```python eval +rx.center( + rx.image(src="https://web.reflex-assets.dev/custom_components/pypi_reflex_custom_components.webp", width="400px", border_radius="15px", border="1px solid"), +) +``` + +## Prerequisites for Publishing + +In order to publish a Python package, an account is required with a python package index, for example, PyPI. The documentation to create accounts and generate API tokens can be found on their websites. For a quick reference, check out our [Prerequisites for Publishing](/docs/custom-components/prerequisites-for-publishing) page. + +## Steps to Publishing + +Follow these steps to publish the custom component as a python package: + +1. `reflex component init`: creates a new custom component project from templates. +2. dev and test: developer implements and tests the custom component. +3. `reflex component build`: builds the package. +4. `twine upload` or `uv publish`: uploads the package to a python package index. + +### Initialization + +```bash +reflex component init +``` + +First create a new folder for your custom component project, for example `color_picker`. The package name will be `reflex-color-picker`. The prefix `reflex-` is intentionally added for all custom components for easy search on PyPI. If you prefer a particular name for the package, you can either change it manually in the `pyproject.toml` file or add the `--library-name` option in the `reflex component init` command initially. + +Run `reflex component init`, and a set of files and folders will be created in the `color_picker` folder. The `pyproject.toml` file is the configuration file for the project. The `custom_components` folder is where the custom component implementation is. The `color_picker_demo` folder is a demo Reflex app that uses the custom component. If this is the first time of creating python packages, it is encouraged to browse through all the files (there are not that many) to understand the structure of the project. + +```bash +color_picker/ +├── pyproject.toml <- Configuration file +├── README.md +├── .gitignore <- Exclude dist/ and metadata folders +├── custom_components/ +│ └── reflex_color_picker/ <- Custom component source directory +│ ├── color_picker.py +│ └── __init__.py +└── color_picker_demo/ <- Demo Reflex app directory + └── assets/ + color_picker_demo/ + requirements.txt + rxconfig.py +``` + +### Develop and Test + +After finishing the custom component implementation, the user is encouraged to fully test it before publishing. The generated Reflex demo app `color_picker_demo` is a good place to start. It is a regular Reflex app prefilled with imports and usage of this component. During the init, the `custom_component` folder is installed locally in editable mode, so a developer can incrementally develop and test with ease. The changes in component implementation are automatically reflected in the demo app. + +### Publish + +```bash +reflex component build +``` + +Once you're ready to publish your package, run `reflex component build` to build the package. The command builds the distribution files if they are not already built. The end result is a `dist` folder containing the distribution files. The user does not need to do anything manually with these distribution files. + +In order to publish these files as a Python package, you need to use a publishing utility. Any would work, but we recommend either [Twine](https://twine.readthedocs.io/en/stable/) or (uv)[https://docs.astral.sh/uv/guides/package/#publishing-your-package]. Make sure to keep your package version in pyproject.toml updated. + +You can also share your components with the rest of the community at our website using the command `reflex component share`. See you there! diff --git a/docs/custom-components/prerequisites-for-publishing.md b/docs/custom-components/prerequisites-for-publishing.md new file mode 100644 index 00000000000..7eaea88e115 --- /dev/null +++ b/docs/custom-components/prerequisites-for-publishing.md @@ -0,0 +1,38 @@ +# Python Package Index + +```python exec +import reflex as rx +image_style = { + "width": "400px", + "border_radius": "12px", + "border": "1px solid var(--c-slate-5)", +} +``` + +In order to publish a Python package, you need to use a publishing utility. Any would work, but we recommend either [Twine](https://twine.readthedocs.io/en/stable/) or [uv](https://docs.astral.sh/uv/guides/package/#publishing-your-package). + +## PyPI + +It is straightforward to create accounts and API tokens with PyPI. There is official help on the [PyPI website](https://pypi.org/help/). For a quick reference here, go to the top right corner of the PyPI website and look for the button to register and fill out personal information. + +```python eval +rx.center( + rx.image(src="https://web.reflex-assets.dev/custom_components/pypi_register.webp", style=image_style, margin_bottom="16px", loading="lazy"), +) +``` + +A user can use username and password to authenticate with PyPI when publishing. + +```python eval +rx.center( + rx.image(src="https://web.reflex-assets.dev/custom_components/pypi_account_settings.webp", style=image_style, margin_bottom="16px", loading="lazy"), +) +``` + +Scroll down to the API tokens section and click on the "Add API token" button. Fill out the form and click "Generate API token". + +```python eval +rx.center( + rx.image(src="https://web.reflex-assets.dev/custom_components/pypi_api_tokens.webp", style=image_style, width="700px", loading="lazy"), +) +``` diff --git a/docs/database/overview.md b/docs/database/overview.md new file mode 100644 index 00000000000..830d24a6716 --- /dev/null +++ b/docs/database/overview.md @@ -0,0 +1,87 @@ +# Database + +Reflex uses [sqlmodel](https://sqlmodel.tiangolo.com) to provide a built-in ORM wrapping SQLAlchemy. + +The examples on this page refer specifically to how Reflex uses various tools to +expose an integrated database interface. Only basic use cases will be covered +below, but you can refer to the +[sqlmodel tutorial](https://sqlmodel.tiangolo.com/tutorial/select/) +for more examples and information, just replace `SQLModel` with `rx.Model` and +`Session(engine)` with `rx.session()` + +For advanced use cases, please see the +[SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/orm/quickstart.html) (v1.4). + +```md alert info +# Using NoSQL Databases + +If you are using a NoSQL database (e.g. MongoDB), you can work with it in Reflex by installing the appropriate Python client library. In this case, Reflex will not provide any ORM features. +``` + +## Connecting + +Reflex provides a built-in SQLite database for storing and retrieving data. + +You can connect to your own SQL compatible database by modifying the +`rxconfig.py` file with your database url. + +```python +config = rx.Config( + app_name="my_app", + db_url="sqlite:///reflex.db", +) +``` + +For more examples of database URLs that can be used, see the [SQLAlchemy +docs](https://docs.sqlalchemy.org/en/14/core/engines.html#backend-specific-urls). +Be sure to install the appropriate DBAPI driver for the database you intend to +use. + +## Tables + +To create a table make a class that inherits from `rx.Model` with and specify +that it is a table. + +```python +class User(rx.Model, table=True): + username: str + email: str + password: str +``` + +## Migrations + +Reflex leverages [alembic](https://alembic.sqlalchemy.org/en/latest/) +to manage database schema changes. + +Before the database feature can be used in a new app you must call `reflex db init` +to initialize alembic and create a migration script with the current schema. + +After making changes to the schema, use +`reflex db makemigrations --message 'something changed'` +to generate a script in the `alembic/versions` directory that will update the +database schema. It is recommended that generated scripts be inspected before applying them. + +Bear in mind that your newest models will not be detected by the `reflex db makemigrations` +command unless imported and used somewhere within the application. + +The `reflex db migrate` command is used to apply migration scripts to bring the +database up to date. During app startup, if Reflex detects that the current +database schema is not up to date, a warning will be displayed on the console. + +## Queries + +To query the database you can create a `rx.session()` +which handles opening and closing the database connection. + +You can use normal SQLAlchemy queries to query the database. + +```python +with rx.session() as session: + session.add(User(username="test", email="admin@reflex.dev", password="admin")) + session.commit() +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=6835&end=8225 +# Video: Tutorial of Database Model with Forms, Model Field Changes and Migrations, and adding a DateTime Field +``` diff --git a/docs/database/queries.md b/docs/database/queries.md new file mode 100644 index 00000000000..61267afb6d4 --- /dev/null +++ b/docs/database/queries.md @@ -0,0 +1,345 @@ +# Queries + +Queries are used to retrieve data from a database. + +A query is a request for information from a database table or combination of +tables. A query can be used to retrieve data from a single table or multiple +tables. A query can also be used to insert, update, or delete data from a table. + +## Session + +To execute a query you must first create a `rx.session`. You can use the session +to query the database using SQLModel or SQLAlchemy syntax. + +The `rx.session` statement will automatically close the session when the code +block is finished. **If `session.commit()` is not called, the changes will be +rolled back and not persisted to the database.** The code can also explicitly +rollback without closing the session via `session.rollback()`. + +The following example shows how to create a session and query the database. +First we create a table called `User`. + +```python +class User(rx.Model, table=True): + username: str + email: str +``` + +### Select + +Then we create a session and query the User table. + +```python +class QueryUser(rx.State): + name: str + users: list[User] + + @rx.event + def get_users(self): + with rx.session() as session: + self.users = session.exec( + User.select().where( + User.username.contains(self.name))).all() +``` + +The `get_users` method will query the database for all users that contain the +value of the state var `name`. + +### Insert + +Similarly, the `session.add()` method to add a new record to the +database or persist an existing object. + +```python +class AddUser(rx.State): + username: str + email: str + + @rx.event + def add_user(self): + with rx.session() as session: + session.add(User(username=self.username, email=self.email)) + session.commit() +``` + +### Update + +To update the user, first query the database for the object, make the desired +modifications, `.add` the object to the session and finally call `.commit()`. + +```python +class ChangeEmail(rx.State): + username: str + email: str + + @rx.event + def modify_user(self): + with rx.session() as session: + user = session.exec(User.select().where( + (User.username == self.username))).first() + user.email = self.email + session.add(user) + session.commit() +``` + +### Delete + +To delete a user, first query the database for the object, then call +`.delete()` on the session and finally call `.commit()`. + +```python +class RemoveUser(rx.State): + username: str + + @rx.event + def delete_user(self): + with rx.session() as session: + user = session.exec(User.select().where( + User.username == self.username)).first() + session.delete(user) + session.commit() +``` + +## ORM Object Lifecycle + +The objects returned by queries are bound to the session that created them, and cannot generally +be used outside that session. After adding or updating an object, not all fields are automatically +updated, so accessing certain attributes may trigger additional queries to refresh the object. + +To avoid this, the `session.refresh()` method can be used to update the object explicitly and +ensure all fields are up to date before exiting the session. + +```python +class AddUserForm(rx.State): + user: User | None = None + + @rx.event + def add_user(self, form_data: dict[str, Any]): + with rx.session() as session: + self.user = User(**form_data) + session.add(self.user) + session.commit() + session.refresh(self.user) +``` + +Now the `self.user` object will have a correct reference to the autogenerated +primary key, `id`, even though this was not provided when the object was created +from the form data. + +If `self.user` needs to be modified or used in another query in a new session, +it must be added to the session. Adding an object to a session does not +necessarily create the object, but rather associates it with a session where it +may either be created or updated accordingly. + +```python +class AddUserForm(rx.State): + ... + + @rx.event + def update_user(self, form_data: dict[str, Any]): + if self.user is None: + return + with rx.session() as session: + self.user.set(**form_data) + session.add(self.user) + session.commit() + session.refresh(self.user) +``` + +If an ORM object will be referenced and accessed outside of a session, you +should call `.refresh()` on it to avoid stale object exceptions. + +## Using SQL Directly + +Avoiding SQL is one of the main benefits of using an ORM, but sometimes it is +necessary for particularly complex queries, or when using database-specific +features. + +SQLModel exposes the `session.execute()` method that can be used to execute raw +SQL strings. If parameter binding is needed, the query may be wrapped in +[`sqlalchemy.text`](https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.text), +which allows colon-prefix names to be used as placeholders. + +```md alert info +# Never use string formatting to construct SQL queries, as this may lead to SQL injection vulnerabilities in the app. +``` + +```python +import sqlalchemy + +import reflex as rx + + +class State(rx.State): + + @rx.event + def insert_user_raw(self, username, email): + with rx.session() as session: + session.execute( + sqlalchemy.text( + "INSERT INTO user (username, email) " + "VALUES (:username, :email)" + ), + \{"username": username, "email": email}, + ) + session.commit() + + @rx.var + def raw_user_tuples(self) -> list[list]: + with rx.session() as session: + return [list(row) for row in session.execute("SELECT * FROM user").all()] +``` + +## Async Database Operations + +Reflex provides an async version of the session function called `rx.asession` for asynchronous database operations. This is useful when you need to perform database operations in an async context, such as within async event handlers. + +The `rx.asession` function returns an async SQLAlchemy session that must be used with an async context manager. Most operations against the `asession` must be awaited. + +```python +import sqlalchemy.ext.asyncio +import sqlalchemy + +import reflex as rx + + +class AsyncUserState(rx.State): + users: list[User] = [] + + @rx.event(background=True) + async def get_users_async(self): + async with rx.asession() as asession: + result = await asession.execute(User.select()) + async with self: + self.users = result.all() +``` + +### Async Select + +The following example shows how to query the database asynchronously: + +```python +class AsyncQueryUser(rx.State): + name: str + users: list[User] = [] + + @rx.event(background=True) + async def get_users(self): + async with rx.asession() as asession: + stmt = User.select().where(User.username.contains(self.name)) + result = await asession.execute(stmt) + async with self: + self.users = result.all() +``` + +### Async Insert + +To add a new record to the database asynchronously: + +```python +class AsyncAddUser(rx.State): + username: str + email: str + + @rx.event(background=True) + async def add_user(self): + async with rx.asession() as asession: + asession.add(User(username=self.username, email=self.email)) + await asession.commit() +``` + +### Async Update + +To update a user asynchronously: + +```python +class AsyncChangeEmail(rx.State): + username: str + email: str + + @rx.event(background=True) + async def modify_user(self): + async with rx.asession() as asession: + stmt = User.select().where(User.username == self.username) + result = await asession.execute(stmt) + user = result.first() + if user: + user.email = self.email + asession.add(user) + await asession.commit() +``` + +### Async Delete + +To delete a user asynchronously: + +```python +class AsyncRemoveUser(rx.State): + username: str + + @rx.event(background=True) + async def delete_user(self): + async with rx.asession() as asession: + stmt = User.select().where(User.username == self.username) + result = await asession.execute(stmt) + user = result.first() + if user: + await asession.delete(user) + await asession.commit() +``` + +### Async Refresh + +Similar to the regular session, you can refresh an object to ensure all fields are up to date: + +```python +class AsyncAddUserForm(rx.State): + user: User | None = None + + @rx.event(background=True) + async def add_user(self, form_data: dict[str, str]): + async with rx.asession() as asession: + async with self: + self.user = User(**form_data) + asession.add(self.user) + await asession.commit() + await asession.refresh(self.user) +``` + +### Async SQL Execution + +You can also execute raw SQL asynchronously: + +```python +class AsyncRawSQL(rx.State): + users: list[list] = [] + + @rx.event(background=True) + async def insert_user_raw(self, username, email): + async with rx.asession() as asession: + await asession.execute( + sqlalchemy.text( + "INSERT INTO user (username, email) " + "VALUES (:username, :email)" + ), + dict(username=username, email=email), + ) + await asession.commit() + + @rx.event(background=True) + async def get_raw_users(self): + async with rx.asession() as asession: + result = await asession.execute("SELECT * FROM user") + async with self: + self.users = [list(row) for row in result.all()] +``` + +```md alert info +# Important Notes for Async Database Operations + +- Always use the `@rx.event(background=True)` decorator for async event handlers +- Most operations against the `asession` must be awaited, including `commit()`, `execute()`, `refresh()`, and `delete()` +- The `add()` method does not need to be awaited +- Result objects from queries have methods like `all()` and `first()` that are synchronous and return data directly +- Use `async with self:` when updating state variables in background tasks +``` diff --git a/docs/database/relationships.md b/docs/database/relationships.md new file mode 100644 index 00000000000..93bfb53c5aa --- /dev/null +++ b/docs/database/relationships.md @@ -0,0 +1,164 @@ +# Relationships + +Foreign key relationships are used to link two tables together. For example, +the `Post` model may have a field, `user_id`, with a foreign key of `user.id`, +referencing a `User` model. This would allow us to automatically query the `Post` objects +associated with a user, or find the `User` object associated with a `Post`. + +To establish bidirectional relationships a model must correctly set the +`back_populates` keyword argument on the `Relationship` to the relationship +attribute in the _other_ model. + +## Foreign Key Relationships + +To create a relationship, first add a field to the model that references the +primary key of the related table, then add a `sqlmodel.Relationship` attribute +which can be used to access the related objects. + +Defining relationships like this requires the use of `sqlmodel` objects as +seen in the example. + +```python +from typing import List, Optional + +import sqlmodel + +import reflex as rx + + +class Post(rx.Model, table=True): + title: str + body: str + user_id: int = sqlmodel.Field(foreign_key="user.id") + + user: Optional["User"] = sqlmodel.Relationship(back_populates="posts") + flags: Optional[List["Flag"]] = sqlmodel.Relationship(back_populates="post") + + +class User(rx.Model, table=True): + username: str + email: str + + posts: List[Post] = sqlmodel.Relationship(back_populates="user") + flags: List["Flag"] = sqlmodel.Relationship(back_populates="user") + + +class Flag(rx.Model, table=True): + post_id: int = sqlmodel.Field(foreign_key="post.id") + user_id: int = sqlmodel.Field(foreign_key="user.id") + message: str + + post: Optional[Post] = sqlmodel.Relationship(back_populates="flags") + user: Optional[User] = sqlmodel.Relationship(back_populates="flags") +``` + +See the [SQLModel Relationship Docs](https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/define-relationships-attributes/) for more details. + +## Querying Relationships + +### Inserting Linked Objects + +The following example assumes that the flagging user is stored in the state as a +`User` instance and that the post `id` is provided in the data submitted in the +form. + +```python +class FlagPostForm(rx.State): + user: User + + @rx.event + def flag_post(self, form_data: dict[str, Any]): + with rx.session() as session: + post = session.get(Post, int(form_data.pop("post_id"))) + flag = Flag(message=form_data.pop("message"), post=post, user=self.user) + session.add(flag) + session.commit() +``` + +### How are Relationships Dereferenced? + +By default, the relationship attributes are in **lazy loading** or `"select"` +mode, which generates a query _on access_ to the relationship attribute. Lazy +loading is generally fine for single object lookups and manipulation, but can be +inefficient when accessing many linked objects for serialization purposes. + +There are several alternative loading mechanisms available that can be set on +the relationship object or when performing the query. + +- "joined" or `joinload` - generates a single query to load all related objects + at once. +- "subquery" or `subqueryload` - generates a single query to load all related + objects at once, but uses a subquery to do the join, instead of a join in the + main query. +- "selectin" or `selectinload` - emits a second (or more) SELECT statement which + assembles the primary key identifiers of the parent objects into an IN clause, + so that all members of related collections / scalar references are loaded at + once by primary key + +There are also non-loading mechanisms, "raise" and "noload" which are used to +specifically avoid loading a relationship. + +Each loading method comes with tradeoffs and some are better suited for different +data access patterns. +See [SQLAlchemy: Relationship Loading Techniques](https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html) +for more detail. + +### Querying Linked Objects + +To query the `Post` table and include all `User` and `Flag` objects up front, +the `.options` interface will be used to specify `selectinload` for the required +relationships. Using this method, the linked objects will be available for +rendering in frontend code without additional steps. + +```python +import sqlalchemy + + +class PostState(rx.State): + posts: List[Post] + + @rx.event + def load_posts(self): + with rx.session() as session: + self.posts = session.exec( + Post.select + .options( + sqlalchemy.orm.selectinload(Post.user), + sqlalchemy.orm.selectinload(Post.flags).options( + sqlalchemy.orm.selectinload(Flag.user), + ), + ) + .limit(15) + ).all() +``` + +The loading methods create new query objects and thus may be linked if the +relationship itself has other relationships that need to be loaded. In this +example, since `Flag` references `User`, the `Flag.user` relationship must be +chain loaded from the `Post.flags` relationship. + +### Specifying the Loading Mechanism on the Relationship + +Alternatively, the loading mechanism can be specified on the relationship by +passing `sa_relationship_kwargs=\{"lazy": method}` to `sqlmodel.Relationship`, +which will use the given loading mechanism in all queries by default. + +```python +from typing import List, Optional + +import sqlmodel + +import reflex as rx + + +class Post(rx.Model, table=True): + ... + user: Optional["User"] = sqlmodel.Relationship( + back_populates="posts", + sa_relationship_kwargs=\{"lazy": "selectin"}, + ) + flags: Optional[List["Flag"]] = sqlmodel.Relationship( + back_populates="post", + sa_relationship_kwargs=\{"lazy": "selectin"}, + ) +``` diff --git a/docs/database/tables.md b/docs/database/tables.md new file mode 100644 index 00000000000..4be4b164540 --- /dev/null +++ b/docs/database/tables.md @@ -0,0 +1,70 @@ +# Tables + +Tables are database objects that contain all the data in a database. + +In tables, data is logically organized in a row-and-column format similar to a +spreadsheet. Each row represents a unique record, and each column represents a +field in the record. + +## Creating a Table + +To create a table, make a class that inherits from `rx.Model`. + +The following example shows how to create a table called `User`. + +```python +class User(rx.Model, table=True): + username: str + email: str +``` + +The `table=True` argument tells Reflex to create a table in the database for +this class. + +### Primary Key + +By default, Reflex will create a primary key column called `id` for each table. + +However, if an `rx.Model` defines a different field with `primary_key=True`, then the +default `id` field will not be created. A table may also redefine `id` as needed. + +It is not currently possible to create a table without a primary key. + +## Advanced Column Types + +SQLModel automatically maps basic python types to SQLAlchemy column types, but +for more advanced use cases, it is possible to define the column type using +`sqlalchemy` directly. For example, we can add a last updated timestamp to the +post example as a proper `DateTime` field with timezone. + +```python +import datetime + +import sqlmodel +import sqlalchemy + +class Post(rx.Model, table=True): + ... + update_ts: datetime.datetime = sqlmodel.Field( + default=None, + sa_column=sqlalchemy.Column( + "update_ts", + sqlalchemy.DateTime(timezone=True), + server_default=sqlalchemy.func.now(), + ), + ) +``` + +To make the `Post` model more usable on the frontend, a `dict` method may be provided +that converts any fields to a JSON serializable value. In this case, the dict method is +overriding the default `datetime` serializer to strip off the microsecond part. + +```python +class Post(rx.Model, table=True): + ... + + def dict(self, *args, **kwargs) -> dict: + d = super().dict(*args, **kwargs) + d["update_ts"] = self.update_ts.replace(microsecond=0).isoformat() + return d +``` diff --git a/docs/de/README.md b/docs/de/README.md deleted file mode 100644 index 3ce5c6c02f8..00000000000 --- a/docs/de/README.md +++ /dev/null @@ -1,250 +0,0 @@ -
-Reflex Logo -
- -### **✨ Performante, anpassbare Web-Apps in purem Python. Bereitstellung in Sekunden. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex ist eine Bibliothek, mit der man Full-Stack-Web-Applikationen in purem Python erstellen kann. - -Wesentliche Merkmale: - -- **Pures Python** - Schreibe dein Front- und Backend in Python, es gibt also keinen Grund, JavaScript zu lernen. -- **Volle Flexibilität** - Reflex ist einfach zu handhaben, kann aber auch für komplexe Anwendungen skaliert werden. -- **Sofortige Bereitstellung** - Nach dem Erstellen kannst du deine App mit einem [einzigen Befehl](https://reflex.dev/docs/hosting/deploy-quick-start/) bereitstellen oder auf deinem eigenen Server hosten. - -Auf unserer [Architektur-Seite](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) erfahren Sie, wie Reflex unter der Haube funktioniert. - -## ⚙️ Installation - -Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 Erstelle deine erste App - -Die Installation von `reflex` installiert auch das `reflex`-Kommandozeilen-Tool. - -Teste, ob die Installation erfolgreich war, indem du ein neues Projekt erstellst. (Ersetze `my_app_name` durch deinen Projektnamen): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -Dieser Befehl initialisiert eine Vorlage in deinem neuen Verzeichnis. - -Du kannst diese App im Entwicklungsmodus ausführen: - -```bash -reflex run -``` - -Du solltest deine App unter http://localhost:3000 laufen sehen. - -Nun kannst du den Quellcode in `my_app_name/my_app_name.py` ändern. Reflex hat schnelle Aktualisierungen, sodass du deine Änderungen sofort siehst, wenn du deinen Code speicherst. - -## 🫧 Beispiel-App - -Lass uns ein Beispiel durchgehen: die Erstellung einer Benutzeroberfläche für die Bildgenerierung mit [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Zur Vereinfachung rufen wir einfach die [OpenAI-API](https://platform.openai.com/docs/api-reference/authentication) auf, aber du könntest dies auch durch ein lokal ausgeführtes ML-Modell ersetzen. - -  - -
-Eine Benutzeroberfläche für DALL·E, die im Prozess der Bildgenerierung gezeigt wird. -
- -  - -Hier ist der komplette Code, um dies zu erstellen. Das alles wird in einer Python-Datei gemacht! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """Der Zustand der App.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Hole das Bild aus dem Prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Füge Zustand und Seite zur App hinzu. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Schauen wir uns das mal genauer an. - -
-Erläuterung der Unterschiede zwischen Backend- und Frontend-Teilen der DALL-E-App. -
- -### **Reflex-UI** - -Fangen wir mit der Benutzeroberfläche an. - -```python -def index(): - return rx.center( - ... - ) -``` - -Diese `index`-Funktion definiert das Frontend der App. - -Wir verwenden verschiedene Komponenten wie `center`, `vstack`, `input` und `button`, um das Frontend zu erstellen. Komponenten können ineinander verschachtelt werden, um komplexe Layouts zu erstellen. Und du kannst Schlüsselwortargumente verwenden, um sie mit der vollen Kraft von CSS zu stylen. - -Reflex wird mit [über 60 eingebauten Komponenten](https://reflex.dev/docs/library) geliefert, die dir den Einstieg erleichtern. Wir fügen aktiv weitere Komponenten hinzu, und es ist einfach, [eigene Komponenten zu erstellen](https://reflex.dev/docs/wrapping-react/overview/). - -### **State** - -Reflex stellt deine Benutzeroberfläche als Funktion deines Zustands dar. - -```python -class State(rx.State): - """Der Zustand der App.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -Der Zustand definiert alle Variablen (genannt Vars) in einer App, die sich ändern können, und die Funktionen, die sie ändern. - -Hier besteht der Zustand aus einem `prompt` und einer `image_url`. Es gibt auch die Booleans `processing` und `complete`, um anzuzeigen, wann der Button deaktiviert werden soll (während der Bildgenerierung) und wann das resultierende Bild angezeigt werden soll. - -### **Event-Handler** - -```python -def get_image(self): - """Hole das Bild aus dem Prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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 -``` - -Innerhalb des Zustands definieren wir Funktionen, die als Event-Handler bezeichnet werden und die Zustand-Variablen ändern. Event-Handler sind die Art und Weise, wie wir den Zustand in Reflex ändern können. Sie können als Reaktion auf Benutzeraktionen aufgerufen werden, z.B. beim Klicken auf eine Schaltfläche oder bei der Eingabe in ein Textfeld. Diese Aktionen werden als Ereignisse bezeichnet. - -Unsere DALL-E.-App hat einen Event-Handler, `get_image`, der dieses Bild von der OpenAI-API abruft. Die Verwendung von `yield` in der Mitte eines Event-Handlers führt zu einer Aktualisierung der Benutzeroberfläche. Andernfalls wird die Benutzeroberfläche am Ende des Ereignishandlers aktualisiert. - -### **Routing** - -Schließlich definieren wir unsere App. - -```python -app = rx.App() -``` - -Wir fügen der Indexkomponente eine Seite aus dem Stammverzeichnis der Anwendung hinzu. Wir fügen auch einen Titel hinzu, der in der Seitenvorschau/Browser-Registerkarte angezeigt wird. - -```python -app.add_page(index, title="DALL-E") -``` - -Du kannst eine mehrseitige App erstellen, indem du weitere Seiten hinzufügst. - -## 📑 Ressourcen - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Komponentenbibliothek](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Bereitstellung](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Status - -Reflex wurde im Dezember 2022 unter dem Namen Pynecone gestartet. - -Ab 2025 wurde [Reflex Cloud](https://cloud.reflex.dev) gestartet, um die beste Hosting-Erfahrung für Reflex-Apps zu bieten. Wir werden es weiterhin entwickeln und mehr Funktionen implementieren. - -Reflex hat wöchentliche Veröffentlichungen und neue Features! Stelle sicher, dass du dieses Repository mit einem :star: Stern markierst und :eyes: beobachtest, um auf dem Laufenden zu bleiben. - -## Beitragende - -Wir begrüßen Beiträge jeder Größe! Hier sind einige gute Möglichkeiten, um in der Reflex-Community zu starten. - -- **Tritt unserem Discord bei**: Unser [Discord](https://discord.gg/T5WSbC2YtQ) ist der beste Ort, um Hilfe für dein Reflex-Projekt zu bekommen und zu besprechen, wie du beitragen kannst. -- **GitHub-Diskussionen**: Eine großartige Möglichkeit, über Funktionen zu sprechen, die du hinzugefügt haben möchtest oder Dinge, die verwirrend sind/geklärt werden müssen. -- **GitHub-Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) sind eine ausgezeichnete Möglichkeit, Bugs zu melden. Außerdem kannst du versuchen, ein bestehendes Problem zu lösen und eine PR einzureichen. - -Wir suchen aktiv nach Mitwirkenden, unabhängig von deinem Erfahrungslevel oder deiner Erfahrung. Um beizutragen, sieh dir [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) an. - -## Vielen Dank an unsere Mitwirkenden: - - - - - -## Lizenz - -Reflex ist Open-Source und lizenziert unter der [Apache License 2.0](/LICENSE). diff --git a/docs/es/README.md b/docs/es/README.md deleted file mode 100644 index 9f9ab69d628..00000000000 --- a/docs/es/README.md +++ /dev/null @@ -1,247 +0,0 @@ -
-Reflex Logo -
- -### **✨ 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) -![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) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/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.10+): - -```bash -pip install reflex -``` - -## 🥳 Crea tu primera aplicación - -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): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -Este comando inicializa una plantilla en tu nuevo directorio. - -Puedes iniciar esta aplicación en modo de desarrollo: - -```bash -reflex run -``` - -Debería ver su aplicación ejecutándose en http://localhost:3000. - -Ahora puede modificar el código fuente en `my_app_name/my_app_name.py`. Reflex se actualiza rápidamente para que pueda ver los cambios al instante cuando guarde el código. - -## 🫧 Ejemplo de una Aplicación - -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. - -  - -
-Un envoltorio frontend para DALL·E, mostrado en el proceso de generar una imagen. -
- -  - -Aquí está el código completo para crear esto. ¡Todo esto se hace en un archivo de Python! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - -class State(rx.State): - """El estado de la aplicación""" - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Obtiene la imagen desde la consulta.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Agrega el estado y la pagina a la aplicación -app = rx.App() -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). - -```python -def index(): - return rx.center( - ... - ) -``` - -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 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/wrapping-react/overview/). - -### **Estado** - -Reflex representa su UI como una función de su estado (State). - -```python -class State(rx.State): - """El estado de la aplicación""" - prompt = "" - image_url = "" - processing = False - complete = False -``` - -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 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. - -### **Manejadores de Evento** - -```python -def get_image(self): - """Obtiene la imagen desde la consulta.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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, 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 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. - -### **Enrutamiento** - -Por último, definimos nuestra app. - -```python -app = rx.App() -``` - -Añadimos una página desde la raíz (root) de la aplicación al componente de índice (index). También agregamos un título que se mostrará en la vista previa de la página/pestaña del navegador. - -```python -app.add_page(index, title="DALL-E") -``` - -Puedes crear una aplicación multipágina añadiendo más páginas. - -## 📑 Recursos - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Librería de componentes](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Despliegue](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Estado - -Reflex se lanzó en diciembre de 2022 con el nombre de Pynecone. - -A partir de 2025, [Reflex Cloud](https://cloud.reflex.dev) se ha lanzado para proporcionar la mejor experiencia de alojamiento para aplicaciones Reflex. Continuaremos desarrollándolo e implementando más características. - -¡Reflex tiene nuevas versiones y características cada semana! Asegúrate de :star: marcar como favorito y :eyes: seguir este repositorio para mantenerte actualizado. - -## 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 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. Para contribuir consulta [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## Todo Gracias A Nuestros Contribuidores: - - - - - -## Licencia - -Reflex es de código abierto y está licenciado bajo la [Apache License 2.0](/LICENSE). diff --git a/docs/events/background_events.md b/docs/events/background_events.md new file mode 100644 index 00000000000..94efaf1f573 --- /dev/null +++ b/docs/events/background_events.md @@ -0,0 +1,156 @@ +```python exec +import reflex as rx +``` + +# Background Tasks + +A background task is a special type of `EventHandler` that may run +concurrently with other `EventHandler` functions. This enables long-running +tasks to execute without blocking UI interactivity. + +A background task is defined by decorating an async `State` method with +`@rx.event(background=True)`. + +```md alert warning +# `@rx.event(background=True)` used to be called `@rx.background`. + +In Reflex version 0.6.5 and later, the `@rx.background` decorator has been renamed to `@rx.event(background=True)`. +``` + +Whenever a background task needs to interact with the state, **it must enter an +`async with self` context block** which refreshes the state and takes an +exclusive lock to prevent other tasks or event handlers from modifying it +concurrently. Because other `EventHandler` functions may modify state while the +task is running, **outside of the context block, Vars accessed by the background +task may be _stale_**. Attempting to modify the state from a background task +outside of the context block will raise an `ImmutableStateError` exception. + +In the following example, the `my_task` event handler is decorated with +`@rx.event(background=True)` and increments the `counter` variable every half second, as +long as certain conditions are met. While it is running, the UI remains +interactive and continues to process events normally. + +```md alert info +# Background events are similar to simple Task Queues like [Celery](https://www.fullstackpython.com/celery.html) allowing asynchronous events. +``` + +```python demo exec id=background_demo +import asyncio +import reflex as rx + + +class MyTaskState(rx.State): + counter: int = 0 + max_counter: int = 10 + running: bool = False + _n_tasks: int = 0 + + @rx.event + def set_max_counter(self, value: str): + self.max_counter = int(value) + + @rx.event(background=True) + async def my_task(self): + async with self: + # The latest state values are always available inside the context + if self._n_tasks > 0: + # only allow 1 concurrent task + return + + # State mutation is only allowed inside context block + self._n_tasks += 1 + + while True: + async with self: + # Check for stopping conditions inside context + if self.counter >= self.max_counter: + self.running = False + if not self.running: + self._n_tasks -= 1 + return + + self.counter += 1 + + # Await long operations outside the context to avoid blocking UI + await asyncio.sleep(0.5) + + @rx.event + def toggle_running(self): + self.running = not self.running + if self.running: + return MyTaskState.my_task + + @rx.event + def clear_counter(self): + self.counter = 0 + + +def background_task_example(): + return rx.hstack( + rx.heading(MyTaskState.counter, " /"), + rx.input( + value=MyTaskState.max_counter, + on_change=MyTaskState.set_max_counter, + width="8em", + ), + rx.button( + rx.cond(~MyTaskState.running, "Start", "Stop"), + on_click=MyTaskState.toggle_running, + ), + rx.button( + "Reset", + on_click=MyTaskState.clear_counter, + ), + ) +``` + +## Terminating Background Tasks on Page Close or Navigation + +Sometimes, background tasks may continue running even after the user navigates away from the page or closes the browser tab. To handle such cases, you can check if the websocket associated with the state is disconnected and terminate the background task when necessary. + +The solution involves checking if the client_token is still valid in the app.event_namespace.token_to_sid mapping. If the session is lost (e.g., the user navigates away or closes the page), the background task will stop. + +```python +import asyncio +import reflex as rx + +class State(rx.State): + @rx.event(background=True) + async def loop_function(self): + while True: + if self.router.session.client_token not in app.event_namespace.token_to_sid: + print("WebSocket connection closed or user navigated away. Stopping background task.") + break + + print("Running background task...") + await asyncio.sleep(2) + + +@rx.page(on_load=State.loop_function) +def index(): + return rx.text("Hello, this page will manage background tasks and stop them when the page is closed or navigated away.") + +``` + +## Task Lifecycle + +When a background task is triggered, it starts immediately, saving a reference to +the task in `app.background_tasks`. When the task completes, it is removed from +the set. + +Multiple instances of the same background task may run concurrently, and the +framework makes no attempt to avoid duplicate tasks from starting. + +It is up to the developer to ensure that duplicate tasks are not created under +the circumstances that are undesirable. In the example above, the `_n_tasks` +backend var is used to control whether `my_task` will enter the increment loop, +or exit early. + +## Background Task Limitations + +Background tasks mostly work like normal `EventHandler` methods, with certain exceptions: + +- Background tasks must be `async` functions. +- Background tasks cannot modify the state outside of an `async with self` context block. +- Background tasks may read the state outside of an `async with self` context block, but the value may be stale. +- Background tasks may not be directly called from other event handlers or background tasks. Instead use `yield` or `return` to trigger the background task. diff --git a/docs/events/chaining_events.md b/docs/events/chaining_events.md new file mode 100644 index 00000000000..1d14ff16eee --- /dev/null +++ b/docs/events/chaining_events.md @@ -0,0 +1,94 @@ +```python exec +import reflex as rx +``` + +# Chaining events + +## Calling Event Handlers From Event Handlers + +You can call other event handlers from event handlers to keep your code modular. Just use the `self.call_handler` syntax to run another event handler. As always, you can yield within your function to send incremental updates to the frontend. + +```python demo exec id=call-handler +import asyncio + +class CallHandlerState(rx.State): + count: int = 0 + progress: int = 0 + + @rx.event + async def run(self): + # Reset the count. + self.set_progress(0) + yield + + # Count to 10 while showing progress. + for i in range(10): + # Wait and increment. + await asyncio.sleep(0.5) + self.count += 1 + + # Update the progress. + self.set_progress(i + 1) + + # Yield to send the update. + yield + + +def call_handler_example(): + return rx.vstack( + rx.badge(CallHandlerState.count, font_size="1.5em", color_scheme="green"), + rx.progress(value=CallHandlerState.progress, max=10, width="100%"), + rx.button("Run", on_click=CallHandlerState.run), + ) +``` + +## Returning Events From Event Handlers + +So far, we have only seen events that are triggered by components. However, an event handler can also return events. + +In Reflex, event handlers run synchronously, so only one event handler can run at a time, and the events in the queue will be blocked until the current event handler finishes.The difference between returning an event and calling an event handler is that returning an event will send the event to the frontend and unblock the queue. + +```md alert info +# Be sure to use the class name `State` (or any substate) rather than `self` when returning events. +``` + +Try entering an integer in the input below then clicking out. + +```python demo exec id=collatz +class CollatzState(rx.State): + count: int = 1 + + @rx.event + def start_collatz(self, count: str): + """Run the collatz conjecture on the given number.""" + self.count = abs(int(count if count else 1)) + return CollatzState.run_step + + @rx.event + async def run_step(self): + """Run a single step of the collatz conjecture.""" + + while self.count > 1: + + await asyncio.sleep(0.5) + + if self.count % 2 == 0: + # If the number is even, divide by 2. + self.count //= 2 + else: + # If the number is odd, multiply by 3 and add 1. + self.count = self.count * 3 + 1 + yield + + +def collatz_example(): + return rx.vstack( + rx.badge(CollatzState.count, font_size="1.5em", color_scheme="green"), + rx.input(on_blur=CollatzState.start_collatz), + ) + +``` + +In this example, we run the [Collatz Conjecture](https://en.wikipedia.org/wiki/Collatz_conjecture) on a number entered by the user. + +When the `on_blur` event is triggered, the event handler `start_collatz` is called. It sets the initial count, then calls `run_step` which runs until the count reaches `1`. diff --git a/docs/events/decentralized_event_handlers.md b/docs/events/decentralized_event_handlers.md new file mode 100644 index 00000000000..c7a20029cd0 --- /dev/null +++ b/docs/events/decentralized_event_handlers.md @@ -0,0 +1,152 @@ +```python exec +import reflex as rx +``` + +# Decentralized Event Handlers + +## Overview + +Decentralized event handlers allow you to define event handlers outside of state classes, providing more flexible code organization. This feature was introduced in Reflex v0.7.10 and enables a more modular approach to event handling. + +With decentralized event handlers, you can: + +- Organize event handlers by feature rather than by state class +- Separate UI logic from state management +- Create more maintainable and scalable applications + +## Basic Usage + +To create a decentralized event handler, use the `@rx.event` decorator on a function that takes a state instance as its first parameter: + +```python demo exec +import reflex as rx + +class MyState(rx.State): + count: int = 0 + +@rx.event +def increment(state: MyState, amount: int): + state.count += amount + +def decentralized_event_example(): + return rx.vstack( + rx.heading(f"Count: {MyState.count}"), + rx.hstack( + rx.button("Increment by 1", on_click=increment(1)), + rx.button("Increment by 5", on_click=increment(5)), + rx.button("Increment by 10", on_click=increment(10)), + ), + spacing="4", + align="center", + ) +``` + +In this example: + +1. We define a `MyState` class with a `count` variable +2. We create a decentralized event handler `increment` that takes a `MyState` instance as its first parameter +3. We use the event handler in buttons, passing different amounts to increment by + +## Compared to Traditional Event Handlers + +Here's a comparison between traditional event handlers defined within state classes and decentralized event handlers: + +```python box +# Traditional event handler within a state class +class TraditionalState(rx.State): + count: int = 0 + + @rx.event + def increment(self, amount: int = 1): + self.count += amount + +# Usage in components +rx.button("Increment", on_click=TraditionalState.increment(5)) + +# Decentralized event handler outside the state class +class DecentralizedState(rx.State): + count: int = 0 + +@rx.event +def increment(state: DecentralizedState, amount: int = 1): + state.count += amount + +# Usage in components +rx.button("Increment", on_click=increment(5)) +``` + +Key differences: + +- Traditional event handlers use `self` to reference the state instance +- Decentralized event handlers explicitly take a state instance as the first parameter +- Both approaches use the same syntax for triggering events in components +- Both can be decorated with `@rx.event` respectively + +## Best Practices + +### When to Use Decentralized Event Handlers + +Decentralized event handlers are particularly useful in these scenarios: + +1. **Large applications** with many event handlers that benefit from better organization +2. **Feature-based organization** where you want to group related event handlers together +3. **Separation of concerns** when you want to keep state definitions clean and focused + +### Type Annotations + +Always use proper type annotations for your state parameter and any additional parameters: + +```python box +@rx.event +def update_user(state: UserState, name: str, age: int): + state.name = name + state.age = age +``` + +### Naming Conventions + +Follow these naming conventions for clarity: + +1. Use descriptive names that indicate the action being performed +2. Use the state class name as the type annotation for the first parameter +3. Name the state parameter consistently across your codebase (e.g., always use `state` or the first letter of the state class) + +### Organization + +Consider these approaches for organizing decentralized event handlers: + +1. Group related event handlers in the same file +2. Place event handlers near the state classes they modify +3. For larger applications, create a dedicated `events` directory with files organized by feature + +```python box +# Example organization in a larger application +# events/user_events.py +@rx.event +def update_user(state: UserState, name: str, age: int): + state.name = name + state.age = age + +@rx.event +def delete_user(state: UserState): + state.name = "" + state.age = 0 +``` + +### Combining with Other Event Features + +Decentralized event handlers work seamlessly with other Reflex event features: + +```python box +# Background event +@rx.event(background=True) +async def long_running_task(state: AppState): + # Long-running task implementation + pass + +# Event chaining +@rx.event +def process_form(state: FormState, data: dict): + # Process form data + return validate_data # Chain to another event +``` diff --git a/docs/events/event_actions.md b/docs/events/event_actions.md new file mode 100644 index 00000000000..6bd514799b8 --- /dev/null +++ b/docs/events/event_actions.md @@ -0,0 +1,252 @@ +```python exec +import reflex as rx +import datetime +``` + +# Event Actions + +In Reflex, an event action is a special behavior that occurs during or after +processing an event on the frontend. + +Event actions can modify how the browser handles DOM events or throttle and +debounce events before they are processed by the backend. + +An event action is specified by accessing attributes and methods present on all +EventHandlers and EventSpecs. + +## DOM Event Propagation + +_Added in v0.3.2_ + +### prevent_default + +The `.prevent_default` action prevents the default behavior of the browser for +the action. This action can be added to any existing event, or it can be used on its own by +specifying `rx.prevent_default` as an event handler. + +A common use case for this is to prevent navigation when clicking a link. + +```python demo +rx.link("This Link Does Nothing", href="https://reflex.dev/", on_click=rx.prevent_default) +``` + +```python demo exec +class LinkPreventDefaultState(rx.State): + status: bool = False + + @rx.event + def toggle_status(self): + self.status = not self.status + +def prevent_default_example(): + return rx.vstack( + rx.heading(f"The value is {LinkPreventDefaultState.status}"), + rx.link( + "Toggle Value", + href="https://reflex.dev/", + on_click=LinkPreventDefaultState.toggle_status.prevent_default, + ), + ) +``` + +### stop_propagation + +The `.stop_propagation` action stops the event from propagating to parent elements. + +This action is often used when a clickable element contains nested buttons that +should not trigger the parent element's click event. + +In the following example, the first button uses `.stop_propagation` to prevent +the click event from propagating to the outer vstack. The second button does not +use `.stop_propagation`, so the click event will also be handled by the on_click +attached to the outer vstack. + +```python demo exec +class StopPropagationState(rx.State): + where_clicked: list[str] = [] + + @rx.event + def handle_click(self, where: str): + self.where_clicked.append(where) + + @rx.event + def handle_reset(self): + self.where_clicked = [] + +def stop_propagation_example(): + return rx.vstack( + rx.button( + "btn1 - Stop Propagation", + on_click=StopPropagationState.handle_click("btn1").stop_propagation, + ), + rx.button( + "btn2 - Normal Propagation", + on_click=StopPropagationState.handle_click("btn2"), + ), + rx.foreach(StopPropagationState.where_clicked, rx.text), + rx.button( + "Reset", + on_click=StopPropagationState.handle_reset.stop_propagation, + ), + padding="2em", + border=f"1px dashed {rx.color('accent', 5)}", + on_click=StopPropagationState.handle_click("outer") + ) +``` + +## Throttling and Debounce + +_Added in v0.5.0_ + +For events that are fired frequently, it can be useful to throttle or debounce +them to avoid network latency and improve performance. These actions both take a +single argument which specifies the delay time in milliseconds. + +### throttle + +The `.throttle` action limits the number of times an event is processed within a +a given time period. It is useful for `on_scroll` and `on_mouse_move` events which are +fired very frequently, causing lag when handling them in the backend. + +```md alert warning +# Throttled events are discarded. + +There is no eventual delivery of any event that is triggered while the throttle +period is active. Throttle is not appropriate for events when the final payload +contains data that must be processed, like `on_change`. +``` + +In the following example, the `on_scroll` event is throttled to only fire every half second. + +```python demo exec +class ThrottleState(rx.State): + last_scroll: datetime.datetime | None + + @rx.event + def handle_scroll(self): + self.last_scroll = datetime.datetime.now(datetime.timezone.utc) + +def scroll_box(): + return rx.scroll_area( + rx.heading("Scroll Me"), + *[rx.text(f"Item {i}") for i in range(100)], + height="75px", + width="50%", + border=f"1px solid {rx.color('accent', 5)}", + on_scroll=ThrottleState.handle_scroll.throttle(500), + ) + +def throttle_example(): + return ( + scroll_box(), + rx.text( + "Last Scroll Event: ", + rx.moment(ThrottleState.last_scroll, format="HH:mm:ss.SSS"), + ), + ) +``` + +```md alert info +# Event Actions are Chainable + +Event actions can be chained together to create more complex event handling +behavior. For example, you can throttle an event and prevent its default +behavior in the same event handler: `on_click=MyState.handle_click.throttle(500).prevent_default`. +``` + +### debounce + +The `.debounce` action delays the processing of an event until the specified +timeout occurs. If another event is triggered during the timeout, the timer is +reset and the original event is discarded. + +Debounce is useful for handling the final result of a series of events, such as +moving a slider. + +```md alert warning +# Debounced events are discarded. + +When a new event is triggered during the debounce period, the original event is +discarded. Debounce is not appropriate for events where each payload contains +unique data that must be processed, like `on_key_down`. +``` + +In the following example, the slider's `on_change` handler, `update_value`, is +only triggered on the backend when the slider value has not changed for half a +second. + +```python demo exec +class DebounceState(rx.State): + settled_value: int = 50 + + @rx.event + def update_value(self, value: list[int | float]): + self.settled_value = value[0] + + +def debounced_slider(): + return rx.slider( + key=rx.State.router.session.session_id, + default_value=[DebounceState.settled_value], + on_change=DebounceState.update_value.debounce(500), + width="100%", + ) + +def debounce_example(): + return rx.vstack( + debounced_slider(), + rx.text(f"Settled Value: {DebounceState.settled_value}"), + ) +``` + +```md alert info +# Why set key on the slider? + +Setting `key` to the `session_id` with a dynamic `default_value` ensures that +when the page is refreshed, the component will be re-rendered to reflect the +updated default_value from the state. + +Without the `key` set, the slider would always display the original +`settled_value` after a page reload, instead of its current value. +``` + +## Temporal Events + +_Added in [v0.6.6](https://github.com/reflex-dev/reflex/releases/tag/v0.6.6)_ + +### temporal + +The `.temporal` action prevents events from being queued when the backend is down. +This is useful for non-critical events where you do not want them to pile up if there is +a temporary connection issue. + +```md alert warning +# Temporal events are discarded when the backend is down. + +When the backend is unavailable, events with the `.temporal` action will be +discarded rather than queued for later processing. Only use this for events +where it is acceptable to lose some interactions during connection issues. +``` + +In the following example, the `rx.moment` component with `interval` and `on_change` uses `.temporal` to +prevent periodic updates from being queued when the backend is down: + +```python demo exec +class TemporalState(rx.State): + current_time: str = "" + + @rx.event + def update_time(self): + self.current_time = datetime.datetime.now().strftime("%H:%M:%S") + +def temporal_example(): + return rx.vstack( + rx.heading("Current Time:"), + rx.heading(TemporalState.current_time), + rx.moment( + interval=1000, + on_change=TemporalState.update_time.temporal, + ), + rx.text("Time updates will not be queued if the backend is down."), + ) +``` diff --git a/docs/events/event_arguments.md b/docs/events/event_arguments.md new file mode 100644 index 00000000000..42621f5856e --- /dev/null +++ b/docs/events/event_arguments.md @@ -0,0 +1,123 @@ +```python exec +import reflex as rx +``` + +# Event Arguments + +The event handler signature needs to match the event trigger definition argument count. If the event handler takes two arguments, the event trigger must be able to provide two arguments. + +Here is a simple example: + +```python demo exec + +class EventArgStateSlider(rx.State): + value: int = 50 + + @rx.event + def set_end(self, value: list[int | float]): + self.value = value[0] + + +def slider_max_min_step(): + return rx.vstack( + rx.heading(EventArgStateSlider.value), + rx.slider( + default_value=40, + on_value_commit=EventArgStateSlider.set_end, + ), + width="100%", + ) + +``` + +The event trigger here is `on_value_commit` and it is called when the value changes at the end of an interaction. This event trigger passes one argument, which is the value of the slider. The event handler which is triggered by the event trigger must therefore take one argument, which is `value` here. + +Here is a form example: + +```python demo exec + +class EventArgState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def event_arg_example(): + return rx.vstack( + rx.form( + rx.vstack( + rx.input( + placeholder="Name", + name="name", + ), + rx.checkbox("Checked", name="check"), + rx.button("Submit", type="submit"), + ), + on_submit=EventArgState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.heading("Results"), + rx.text(EventArgState.form_data.to_string()), + ) +``` + +In this example the event trigger is the `on_submit` event of the form. The event handler is `handle_submit`. The `on_submit` event trigger passes one argument, the form data as a dictionary, to the `handle_submit` event handler. The `handle_submit` event handler must take one argument because the `on_submit` event trigger passes one argument. + +When the number of args accepted by an EventHandler differs from that provided by the event trigger, an `EventHandlerArgMismatch` error will be raised. + +## Pass Additional Arguments to Event Handlers + +In some use cases, you want to pass additional arguments to your event handlers. To do this you can bind an event trigger to a lambda, which can call your event handler with the arguments you want. + +Try typing a color in an input below and clicking away from it to change the color of the input. + +```python demo exec +class ArgState(rx.State): + colors: list[str] = ["rgba(245,168,152)", "MediumSeaGreen", "#DEADE3"] + + @rx.event + def change_color(self, color: str, index: int): + self.colors[index] = color + +def event_arguments_example(): + return rx.hstack( + rx.input(default_value=ArgState.colors[0], on_blur=lambda c: ArgState.change_color(c, 0), bg=ArgState.colors[0]), + rx.input(default_value=ArgState.colors[1], on_blur=lambda c: ArgState.change_color(c, 1), bg=ArgState.colors[1]), + rx.input(default_value=ArgState.colors[2], on_blur=lambda c: ArgState.change_color(c, 2), bg=ArgState.colors[2]), + ) + +``` + +In this case, in we want to pass two arguments to the event handler `change_color`, the color and the index of the color to change. + +The `on_blur` event trigger passes the text of the input as an argument to the lambda, and the lambda calls the `change_color` event handler with the text and the index of the input. + +When the number of args accepted by a lambda differs from that provided by the event trigger, an `EventFnArgMismatch` error will be raised. + +```md alert warning +# Event Handler Parameters should provide type annotations. + +Like state vars, be sure to provide the right type annotations for the parameters in an event handler. +``` + +## Events with Partial Arguments (Advanced) + +_Added in v0.5.0_ + +Event arguments in Reflex are passed positionally. Any additional arguments not +passed to an EventHandler will be filled in by the event trigger when it is +fired. + +The following two code samples are equivalent: + +```python +# Use a lambda to pass event trigger args to the EventHandler. +rx.text(on_blur=lambda v: MyState.handle_update("field1", v)) + +# Create a partial that passes event trigger args for any args not provided to the EventHandler. +rx.text(on_blur=MyState.handle_update("field1")) +``` diff --git a/docs/events/events_overview.md b/docs/events/events_overview.md new file mode 100644 index 00000000000..909beb53963 --- /dev/null +++ b/docs/events/events_overview.md @@ -0,0 +1,51 @@ +```python exec +import reflex as rx + +``` + +# Events Overview + +Events are composed of two parts: Event Triggers and Event Handlers. + +- **Events Handlers** are how the State of a Reflex application is updated. They are triggered by user interactions with the UI, such as clicking a button or hovering over an element. Events can also be triggered by the page loading or by other events. + +- **Event triggers** are component props that create an event to be sent to an event handler. + Each component supports a set of events triggers. They are described in each [component's documentation](/docs/library) in the event trigger section. + +## Example + +Lets take a look at an example below. Try mousing over the heading to change the word. + +```python demo exec +class WordCycleState(rx.State): + # The words to cycle through. + text: list[str] = ["Welcome", "to", "Reflex", "!"] + + # The index of the current word. + index: int = 0 + + @rx.event + def next_word(self): + self.index = (self.index + 1) % len(self.text) + + @rx.var + def get_text(self) -> str: + return self.text[self.index] + +def event_triggers_example(): + return rx.heading( + WordCycleState.get_text, + on_mouse_over=WordCycleState.next_word, + color="green", + ) + +``` + +In this example, the heading component has the **event trigger**, `on_mouse_over`. +Whenever the user hovers over the heading, the `next_word` **event handler** will be called to cycle the word. Once the handler returns, the UI will be updated to reflect the new state. + +Adding the `@rx.event` decorator above the event handler is strongly recommended. This decorator enables proper static type checking, which ensures event handlers receive the correct number and types of arguments. + +# What's in this section? + +In the event section of the documentation, you will explore the different types of events supported by Reflex, along with the different ways to call them. diff --git a/docs/events/page_load_events.md b/docs/events/page_load_events.md new file mode 100644 index 00000000000..3c4e9b68678 --- /dev/null +++ b/docs/events/page_load_events.md @@ -0,0 +1,40 @@ +```python exec +import reflex as rx +``` + +# Page Load Events + +You can also specify a function to run when the page loads. This can be useful for fetching data once vs on every render or state change. +In this example, we fetch data when the page loads: + +```python +class State(rx.State): + data: Dict[str, Any] + + @rx.event + def get_data(self): + # Fetch data + self.data = fetch_data() + +@rx.page(on_load=State.get_data) +def index(): + return rx.text('A Beautiful App') +``` + +Another example would be checking if the user is authenticated when the page loads. If the user is not authenticated, we redirect them to the login page. If they are authenticated, we don't do anything, letting them access the page. This `on_load` event would be placed on every page that requires authentication to access. + +```python +class State(rx.State): + authenticated: bool + + @rx.event + def check_auth(self): + # Check if user is authenticated + self.authenticated = check_auth() + if not self.authenticated: + return rx.redirect('/login') + +@rx.page(on_load=State.check_auth) +def index(): + return rx.text('A Beautiful App') +``` diff --git a/docs/events/setters.md b/docs/events/setters.md new file mode 100644 index 00000000000..243926b0e76 --- /dev/null +++ b/docs/events/setters.md @@ -0,0 +1,57 @@ +```python exec +import reflex as rx +``` + +# Setters + +Every base var has a built-in event handler to set it's value for convenience, called `set_VARNAME`. + +Say you wanted to change the value of the select component. You could write your own event handler to do this: + +```python demo exec + +options: list[str] = ["1", "2", "3", "4"] +class SetterState1(rx.State): + selected: str = "1" + + @rx.event + def change(self, value): + self.selected = value + + +def code_setter(): + return rx.vstack( + rx.badge(SetterState1.selected, color_scheme="green"), + rx.select( + options, + on_change= lambda value: SetterState1.change(value), + ) + ) + +``` + +Or you could could use a built-in setter for conciseness. + +```python demo exec + +options: list[str] = ["1", "2", "3", "4"] +class SetterState2(rx.State): + selected: str = "1" + + @rx.event + def set_selected(self, selected: str): + self.selected = selected + +def code_setter_2(): + return rx.vstack( + rx.badge(SetterState2.selected, color_scheme="green"), + rx.select( + options, + on_change= SetterState2.set_selected, + ) + ) +``` + +In this example, the setter for `selected` is `set_selected`. Both of these examples are equivalent. + +Setters are a great way to make your code more concise. But if you want to do something more complicated, you can always write your own function in the state. diff --git a/docs/events/special_events.md b/docs/events/special_events.md new file mode 100644 index 00000000000..2c8b6a9b091 --- /dev/null +++ b/docs/events/special_events.md @@ -0,0 +1,27 @@ +```python exec +import reflex as rx + +``` + +# Special Events + +Reflex also has built-in special events can be found in the [reference](/docs/api-reference/special_events). + +For example, an event handler can trigger an alert on the browser. + +```python demo exec +class SpecialEventsState(rx.State): + @rx.event + def alert(self): + return rx.window_alert("Hello World!") + +def special_events_example(): + return rx.button("Alert", on_click=SpecialEventsState.alert) +``` + +Special events can also be triggered directly in the UI by attaching them to an event trigger. + +```python +def special_events_example(): + return rx.button("Alert", on_click=rx.window_alert("Hello World!")) +``` diff --git a/docs/events/yield_events.md b/docs/events/yield_events.md new file mode 100644 index 00000000000..78bdb1c02c6 --- /dev/null +++ b/docs/events/yield_events.md @@ -0,0 +1,107 @@ +```python exec +import reflex as rx + +``` + +# Yielding Updates + +A regular event handler will send a `StateUpdate` when it has finished running. This works fine for basic event, but sometimes we need more complex logic. To update the UI multiple times in an event handler, we can `yield` when we want to send an update. + +To do so, we can use the Python keyword `yield`. For every yield inside the function, a `StateUpdate` will be sent to the frontend with the changes up to this point in the execution of the event handler. + +This example below shows how to yield 100 updates to the UI. + +```python demo exec + +class MultiUpdateState(rx.State): + count: int = 0 + + @rx.event + def timed_update(self): + for i in range(100): + self.count += 1 + yield + + +def multi_update(): + return rx.vstack( + rx.text(MultiUpdateState.count), + rx.button("Start", on_click=MultiUpdateState.timed_update) +) + +``` + +Here is another example of yielding multiple updates with a loading icon. + +```python demo exec + +import asyncio + +class ProgressExampleState(rx.State): + count: int = 0 + show_progress: bool = False + + @rx.event + async def increment(self): + self.show_progress = True + yield + # Think really hard. + await asyncio.sleep(0.5) + self.count += 1 + self.show_progress = False + +def progress_example(): + return rx.button( + ProgressExampleState.count, + on_click=ProgressExampleState.increment, + loading=ProgressExampleState.show_progress, + ) + +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=6463&end=6835 +# Video: Asyncio with Yield +``` + +## Yielding Other Events + +Events can also yield other events. This is useful when you want to chain events together. To do this, you can yield the event handler function itself. + +```md alert +# Reference other Event Handler via class + +When chaining another event handler with `yield`, access it via the state class, not `self`. +``` + +```python demo exec + +import asyncio + +class YieldEventsState(rx.State): + count: int = 0 + show_progress: bool = False + + @rx.event + async def add_five(self): + self.show_progress = True + yield + # Think really hard. + await asyncio.sleep(1) + self.count += 5 + self.show_progress = False + + @rx.event + async def increment(self): + yield YieldEventsState.add_five + yield YieldEventsState.add_five + yield YieldEventsState.add_five + + +def multiple_yield_example(): + return rx.button( + YieldEventsState.count, + on_click=YieldEventsState.increment, + loading=YieldEventsState.show_progress, + ) + +``` diff --git a/docs/getting_started/__init__.py b/docs/getting_started/__init__.py new file mode 100644 index 00000000000..0cdc5e99ec8 --- /dev/null +++ b/docs/getting_started/__init__.py @@ -0,0 +1 @@ +"""Getting started docs.""" diff --git a/docs/getting_started/basics.md b/docs/getting_started/basics.md new file mode 100644 index 00000000000..017dcf4320e --- /dev/null +++ b/docs/getting_started/basics.md @@ -0,0 +1,402 @@ +```python exec +import reflex as rx +``` + +# Reflex Basics + +This page gives an introduction to the most common concepts that you will use to build Reflex apps. + +```md section +# You will learn how to: + +- Create and nest components +- Customize and style components +- Distinguish between compile-time and runtime +- Display data that changes over time +- Respond to events and update the screen +- Render conditions and lists +- Create pages and navigate between them +``` + +[Install](/docs/getting_started/installation) `reflex` using pip. + +```bash +pip install reflex +``` + +Import the `reflex` library to get started. + +```python +import reflex as rx +``` + +## Creating and nesting components + +[Components](/docs/ui/overview) are the building blocks for your app's user interface (UI). They are the visual elements that make up your app, like buttons, text, and images. Reflex has a wide selection of [built-in components](/docs/library) to get you started quickly. + +Components are created using functions that return a component object. + +```python demo exec +def my_button(): + return rx.button("Click Me") +``` + +Components can be nested inside each other to create complex UIs. + +To nest components as children, pass them as positional arguments to the parent component. In the example below, the `rx.text` and `my_button` components are children of the `rx.box` component. + +```python demo exec +def my_page(): + return rx.box( + rx.text("This is a page"), + # Reference components defined in other functions. + my_button() + ) +``` + +You can also use any base HTML element through the [`rx.el`](/docs/library/other/html) namespace. This allows you to use standard HTML elements directly in your Reflex app when you need more control or when a specific component isn't available in the Reflex component library. + +```python demo exec +def my_div(): + return rx.el.div( + rx.el.p("Use base html!"), + ) +``` + +If you need a component not provided by Reflex, you can check the [3rd party ecosystem](/docs/custom-components) or [wrap your own React component](/docs/wrapping-react/library-and-tags). + +## Customizing and styling components + +Components can be customized using [props](/docs/components/props), which are passed in as keyword arguments to the component function. + +Each component has props that are specific to that component. Check the docs for the component you are using to see what props are available. + +```python demo exec +def half_filled_progress(): + return rx.progress(value=50) +``` + +In addition to component-specific props, components can also be styled using CSS properties passed as props. + +```python demo exec +def round_button(): + return rx.button("Click Me", border_radius="15px", font_size="18px") +``` + +```md alert +Use the `snake_case` version of the CSS property name as the prop name. +``` + +See the [styling guide](/docs/styling/overview) for more information on how to style components + +In summary, components are made up of children and props. + +```md definition +# Children + +- Text or other Reflex components nested inside a component. +- Passed as **positional arguments**. + +# Props + +- Attributes that affect the behavior and appearance of a component. +- Passed as **keyword arguments**. +``` + +## Displaying data that changes over time + +Apps need to store and display data that changes over time. Reflex handles this through [State](/docs/state/overview), which is a Python class that stores variables that can change when the app is running, as well as the functions that can change those variables. + +To define a state class, subclass `rx.State` and define fields that store the state of your app. The state variables ([vars](/docs/vars/base_vars)) should have a type annotation, and can be initialized with a default value. + +```python +class MyState(rx.State): + count: int = 0 +``` + +### Referencing state vars in components + +To reference a state var in a component, you can pass it as a child or prop. The component will automatically update when the state changes. + +Vars are referenced through class attributes on your state class. For example, to reference the `count` var in a component, use `MyState.count`. + +```python demo exec +class MyState(rx.State): + count: int = 0 + color: str = "red" + +def counter(): + return rx.hstack( + # The heading `color` prop is set to the `color` var in MyState. + rx.heading("Count: ", color=MyState.color), + # The `count` var in `MyState` is passed as a child to the heading component. + rx.heading(MyState.count), + ) +``` + +Vars can be referenced in multiple components, and will automatically update when the state changes. + +## Responding to events and updating the screen + +So far, we've defined state vars but we haven't shown how to change them. All state changes are handled through functions in the state class, called [event handlers](/docs/events/events_overview). + +```md alert +Event handlers are the ONLY way to change state in Reflex. +``` + +Components have special props, such as `on_click`, called event triggers that can be used to make components interactive. Event triggers connect components to event handlers, which update the state. + +```python demo exec +class CounterState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + +def counter_increment(): + return rx.hstack( + rx.heading(CounterState.count), + rx.button("Increment", on_click=CounterState.increment) + ) +``` + +When an event trigger is activated, the event handler is called, which updates the state. The UI is automatically re-rendered to reflect the new state. + +```md alert info +# What is the `@rx.event` decorator? + +Adding the `@rx.event` decorator above the event handler is strongly recommended. This decorator enables proper static type checking, which ensures event handlers receive the correct number and types of arguments. This was introduced in Reflex version 0.6.5. +``` + +### Event handlers with arguments + +Event handlers can also take in arguments. For example, the `increment` event handler can take an argument to increment the count by a specific amount. + +```python demo exec +class CounterState2(rx.State): + count: int = 0 + + @rx.event + def increment(self, amount: int): + self.count += amount + +def counter_variable(): + return rx.hstack( + rx.heading(CounterState2.count), + rx.button("Increment by 1", on_click=lambda: CounterState2.increment(1)), + rx.button("Increment by 5", on_click=lambda: CounterState2.increment(5)), + ) +``` + +The `on_click` event trigger doesn't pass any arguments here, but some event triggers do. For example, the `on_blur` event trigger passes the text of an input as an argument to the event handler. + +```python demo exec +class TextState(rx.State): + text: str = "" + + @rx.event + def update_text(self, new_text: str): + self.text = new_text + +def text_input(): + return rx.vstack( + rx.heading(TextState.text), + rx.input(default_value=TextState.text, on_blur=TextState.update_text), + ) +``` + +```md alert +Make sure that the event handler has the same number of arguments as the event trigger, or an error will be raised. +``` + +## Compile-time vs. runtime (IMPORTANT) + +Before we dive deeper into state, it's important to understand the difference between compile-time and runtime in Reflex. + +When you run your app, the frontend gets compiled to Javascript code that runs in the browser (compile-time). The backend stays in Python and runs on the server during the lifetime of the app (runtime). + +### When can you not use pure Python? + +We cannot compile arbitrary Python code, only the components that you define. What this means importantly is that you cannot use arbitrary Python operations and functions on state vars in components. + +However, since any event handlers in your state are on the backend, you **can use any Python code or library** within your state. + +### Examples that work + +Within an event handler, use any Python code or library. + +```python demo exec +def check_even(num: int): + return num % 2 == 0 + +class MyState3(rx.State): + count: int = 0 + text: str = "even" + + @rx.event + def increment(self): + # Use any Python code within state. + # Even reference functions defined outside the state. + if check_even(self.count): + self.text = "even" + else: + self.text = "odd" + self.count += 1 + +def count_and_check(): + return rx.box( + rx.heading(MyState3.text), + rx.button("Increment", on_click=MyState3.increment) + ) +``` + +Use any Python function within components, as long as it is defined at compile time (i.e. does not reference any state var) + +```python demo exec +def show_numbers(): + return rx.vstack( + *[ + rx.hstack(i, check_even(i)) + for i in range(10) + ] + ) +``` + +### Examples that don't work + +You cannot do an `if` statement on vars in components, since the value is not known at compile time. + +```python +class BadState(rx.State): + count: int = 0 + +def count_if_even(): + return rx.box( + rx.heading("Count: "), + # This will raise a compile error, as BadState.count is a var and not known at compile time. + rx.text(BadState.count if BadState.count % 2 == 0 else "Odd"), + # Using an if statement with a var as a prop will NOT work either. + rx.text("hello", color="red" if BadState.count % 2 == 0 else "blue"), + ) +``` + +You cannot do a `for` loop over a list of vars. + +```python +class BadState(rx.State): + items: list[str] = ["Apple", "Banana", "Cherry"] + +def loop_over_list(): + return rx.box( + # This will raise a compile error, as BadState.items is a list and not known at compile time. + *[rx.text(item) for item in BadState.items] + ) +``` + +You cannot do arbitrary Python operations on state vars in components. + +```python +class BadTextState(rx.State): + text: str = "Hello world" + +def format_text(): + return rx.box( + # Python operations such as `len` will not work on state vars. + rx.text(len(BadTextState.text)), + ) +``` + +In the next sections, we will show how to handle these cases. + +## Conditional rendering + +As mentioned above, you cannot use Python `if/else` statements with state vars in components. Instead, use the [`rx.cond`](/docs/components/conditional_rendering) function to conditionally render components. + +```python demo exec +class LoginState(rx.State): + logged_in: bool = False + + @rx.event + def toggle_login(self): + self.logged_in = not self.logged_in + +def show_login(): + return rx.box( + rx.cond( + LoginState.logged_in, + rx.heading("Logged In"), + rx.heading("Not Logged In"), + ), + rx.button("Toggle Login", on_click=LoginState.toggle_login) + ) +``` + +## Rendering lists + +To iterate over a var that is a list, use the [`rx.foreach`](/docs/components/rendering_iterables) function to render a list of components. + +Pass the list var and a function that returns a component as arguments to `rx.foreach`. + +```python demo exec +class ListState(rx.State): + items: list[str] = ["Apple", "Banana", "Cherry"] + +def render_item(item: rx.Var[str]): + """Render a single item.""" + # Note that item here is a Var, not a str! + return rx.list.item(item) + +def show_fruits(): + return rx.box( + rx.foreach(ListState.items, render_item), + ) +``` + +The function that renders each item takes in a `Var`, since this will get compiled up front. + +## Var Operations + +You can't use arbitrary Python operations on state vars in components, but Reflex has [var operations](/docs/vars/var-operations) that you can use to manipulate state vars. + +For example, to check if a var is even, you can use the `%` and `==` var operations. + +```python demo exec +class CountEvenState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + +def count_if_even(): + return rx.box( + rx.heading("Count: "), + rx.cond( + # Here we use the `%` and `==` var operations to check if the count is even. + CountEvenState.count % 2 == 0, + rx.text("Even"), + rx.text("Odd"), + ), + rx.button("Increment", on_click=CountEvenState.increment), + ) +``` + +## App and Pages + +Reflex apps are created by instantiating the `rx.App` class. Pages are linked to specific URL routes, and are created by defining a function that returns a component. + +```python +def index(): + return rx.text('Root Page') + +rx.app = rx.App() +app.add_page(index, route="/") +``` + +## Next Steps + +Now that you have a basic understanding of how Reflex works, the next step is to start coding your own apps. Try one of the following tutorials: + +- [Dashboard Tutorial](/docs/getting_started/dashboard_tutorial) +- [Chatapp Tutorial](/docs/getting_started/chatapp_tutorial) diff --git a/docs/getting_started/chat_tutorial_style.py b/docs/getting_started/chat_tutorial_style.py new file mode 100644 index 00000000000..758a089d987 --- /dev/null +++ b/docs/getting_started/chat_tutorial_style.py @@ -0,0 +1,28 @@ +"""Common styles for questions and answers.""" + +import reflex as rx + +shadow = "rgba(0, 0, 0, 0.15) 0px 2px 8px" +chat_margin = "20%" +message_style = { + "padding": "1em", + "border_radius": "5px", + "margin_y": "0.5em", + "box_shadow": shadow, + "max_width": "30em", + "display": "inline-block", +} + +# Set specific styles for questions and answers. +question_style = message_style | { + "background_color": rx.color("gray", 4), + "margin_left": chat_margin, +} +answer_style = message_style | { + "background_color": rx.color("accent", 8), + "margin_right": chat_margin, +} + +# Styles for the action bar. +input_style = {"border_width": "1px", "box_shadow": shadow, "width": "350px"} +button_style = {"background_color": rx.color("accent", 10), "box_shadow": shadow} diff --git a/docs/getting_started/chat_tutorial_utils.py b/docs/getting_started/chat_tutorial_utils.py new file mode 100644 index 00000000000..4bf4a7d1859 --- /dev/null +++ b/docs/getting_started/chat_tutorial_utils.py @@ -0,0 +1,99 @@ +"""Utility classes for the chat app tutorial.""" + +from __future__ import annotations + +import os + +import openai # pyright: ignore[reportMissingImports] + +import reflex as rx + +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") + + +class ChatappState(rx.State): + """State for the chat app tutorial.""" + + # The current question being asked. + question: str + + # Keep track of the chat history as a list of (question, answer) tuples. + chat_history: list[tuple[str, str]] + + def set_question(self, q: str): + """Set the current question.""" + self.question = q + + def set_question1(self, q: str): + """Set the current question (variant 1).""" + self.question = q + + def set_question2(self, q: str): + """Set the current question (variant 2).""" + self.question = q + + def set_question3(self, q: str): + """Set the current question (variant 3).""" + self.question = q + + def answer(self) -> None: + """Answer the question with a static response.""" + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, answer)) + + def answer2(self) -> None: + """Answer the question and clear the input.""" + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, answer)) + # Clear the question input. + self.question = "" + + async def answer3(self): + """Answer with a streaming static response.""" + import asyncio + + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, "")) + + # Clear the question input. + self.question = "" + # Yield here to clear the frontend input before continuing. + yield + + for i in range(len(answer)): + await asyncio.sleep(0.1) + self.chat_history[-1] = (self.chat_history[-1][0], answer[: i + 1]) + yield + + async def answer4(self): + """Answer using the OpenAI API with streaming.""" + # Our chatbot has some brains now! + client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY) + session = await client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": self.question}], + stop=None, + temperature=0.7, + stream=True, + ) + + # Add to the answer as the chatbot responds. + answer = "" + self.chat_history.append((self.question, answer)) + + # Clear the question input. + self.question = "" + # Yield here to clear the frontend input before continuing. + yield + + async for item in session: + if hasattr(item.choices[0].delta, "content"): + if item.choices[0].delta.content is None: + # presence of 'None' indicates the end of the response + break + answer += item.choices[0].delta.content + self.chat_history[-1] = (self.chat_history[-1][0], answer) + yield diff --git a/docs/getting_started/chatapp_tutorial.md b/docs/getting_started/chatapp_tutorial.md new file mode 100644 index 00000000000..6639456e064 --- /dev/null +++ b/docs/getting_started/chatapp_tutorial.md @@ -0,0 +1,782 @@ +```python exec +import os + +import reflex as rx +import openai + +from docs.getting_started import chat_tutorial_style as style +from docs.getting_started.chat_tutorial_utils import ChatappState + +# If it's in environment, no need to hardcode (openai SDK will pick it up) +if "OPENAI_API_KEY" not in os.environ: + openai.api_key = "YOUR_OPENAI_KEY" + +``` + +# Interactive Tutorial: AI Chat App + +This tutorial will walk you through building an AI chat app with Reflex. This app is fairly complex, but don't worry - we'll break it down into small steps. + +You can find the full source code for this app [here](https://github.com/reflex-dev/reflex-chat). + +### What You'll Learn + +In this tutorial you'll learn how to: + +1. Install `reflex` and set up your development environment. +2. Create components to define and style your UI. +3. Use state to add interactivity to your app. +4. Deploy your app to share with others. + +## Setting up Your Project + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=175&end=445 +# Video: Example of Setting up the Chat App +``` + +We will start by creating a new project and setting up our development environment. First, create a new directory for your project and navigate to it. + +```bash +~ $ mkdir chatapp +~ $ cd chatapp +``` + +Next, we will create a virtual environment for our project. This is optional, but recommended. In this example, we will use [venv](https://docs.python.org/3/library/venv.html) to create our virtual environment. + +```bash +chatapp $ python3 -m venv venv +$ source venv/bin/activate +``` + +Now, we will install Reflex and create a new project. This will create a new directory structure in our project directory. + +> **Note:** When prompted to select a template, choose option 0 for a blank project. + +```bash +chatapp $ pip install reflex +chatapp $ reflex init +────────────────────────────────── Initializing chatapp ─────────────────────────────────── +Success: Initialized chatapp +chatapp $ ls +assets chatapp rxconfig.py venv +``` + +```python eval +rx.box(height="20px") +``` + +You can run the template app to make sure everything is working. + +```bash +chatapp $ reflex run +─────────────────────────────────── Starting Reflex App ─────────────────────────────────── +Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00 +─────────────────────────────────────── App Running ─────────────────────────────────────── +App running at: http://localhost:3000 +``` + +```python eval +rx.box(height="20px") +``` + +You should see your app running at [http://localhost:3000]({"http://localhost:3000"}). + +Reflex also starts the backend server which handles all the state management and communication with the frontend. You can test the backend server is running by navigating to [http://localhost:8000/ping]({"http://localhost:8000/ping"}). + +Now that we have our project set up, in the next section we will start building our app! + +## Basic Frontend + +Let's start with defining the frontend for our chat app. In Reflex, the frontend can be broken down into independent, reusable components. See the [components docs](/docs/components/props) for more information. + +### Display A Question And Answer + +We will modify the `index` function in `chatapp/chatapp.py` file to return a component that displays a single question and answer. + +```python demo box +rx.container( + rx.box( + "What is Reflex?", + # The user's question is on the right. + text_align="right", + ), + rx.box( + "A way to build web apps in pure Python!", + # The answer is on the left. + text_align="left", + ), +) +``` + +```python +# chatapp.py + +import reflex as rx + +def index() -> rx.Component: + return rx.container( + rx.box( + "What is Reflex?", + # The user's question is on the right. + text_align="right", + ), + rx.box( + "A way to build web apps in pure Python!", + # The answer is on the left. + text_align="left", + ), + ) + + +# Add state and page to the app. +app = rx.App() +app.add_page(index) +``` + +Components can be nested inside each other to create complex layouts. Here we create a parent container that contains two boxes for the question and answer. + +We also add some basic styling to the components. Components take in keyword arguments, called [props](/docs/components/props), that modify the appearance and functionality of the component. We use the `text_align` prop to align the text to the left and right. + +### Reusing Components + +Now that we have a component that displays a single question and answer, we can reuse it to display multiple questions and answers. We will move the component to a separate function `question_answer` and call it from the `index` function. + +```python exec +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(question, text_align="right"), + rx.box(answer, text_align="left"), + margin_y="1em", + ) + + +qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ( + "What can I make with it?", + "Anything from a simple website to a complex web app!", + ), +] + + +def chat() -> rx.Component: + qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ( + "What can I make with it?", + "Anything from a simple website to a complex web app!", + ), + ] + return rx.box(*[qa(question, answer) for question, answer in qa_pairs]) +``` + +```python demo box +rx.container(chat()) +``` + +```python +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(question, text_align="right"), + rx.box(answer, text_align="left"), + margin_y="1em", + ) + + +def chat() -> rx.Component: + qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ("What can I make with it?", "Anything from a simple website to a complex web app!"), + ] + return rx.box(*[qa(question, answer) for question, answer in qa_pairs]) + + +def index() -> rx.Component: + return rx.container(chat()) +``` + +### Chat Input + +Now we want a way for the user to input a question. For this, we will use the [input](/docs/library/forms/input) component to have the user add text and a [button](/docs/library/forms/button) component to submit the question. + +```python exec +def action_bar() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question"), + rx.button("Ask"), + ) +``` + +```python demo box +rx.container( + chat(), + action_bar(), +) +``` + +```python +def action_bar() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question"), + rx.button("Ask"), + ) + +def index() -> rx.Component: + return rx.container( + chat(), + action_bar(), + ) +``` + +### Styling + +Let's add some styling to the app. More information on styling can be found in the [styling docs](/docs/styling/overview). To keep our code clean, we will move the styling to a separate file `chatapp/style.py`. + +```python +# style.py +import reflex as rx + +# Common styles for questions and answers. +shadow = "rgba(0, 0, 0, 0.15) 0px 2px 8px" +chat_margin = "20%" +message_style = dict( + padding="1em", + border_radius="5px", + margin_y="0.5em", + box_shadow=shadow, + max_width="30em", + display="inline-block", +) + +# Set specific styles for questions and answers. +question_style = message_style | dict(margin_left=chat_margin, background_color=rx.color("gray", 4)) +answer_style = message_style | dict(margin_right=chat_margin, background_color=rx.color("accent", 8)) + +# Styles for the action bar. +input_style = dict( + border_width="1px", padding="0.5em", box_shadow=shadow,width="350px" +) +button_style = dict(background_color=rx.color("accent", 10), box_shadow=shadow) +``` + +We will import the styles in `chatapp.py` and use them in the components. At this point, the app should look like this: + +```python exec +def qa4(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(rx.text(question, style=style.question_style), text_align="right"), + rx.box(rx.text(answer, style=style.answer_style), text_align="left"), + margin_y="1em", + width="100%", + ) + + +def chat4() -> rx.Component: + qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ( + "What can I make with it?", + "Anything from a simple website to a complex web app!", + ), + ] + return rx.box(*[qa4(question, answer) for question, answer in qa_pairs]) + + +def action_bar4() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question", style=style.input_style), + rx.button("Ask", style=style.button_style), + ) +``` + +```python demo box +rx.center( + rx.vstack( + chat4(), + action_bar4(), + align="center", + ) +) +``` + +```python +# chatapp.py +import reflex as rx + +from chatapp import style + + +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(rx.text(question, style=style.question_style), text_align="right"), + rx.box(rx.text(answer, style=style.answer_style), text_align="left"), + margin_y="1em", + width="100%", + ) + +def chat() -> rx.Component: + qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ("What can I make with it?", "Anything from a simple website to a complex web app!"), + ] + return rx.box(*[qa(question, answer) for question, answer in qa_pairs]) + + +def action_bar() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question", style=style.input_style), + rx.button("Ask", style=style.button_style), + ) + + +def index() -> rx.Component: + return rx.center( + rx.vstack( + chat(), + action_bar(), + align="center", + ) + ) + + +app = rx.App() +app.add_page(index) +``` + +The app is looking good, but it's not very useful yet! In the next section, we will add some functionality to the app. + +## State + +Now let’s make the chat app interactive by adding state. The state is where we define all the variables that can change in the app and all the functions that can modify them. You can learn more about state in the [state docs](/docs/state/overview). + +### Defining State + +We will create a new file called `state.py` in the `chatapp` directory. Our state will keep track of the current question being asked and the chat history. We will also define an event handler `answer` which will process the current question and add the answer to the chat history. + +```python +# state.py +import reflex as rx + + +class State(rx.State): + + # The current question being asked. + question: str + + # Keep track of the chat history as a list of (question, answer) tuples. + chat_history: list[tuple[str, str]] + + @rx.event + def answer(self): + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, answer)) + +``` + +### Binding State to Components + +Now we can import the state in `chatapp.py` and reference it in our frontend components. We will modify the `chat` component to use the state instead of the current fixed questions and answers. + +```python exec +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(rx.text(question, style=style.question_style), text_align="right"), + rx.box(rx.text(answer, style=style.answer_style), text_align="left"), + margin_y="1em", + width="100%", + ) + + +def chat1() -> rx.Component: + return rx.box( + rx.foreach( + ChatappState.chat_history, lambda messages: qa(messages[0], messages[1]) + ) + ) + + +def action_bar1() -> rx.Component: + return rx.hstack( + rx.input( + placeholder="Ask a question", + on_change=ChatappState.set_question, + style=style.input_style, + ), + rx.button("Ask", on_click=ChatappState.answer, style=style.button_style), + ) +``` + +```python demo box +rx.container( + chat1(), + action_bar1(), +) +``` + +```python +# chatapp.py +from chatapp.state import State + + +def chat() -> rx.Component: + return rx.box( + rx.foreach( + State.chat_history, + lambda messages: qa(messages[0], messages[1]) + ) + ) + + + +def action_bar() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question", on_change=State.set_question1, style=style.input_style), + rx.button("Ask", on_click=State.answer, style=style.button_style), + ) +``` + +Normal Python `for` loops don't work for iterating over state vars because these values can change and aren't known at compile time. Instead, we use the [foreach](/docs/library/dynamic-rendering/foreach) component to iterate over the chat history. + +We also bind the input's `on_change` event to the `set_question` event handler, which will update the `question` state var while the user types in the input. We bind the button's `on_click` event to the `answer` event handler, which will process the question and add the answer to the chat history. The `set_question` event handler is a built-in implicitly defined event handler. Every base var has one. Learn more in the [events docs](/docs/events/setters) under the Setters section. + +### Clearing the Input + +Currently the input doesn't clear after the user clicks the button. We can fix this by binding the value of the input to `question`, with `value=State.question`, and clear it when we run the event handler for `answer`, with `self.question = ''`. + +```python exec +def action_bar2() -> rx.Component: + return rx.hstack( + rx.input( + value=ChatappState.question, + placeholder="Ask a question", + on_change=ChatappState.set_question, + style=style.input_style, + ), + rx.button("Ask", on_click=ChatappState.answer2, style=style.button_style), + ) +``` + +```python demo box +rx.container( + chat1(), + action_bar2(), +) +``` + +```python +# chatapp.py +def action_bar() -> rx.Component: + return rx.hstack( + rx.input( + value=State.question, + placeholder="Ask a question", + on_change=State.set_question2, + style=style.input_style), + rx.button("Ask", on_click=State.answer, style=style.button_style), + ) +``` + +```python +# state.py +@rx.event +def answer(self): + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, answer)) + self.question = "" +``` + +### Streaming Text + +Normally state updates are sent to the frontend when an event handler returns. However, we want to stream the text from the chatbot as it is generated. We can do this by yielding from the event handler. See the [yield events docs](/docs/events/yield_events) for more info. + +```python exec +def action_bar3() -> rx.Component: + return rx.hstack( + rx.input( + value=ChatappState.question, + placeholder="Ask a question", + on_change=ChatappState.set_question, + style=style.input_style, + ), + rx.button("Ask", on_click=ChatappState.answer3, style=style.button_style), + ) +``` + +```python demo box +rx.container( + chat1(), + action_bar3(), +) +``` + +```python +# state.py +import asyncio + +async def answer(self): + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, "")) + + # Clear the question input. + self.question = "" + # Yield here to clear the frontend input before continuing. + yield + + for i in range(len(answer)): + # Pause to show the streaming effect. + await asyncio.sleep(0.1) + # Add one letter at a time to the output. + self.chat_history[-1] = (self.chat_history[-1][0], answer[:i + 1]) + yield +``` + +In the next section, we will finish our chatbot by adding AI! + +## Final App + +We will use OpenAI's API to give our chatbot some intelligence. + +### Configure the OpenAI API Key + +First, ensure you have an active OpenAI subscription. +Next, install the latest openai package: + +```bash +pip install --upgrade openai +``` + +Direct Configuration of API in Code + +Update the state.py file to include your API key directly: + +```python +# state.py +import os +from openai import AsyncOpenAI + +import reflex as rx + +# Initialize the OpenAI client +client = AsyncOpenAI(api_key="YOUR_OPENAI_API_KEY") # Replace with your actual API key + +``` + +### Using the API + +Making your chatbot intelligent requires connecting to a language model API. This section explains how to integrate with OpenAI's API to power your chatbot's responses. + +1. First, the user types a prompt that is updated via the `on_change` event handler. +2. Next, when a prompt is ready, the user can choose to submit it by clicking the `Ask` button which in turn triggers the `State.answer` method inside our `state.py` file. +3. Finally, if the method is triggered, the `prompt` is sent via a request to OpenAI client and returns an answer that we can trim and use to update the chat history! + +```python +# chatapp.py +def action_bar() -> rx.Component: + return rx.hstack( + rx.input( + value=State.question, + placeholder="Ask a question", + # on_change event updates the input as the user types a prompt. + on_change=State.set_question3, + style=style.input_style), + + # on_click event triggers the API to send the prompt to OpenAI. + rx.button("Ask", on_click=State.answer, style=style.button_style), + ) +``` + +```python +# state.py +import os + +from openai import AsyncOpenAI + +@rx.event +async def answer(self): + # Our chatbot has some brains now! + client = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"]) + + session = await client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + \{"role": "user", "content": self.question} + ], + stop=None, + temperature=0.7, + stream=True, + ) + + # Add to the answer as the chatbot responds. + answer = "" + self.chat_history.append((self.question, answer)) + + # Clear the question input. + self.question = "" + # Yield here to clear the frontend input before continuing. + yield + + async for item in session: + if hasattr(item.choices[0].delta, "content"): + if item.choices[0].delta.content is None: + # presence of 'None' indicates the end of the response + break + answer += item.choices[0].delta.content + self.chat_history[-1] = (self.chat_history[-1][0], answer) + yield +``` + +Finally, we have our chatbot! + +### Final Code + +This application is a simple, interactive chatbot built with Reflex that leverages OpenAI's API for intelligent responses. The chatbot features a clean interface with streaming responses for a natural conversation experience. + +Key Features + +1. Real-time streaming responses +2. Clean, visually distinct chat bubbles for questions and answers +3. Simple input interface with question field and submit button + +Project Structure + +Below is the full chatbot code with a commented title that corresponds to the filename. + +```text +chatapp/ +├── chatapp.py # UI components and app setup +├── state.py # State management and API integration +└── style.py # Styling definitions +``` + +The `chatapp.py` file: + +```python +import reflex as rx +from chatapp import style +from chatapp.state import State + +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(rx.text(question, style=style.question_style), text_align="right"), + rx.box(rx.text(answer, style=style.answer_style), text_align="left"), + margin_y="1em", + ) + +def chat() -> rx.Component: + return rx.box( + rx.foreach( + State.chat_history, + lambda messages: qa(messages[0], messages[1]), + ) + ) + +def action_bar() -> rx.Component: + return rx.hstack( + rx.input( + value=State.question, + placeholder="Ask a question", + on_change=State.set_question, + style=style.input_style, + ), + rx.button( + "Ask", + on_click=State.answer, + style=style.button_style, + ), + ) + +def index() -> rx.Component: + return rx.center( + rx.vstack( + chat(), + action_bar(), + align="center", + ) + ) + +app = rx.App() +app.add_page(index) +``` + +The `state.py` file: + +```python +import os +from openai import AsyncOpenAI +import reflex as rx + +class State(rx.State): + question: str + chat_history: list[tuple[str, str]] = [] + + async def answer(self): + client = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"]) + + # Start streaming completion from OpenAI + session = await client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + \{"role": "user", "content": self.question} + ], + temperature=0.7, + stream=True, + ) + + # Initialize response and update UI + answer = "" + self.chat_history.append((self.question, answer)) + self.question = "" + yield + + # Process streaming response + async for item in session: + if hasattr(item.choices[0].delta, "content"): + if item.choices[0].delta.content is None: + break + answer += item.choices[0].delta.content + self.chat_history[-1] = (self.chat_history[-1][0], answer) + yield +``` + +The `style.py` file: + +```python +import reflex as rx + +# Common style base +shadow = "rgba(0, 0, 0, 0.15) 0px 2px 8px" +chat_margin = "20%" +message_style = dict( + padding="1em", + border_radius="5px", + margin_y="0.5em", + box_shadow=shadow, + max_width="30em", + display="inline-block", +) + +# Styles for questions and answers +question_style = message_style | dict( + margin_left=chat_margin, + background_color=rx.color("gray", 4), +) +answer_style = message_style | dict( + margin_right=chat_margin, + background_color=rx.color("accent", 8), +) + +# Styles for input elements +input_style = dict(border_width="1px", padding="0.5em", box_shadow=shadow, width="350px") +button_style = dict(background_color=rx.color("accent", 10), box_shadow=shadow) +``` + +### Next Steps + +Congratulations! You have built your first chatbot. From here, you can read through the rest of the documentations to learn about Reflex in more detail. The best way to learn is to build something, so try to build your own app using this as a starting point! + +### One More Thing + +With our hosting service, you can deploy this app with a single command within minutes. Check out our [Hosting Quick Start](https://reflex.dev/docs/hosting/deploy-quick-start/). diff --git a/docs/getting_started/dashboard_tutorial.md b/docs/getting_started/dashboard_tutorial.md new file mode 100644 index 00000000000..4c7c0952681 --- /dev/null +++ b/docs/getting_started/dashboard_tutorial.md @@ -0,0 +1,1818 @@ +```python exec +import reflex as rx +``` + +# Tutorial: Data Dashboard + +During this tutorial you will build a small data dashboard, where you can input data and it will be rendered in table and a graph. This tutorial does not assume any existing Reflex knowledge, but we do recommend checking out the quick [Basics Guide](/docs/getting_started/basics) first. + +The techniques you’ll learn in the tutorial are fundamental to building any Reflex app, and fully understanding it will give you a deep understanding of Reflex. + +This tutorial is divided into several sections: + +- **Setup for the Tutorial**: A starting point to follow the tutorial +- **Overview**: The fundamentals of Reflex UI (components and props) +- **Showing Dynamic Data**: How to use State to render data that will change in your app. +- **Add Data to your App**: Using a Form to let a user add data to your app and introduce event handlers. +- **Plotting Data in a Graph**: How to use Reflex's graphing components. +- **Final Cleanup and Conclusion**: How to further customize your app and add some extra styling to it. + +### What are you building? + +In this tutorial, you are building an interactive data dashboard with Reflex. + +You can see what the finished app and code will look like here: + +```python exec +from collections import Counter + +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + +class State5(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + return rx.toast.info( + f"User {form_data['name']} has been added.", + position="bottom-right", + ) + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + + +def show_user5(user: User): + """Show a user in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + style={"_hover": {"bg": rx.color("gray", 3)}}, + align="center", + ) + +def add_customer_button5() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State5.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph5(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State5.users_for_graph, + width="100%", + height=250, + ) +``` + +```python eval +rx.vstack( + add_customer_button5(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State5.users, show_user5 + ), + ), + variant="surface", + size="3", + width="100%", + ), + graph5(), + align="center", + width="100%", + on_mouse_enter=State5.transform_data, + border_width="2px", + border_radius="10px", + padding="1em", + ) +``` + +```python +import reflex as rx +from collections import Counter + +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + + +def show_user(user: User): + """Show a user in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + style={ + "_hover": { + "bg": rx.color("gray", 3) + } + }, + align="center", + ) + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State.users_for_graph, + width="100%", + height=250, + ) + +def index() -> rx.Component: + return rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + width="100%", + ), + graph(), + align="center", + width="100%", + ) + + +app = rx.App( + theme=rx.theme( + radius="full", accent_color="grass" + ), +) + +app.add_page( + index, + title="Customer Data App", + description="A simple app to manage customer data.", + on_load=State.transform_data, +) +``` + +Don't worry if you don't understand the code above, in this tutorial we are going to walk you through the whole thing step by step. + +## Setup for the tutorial + +Check out the [installation docs](/docs/getting_started/installation) to get Reflex set up on your machine. Follow these to create a folder called `dashboard_tutorial`, which you will `cd` into and `pip install reflex`. + +We will choose template `0` when we run `reflex init` to get the blank template. Finally run `reflex run` to start the app and confirm everything is set up correctly. + +## Overview + +Now that you’re set up, let’s get an overview of Reflex! + +### Inspecting the starter code + +Within our `dashboard_tutorial` folder we just `cd`'d into, there is a `rxconfig.py` file that contains the configuration for our Reflex app. (Check out the [config docs](/docs/advanced_onboarding/configuration) for more information) + +There is also an `assets` folder where static files such as images and stylesheets can be placed to be referenced within your app. ([asset docs](/docs/assets/overview) for more information) + +Most importantly there is a folder also called `dashboard_tutorial` which contains all the code for your app. Inside of this folder there is a file named `dashboard_tutorial.py`. To begin this tutorial we will delete all the code in this file so that we can start from scratch and explain every step as we go. + +The first thing we need to do is import `reflex`. Once we have done this we can create a component, which is a reusable piece of user interface code. Components are used to render, manage, and update the UI elements in your application. + +Let's look at the example below. Here we have a function called `index` that returns a `text` component (an in-built Reflex UI component) that displays the text "Hello World!". + +Next we define our app using `app = rx.App()` and add the component we just defined (`index`) to a page using `app.add_page(index)`. The function name (in this example `index`) which defines the component, must be what we pass into the `add_page`. The definition of the app and adding a component to a page are required for every Reflex app. + +```python +import reflex as rx + + +def index() -> rx.Component: + return rx.text("Hello World!") + +app = rx.App() +app.add_page(index) +``` + +This code will render a page with the text "Hello World!" when you run your app like below: + +```python eval +rx.text("Hello World!", + border_width="2px", + border_radius="10px", + padding="1em" +) +``` + +```md alert info +For the rest of the tutorial the `app=rx.App()` and `app.add_page` will be implied and not shown in the code snippets. +``` + +### Creating a table + +Let's create a new component that will render a table. We will use the `table` component to do this. The `table` component has a `root`, which takes in a `header` and a `body`, which in turn take in `row` components. The `row` component takes in `cell` components which are the actual data that will be displayed in the table. + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Male"), + ), + rx.table.row( + rx.table.cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Female"), + ), + ), + border_width="2px", + border_radius="10px", + padding="1em", + ) +``` + +```python +def index() -> rx.Component: + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Male"), + ), + rx.table.row( + rx.table.cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Female"), + ), + ), + ) +``` + +Components in Reflex have `props`, which can be used to customize the component and are passed in as keyword arguments to the component function. + +The `rx.table.root` component has for example the `variant` and `size` props, which customize the table as seen below. + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Male"), + ), + rx.table.row( + rx.table.cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Female"), + ), + ), + variant="surface", + size="3", + border_width="2px", + border_radius="10px", + padding="1em", + ) +``` + +```python +def index() -> rx.Component: + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Male"), + ), + rx.table.row( + rx.table.cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Female"), + ), + ), + variant="surface", + size="3", + ) +``` + +## Showing dynamic data (State) + +Up until this point all the data we are showing in the app is static. This is not very useful for a data dashboard. We need to be able to show dynamic data that can be added to and updated. + +This is where `State` comes in. `State` is a Python class that stores variables that can change when the app is running, as well as the functions that can change those variables. + +To define a state class, subclass `rx.State` and define fields that store the state of your app. The state variables (vars) should have a type annotation, and can be initialized with a default value. Check out the [basics](/docs/getting_started/basics) section for a simple example of how state works. + +In the example below we define a `State` class called `State` that has a variable called `users` that is a list of lists of strings. Each list in the `users` list represents a user and contains their name, email and gender. + +```python +class State(rx.State): + users: list[list[str]] = [ + ["Danilo Sousa", "danilo@example.com", "Male"], + ["Zahra Ambessa", "zahra@example.com", "Female"], + ] +``` + +To iterate over a state var that is a list, we use the [`rx.foreach`](/docs/components/rendering_iterables) function to render a list of components. The `rx.foreach` component takes an `iterable` (list, tuple or dict) and a `function` that renders each item in the `iterable`. + +```md alert info +# Why can we not just splat this in a `for` loop + +You might be wondering why a `foreach` is even needed to render this state variable and why we cannot just splat a `for` loop. Check out this [documentation]() to learn why. +``` + +Here the render function is `show_user` which takes in a single user and returns a `table.row` component that displays the users name, email and gender. + +```python exec +class State1(rx.State): + users: list[list[str]] = [ + ["Danilo Sousa", "danilo@example.com", "Male"], + ["Zahra Ambessa", "zahra@example.com", "Female"], + ] + +def show_user1(person: list): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(person[0]), + rx.table.cell(person[1]), + rx.table.cell(person[2]), + ) +``` + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State1.users, show_user1 + ), + ), + variant="surface", + size="3", + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +class State(rx.State): + users: list[list[str]] = [ + ["Danilo Sousa", "danilo@example.com", "Male"], + ["Zahra Ambessa", "zahra@example.com", "Female"], + ] + +def show_user(person: list): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(person[0]), + rx.table.cell(person[1]), + rx.table.cell(person[2]), + ) + +def index() -> rx.Component: + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", +) +``` + +As you can see the output above looks the same as before, except now the data is no longer static and can change with user input to the app. + +### Using a proper class structure for our data + +So far our data has been defined in a list of lists, where the data is accessed by index i.e. `user[0]`, `user[1]`. This is not very maintainable as our app gets bigger. + +A better way to structure our data in Reflex is to use a class to represent a user. This way we can access the data using attributes i.e. `user.name`, `user.email`. + +In Reflex when we create these classes to showcase our data, the class must inherit from `rx.Base`. + +`rx.Base` is also necessary if we want to have a state var that is an iterable with different types. For example if we wanted to have `age` as an `int` we would have to use `rx.base` as we could not do this with a state var defined as `list[list[str]]`. + +The `show_user` render function is also updated to access the data by named attributes, instead of indexing. + +```python exec +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State2(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + +def show_user2(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) +``` + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State2.users, show_user2 + ), + ), + variant="surface", + size="3", + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def index() -> rx.Component: + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", +) +``` + +Next let's add a form to the app so we can add new users to the table. + +## Using a Form to Add Data + +We build a form using `rx.form`, which takes several components such as `rx.input` and `rx.select`, which represent the form fields that allow you to add information to submit with the form. Check out the [form](/docs/library/forms/form) docs for more information on form components. + +The `rx.input` component takes in several props. The `placeholder` prop is the text that is displayed in the input field when it is empty. The `name` prop is the name of the input field, which gets passed through in the dictionary when the form is submitted. The `required` prop is a boolean that determines if the input field is required. + +The `rx.select` component takes in a list of options that are displayed in the dropdown. The other props used here are identical to the `rx.input` component. + +```python demo +rx.form( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), +) +``` + +This form is all very compact as you can see from the example, so we need to add some styling to make it look better. We can do this by adding a `vstack` component around the form fields. The `vstack` component stacks the form fields vertically. Check out the [layout](/docs/styling/layout) docs for more information on how to layout your app. + +```python demo +rx.form( + rx.vstack( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + ), +) +``` + +Now you have probably realised that we have all the form fields, but we have no way to submit the form. We can add a submit button to the form by adding a `rx.button` component to the `vstack` component. The `rx.button` component takes in the text that is displayed on the button and the `type` prop which is the type of button. The `type` prop is set to `submit` so that the form is submitted when the button is clicked. + +In addition to this we need a way to update the `users` state variable when the form is submitted. All state changes are handled through functions in the state class, called [event handlers](/docs/events/events_overview). + +Components have special props called event triggers, such as `on_submit`, that can be used to make components interactive. Event triggers connect components to event handlers, which update the state. Different event triggers expect the event handler that you hook them up to, to take in different arguments (and some do not take in any arguments). + +The `on_submit` event trigger of `rx.form` is hooked up to the `add_user` event handler that is defined in the `State` class. This event trigger expects to pass a `dict`, containing the form data, to the event handler that it is hooked up to. The `add_user` event handler takes in the form data as a dictionary and appends it to the `users` state variable. + +```python +class State(rx.State): + + ... + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + + +def form(): + return rx.form( + rx.vstack( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.button("Submit", type="submit"), + ), + on_submit=State.add_user, + reset_on_submit=True, + ) +``` + +Finally we must add the new `form()` component we have defined to the `index()` function so that the form is rendered on the page. + +Below is the full code for the app so far. If you try this form out you will see that you can add new users to the table by filling out the form and clicking the submit button. The form data will also appear as a toast (a small window in the corner of the page) on the screen when submitted. + +```python exec +class State3(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + + + return rx.toast.info( + f"User has been added: {form_data}.", + position="bottom-right", + ) + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def form(): + return rx.form( + rx.vstack( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.button("Submit", type="submit"), + ), + on_submit=State3.add_user, + reset_on_submit=True, + ) +``` + +```python eval +rx.vstack( + form(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State3.users, show_user + ), + ), + variant="surface", + size="3", + ), + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def form(): + return rx.form( + rx.vstack( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.button("Submit", type="submit"), + ), + on_submit=State.add_user, + reset_on_submit=True, + ) + +def index() -> rx.Component: + return rx.vstack( + form(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + ), + ) +``` + +### Putting the Form in an Overlay + +In Reflex, we like to make the user interaction as intuitive as possible. Placing the form we just constructed in an overlay creates a focused interaction by dimming the background, and ensures a cleaner layout when you have multiple action points such as editing and deleting as well. + +We will place the form inside of a `rx.dialog` component (also called a modal). The `rx.dialog.root` contains all the parts of a dialog, and the `rx.dialog.trigger` wraps the control that will open the dialog. In our case the trigger will be an `rx.button` that says "Add User" as shown below. + +```python +rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), +) +``` + +After the trigger we have the `rx.dialog.content` which contains everything within our dialog, including a title, a description and our form. The first way to close the dialog is without submitting the form and the second way is to close the dialog by submitting the form as shown below. This requires two `rx.dialog.close` components within the dialog. + +```python +rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), +), +rx.dialog.close( + rx.button( + "Submit", type="submit" + ), +) +``` + +The total code for the dialog with the form in it is below. + +```python demo +rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + # flex is similar to vstack and used to layout the form fields + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State3.add_user, + reset_on_submit=False, + ), + # max_width is used to limit the width of the dialog + max_width="450px", + ), +) +``` + +At this point we have an app that allows you to add users to a table by filling out a form. The form is placed in a dialog that can be opened by clicking the "Add User" button. We change the name of the component from `form` to `add_customer_button` and update this in our `index` component. The full app so far and code are below. + +```python exec +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State3.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) +``` + +```python eval +rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State3.users, show_user + ), + ), + variant="surface", + size="3", + ), + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + + + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def index() -> rx.Component: + return rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + ), + ) +``` + +## Plotting Data in a Graph + +The last part of this tutorial is to plot the user data in a graph. We will use Reflex's built-in graphing library recharts to plot the number of users of each gender. + +### Transforming the data for the graph + +The graphing components in Reflex expect to take in a list of dictionaries. Each dictionary represents a data point on the graph and contains the x and y values. We will create a new event handler in the state called `transform_data` to transform the user data into the format that the graphing components expect. We must also create a new state variable called `users_for_graph` to store the transformed data, which will be used to render the graph. + +```python +from collections import Counter + +class State(rx.State): + users: list[User] = [] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] +``` + +As we can see above the `transform_data` event handler uses the `Counter` class from the `collections` module to count the number of users of each gender. We then create a list of dictionaries from this which we set to the state var `users_for_graph`. + +Finally we can see that whenever we add a new user through submitting the form and running the `add_user` event handler, we call the `transform_data` event handler to update the `users_for_graph` state variable. + +### Rendering the graph + +We use the `rx.recharts.bar_chart` component to render the graph. We pass through the state variable for our graphing data as `data=State.users_for_graph`. We also pass in a `rx.recharts.bar` component which represents the bars on the graph. The `rx.recharts.bar` component takes in the `data_key` prop which is the key in the data dictionary that represents the y value of the bar. The `stroke` and `fill` props are used to set the color of the bars. + +The `rx.recharts.bar_chart` component also takes in `rx.recharts.x_axis` and `rx.recharts.y_axis` components which represent the x and y axes of the graph. The `data_key` prop of the `rx.recharts.x_axis` component is set to the key in the data dictionary that represents the x value of the bar. Finally we add `width` and `height` props to set the size of the graph. + +```python +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State.users_for_graph, + width="100%", + height=250, + ) +``` + +Finally we add this `graph()` component to our `index()` component so that the graph is rendered on the page. The code for the full app with the graph included is below. If you try this out you will see that the graph updates whenever you add a new user to the table. + +```python exec +from collections import Counter + +class State4(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + return rx.toast.info( + f"User {form_data['name']} has been added.", + position="bottom-right", + ) + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State4.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State4.users_for_graph, + width="100%", + height=250, + ) +``` + +```python eval +rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State4.users, show_user + ), + ), + variant="surface", + size="3", + ), + graph(), + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +from collections import Counter + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State.users_for_graph, + width="100%", + height=250, + ) + +def index() -> rx.Component: + return rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + ), + graph(), + ) +``` + +One thing you may have noticed about your app is that the graph does not appear initially when you run the app, and that you must add a user to the table for it to first appear. This occurs because the `transform_data` event handler is only called when a user is added to the table. In the next section we will explore a solution to this. + +## Final Cleanup + +### Revisiting app.add_page + +At the beginning of this tutorial we mentioned that the `app.add_page` function is required for every Reflex app. This function is used to add a component to a page. + +The `app.add_page` currently looks like this `app.add_page(index)`. We could change the route that the page renders on by setting the `route` prop such as `route="/custom-route"`, this would change the route to `http://localhost:3000/custom-route` for this page. + +We can also set a `title` to be shown in the browser tab and a `description` as shown in search results. + +To solve the problem we had above about our graph not loading when the page loads, we can use `on_load` inside of `app.add_page` to call the `transform_data` event handler when the page loads. This would look like `on_load=State.transform_data`. Below see what our `app.add_page` would look like with some of the changes above added. + +```python eval +rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State4.users, show_user + ), + ), + variant="surface", + size="3", + ), + graph(), + on_mouse_enter=State4.transform_data, + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +app.add_page( + index, + title="Customer Data App", + description="A simple app to manage customer data.", + on_load=State.transform_data, +) +``` + +### Revisiting app=rx.App() + +At the beginning of the tutorial we also mentioned that we defined our app using `app=rx.App()`. We can also pass in some props to the `rx.App` component to customize the app. + +The most important one is `theme` which allows you to customize the look and feel of the app. The `theme` prop takes in an `rx.theme` component which has several props that can be set. + +The `radius` prop sets the global radius value for the app that is inherited by all components that have a `radius` prop. It can be overwritten locally for a specific component by manually setting the `radius` prop. + +The `accent_color` prop sets the accent color of the app. Check out other options for the accent color [here](/docs/library/other/theme). + +To see other props that can be set at the app level check out this [documentation](/docs/styling/theming) + +```python +app = rx.App( + theme=rx.theme( + radius="full", accent_color="grass" + ), +) +``` + +Unfortunately in this tutorial here we cannot actually apply this to the live example on the page, but if you copy and paste the code below into a reflex app locally you can see it in action. + +## Conclusion + +Finally let's make some final styling updates to our app. We will add some hover styling to the table rows and center the table inside the `show_user` with `style=\{"_hover": \{"bg": rx.color("gray", 3)}}, align="center"`. + +In addition, we will add some `width="100%"` and `align="center"` to the `index()` component to center the items on the page and ensure they stretch the full width of the page. + +Check out the full code and interactive app below: + +```python eval +rx.vstack( + add_customer_button5(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State5.users, show_user5 + ), + ), + variant="surface", + size="3", + width="100%", + ), + graph5(), + align="center", + width="100%", + on_mouse_enter=State5.transform_data, + border_width="2px", + border_radius="10px", + padding="1em", + ) +``` + +```python +import reflex as rx +from collections import Counter + +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + + +def show_user(user: User): + """Show a user in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + style={ + "_hover": { + "bg": rx.color("gray", 3) + } + }, + align="center", + ) + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State.users_for_graph, + width="100%", + height=250, + ) + +def index() -> rx.Component: + return rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + width="100%", + ), + graph(), + align="center", + width="100%", + ) + + +app = rx.App( + theme=rx.theme( + radius="full", accent_color="grass" + ), +) + +app.add_page( + index, + title="Customer Data App", + description="A simple app to manage customer data.", + on_load=State.transform_data, +) +``` + +And that is it for your first dashboard tutorial. In this tutorial we have created + +- a table to display user data +- a form to add new users to the table +- a dialog to showcase the form +- a graph to visualize the user data + +In addition to the above we have we have + +- explored state to allow you to show dynamic data that changes over time +- explored events to allow you to make your app interactive and respond to user actions +- added styling to the app to make it look better + +## Advanced Section (Hooking this up to a Database) + +Coming Soon! diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md new file mode 100644 index 00000000000..be51070f335 --- /dev/null +++ b/docs/getting_started/installation.md @@ -0,0 +1,166 @@ +```python exec +import reflex as rx +app_name = "my_app_name" +default_url = "http://localhost:3000" +``` + +# Installation + +Reflex requires Python 3.10+. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=758&end=1206 +# Video: Installation +``` + +## Virtual Environment + +We **highly recommend** creating a virtual environment for your project. + +[uv](https://docs.astral.sh/uv/) is the recommended modern option. [venv](https://docs.python.org/3/library/venv.html), [conda](https://conda.io/) and [poetry](https://python-poetry.org/) are some alternatives. + +# Install Reflex on your system + +---md tabs + +--tab macOS/Linux + +## Install on macOS/Linux + +We will go with [uv](https://docs.astral.sh/uv/) here. + +### Prerequisites + +#### Install uv + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +After installation, restart your terminal or run `source ~/.bashrc` (or `source ~/.zshrc` for zsh). + +Alternatively, install via [Homebrew, PyPI, or other methods](https://docs.astral.sh/uv/getting-started/installation/). + +**macOS (Apple Silicon) users:** Install [Rosetta 2](https://support.apple.com/en-us/HT211861). Run this command: + +`/usr/sbin/softwareupdate --install-rosetta --agree-to-license` + +### Create the project directory + +Replace `{app_name}` with your project name. Switch to the new directory. + +```bash +mkdir {app_name} +cd {app_name} +``` + +### Initialize uv project + +```bash +uv init +``` + +### Add Reflex to the project + +```bash +uv add reflex +``` + +### Initialize the Reflex project + +```bash +uv run reflex init +``` + +-- +--tab Windows + +## Install on Windows + +For Windows users, we recommend using [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/about) for optimal performance. + +**WSL users:** Refer to the macOS/Linux instructions above. + +For the rest of this section we will work with native Windows (non-WSL). + +We will go with [uv](https://docs.astral.sh/uv/) here. + +### Prerequisites + +#### Install uv + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +After installation, restart your terminal (PowerShell or Command Prompt). + +Alternatively, install via [WinGet, Scoop, or other methods](https://docs.astral.sh/uv/getting-started/installation/). + +### Create the project directory + +Replace `{app_name}` with your project name. Switch to the new directory. + +```bash +mkdir {app_name} +cd {app_name} +``` + +### Initialize uv project + +```bash +uv init +``` + +### Add Reflex to the project + +```bash +uv add reflex +``` + +### Initialize the Reflex project + +```bash +uv run reflex init +``` + +```md alert warning +# Error `Install Failed - You are missing a DLL required to run bun.exe` Windows + +Bun requires runtime components of Visual C++ libraries to run on Windows. This issue is fixed by installing [Microsoft Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=53840). +``` + +-- + +--- + +Running `uv run reflex init` will return the option to start with a blank Reflex app, premade templates built by the Reflex team, or to try our [AI builder](https://build.reflex.dev/). + +```bash +Initializing the web directory. + +Get started with a template: +(0) A blank Reflex app. +(1) Premade templates built by the Reflex team. +(2) Try our AI builder. +Which template would you like to use? (0): +``` + +From here select an option. + +## Run the App + +Run it in development mode: + +```bash +uv run reflex run +``` + +Your app runs at [http://localhost:3000](http://localhost:3000). + +Reflex prints logs to the terminal. To increase log verbosity to help with debugging, use the `--loglevel` flag: + +```bash +uv run reflex run --loglevel debug +``` + +Reflex will _hot reload_ any code changes in real time when running in development mode. Your code edits will show up on [http://localhost:3000](http://localhost:3000) automatically. diff --git a/docs/getting_started/introduction.md b/docs/getting_started/introduction.md new file mode 100644 index 00000000000..7ac5d59e717 --- /dev/null +++ b/docs/getting_started/introduction.md @@ -0,0 +1,341 @@ +```python exec +import reflex as rx +``` + + + +# Introduction + +**Reflex** is an open-source framework for quickly building beautiful, interactive web applications in **pure Python**. + +## Goals + +```md section +### Pure Python + +Use Python for everything. Don't worry about learning a new language. + +### Easy to Learn + +Build and share your first app in minutes. No web development experience required. + +### Full Flexibility + +Remain as flexible as traditional web frameworks. Reflex is easy to use, yet allows for advanced use cases. + +Build anything from small data science apps to large, multi-page websites. **This entire site was built and deployed with Reflex!** + +### Batteries Included + +No need to reach for a bunch of different tools. Reflex handles the user interface, server-side logic, and deployment of your app. +``` + +## An example: Make it count + +Here, we go over a simple counter app that lets the user count up or down. + +```python exec +class CounterExampleState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def decrement(self): + self.count -= 1 + +class IntroTabsState(rx.State): + """The app state.""" + + value: str = "tab1" + tab_selected: str = "" + + @rx.event + def change_value(self, val: str): + self.tab_selected = f"{val} clicked!" + self.value = val + +def tabs(): + return rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger( + "Frontend", value="tab1", + class_name="tab-style" + ), + rx.tabs.trigger( + "Backend", value="tab2", + class_name="tab-style" + ), + rx.tabs.trigger( + "Page", value="tab3", + class_name="tab-style" + ), + ), + rx.tabs.content( + rx.markdown( + """The frontend is built declaratively using Reflex components. Components are compiled down to JS and served to the users browser, therefore: + +- Only use Reflex components, vars, and var operations when building your UI. Any other logic should be put in your `State` (backend). + +- Use `rx.cond` and `rx.foreach` (replaces if statements and for loops), for creating dynamic UIs. + """, + ), + value="tab1", + class_name="pt-4" + ), + rx.tabs.content( + rx.markdown( + """Write your backend in the `State` class. Here you can define functions and variables that can be referenced in the frontend. This code runs directly on the server and is not compiled, so there are no special caveats. Here you can use any Python external library and call any method/function. + """, + ), + value="tab2", + class_name="pt-4" + ), + rx.tabs.content( + rx.markdown( + """Each page is a Python function that returns a Reflex component. You can define multiple pages and navigate between them, see the [Routing](/docs/pages/overview) section for more information. + +- Start with a single page and scale to 100s of pages. + """, + ), + value="tab3", + class_name="pt-4" + ), + class_name="text-slate-12 font-normal", + default_value="tab1", + value=IntroTabsState.value, + on_change=lambda x: IntroTabsState.change_value( + x + ), + ) +``` + +```python demo box id=counter +rx.hstack( + rx.button( + "Decrement", + color_scheme="ruby", + on_click=CounterExampleState.decrement, + ), + rx.heading(CounterExampleState.count, font_size="2em"), + rx.button( + "Increment", + color_scheme="grass", + on_click=CounterExampleState.increment, + ), + spacing="4", +) +``` + +Here is the full code for this example: + +```python eval +tabs() +``` + +```python demo box +rx.box( + rx._x.code_block( + """import reflex as rx """, + class_name="code-block !bg-transparent !border-none", + ), + rx._x.code_block( + """class State(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def decrement(self): + self.count -= 1""", + background=rx.cond( + IntroTabsState.value == "tab2", + "var(--c-violet-3) !important", + "transparent", + ), + border=rx.cond( + IntroTabsState.value == "tab2", + "1px solid var(--c-violet-5)", + "none !important" + ), + class_name="code-block", + ), + rx._x.code_block( + """def index(): + return rx.hstack( + rx.button( + "Decrement", + color_scheme="ruby", + on_click=State.decrement, + ), + rx.heading(State.count, font_size="2em"), + rx.button( + "Increment", + color_scheme="grass", + on_click=State.increment, + ), + spacing="4", + )""", + border=rx.cond( + IntroTabsState.value == "tab1", + "1px solid var(--c-violet-5)", + "none !important", + ), + background=rx.cond( + IntroTabsState.value == "tab1", + "var(--c-violet-3) !important", + "transparent", + ), + class_name="code-block", + ), + rx._x.code_block( + """app = rx.App() +app.add_page(index)""", + background=rx.cond( + IntroTabsState.value == "tab3", + "var(--c-violet-3) !important", + "transparent", + ), + border=rx.cond( + IntroTabsState.value == "tab3", + "1px solid var(--c-violet-5)", + "none !important", + ), + class_name="code-block", + ), + class_name="w-full flex flex-col", +) +``` + +## The Structure of a Reflex App + +Let's break this example down. + +### Import + +```python +import reflex as rx +``` + +We begin by importing the `reflex` package (aliased to `rx`). We reference Reflex objects as `rx.*` by convention. + +### State + +```python +class State(rx.State): + count: int = 0 +``` + +The state defines all the variables (called **[vars](/docs/vars/base_vars)**) in an app that can change, as well as the functions (called **[event_handlers](#event-handlers)**) that change them. + +Here our state has a single var, `count`, which holds the current value of the counter. We initialize it to `0`. + +### Event Handlers + +```python +@rx.event +def increment(self): + self.count += 1 + +@rx.event +def decrement(self): + self.count -= 1 +``` + +Within the state, we define functions, called **event handlers**, that change the state vars. + +Event handlers are the only way that we can modify the state in Reflex. +They can be called in response to user actions, such as clicking a button or typing in a text box. +These actions are called **events**. + +Our counter app has two event handlers, `increment` and `decrement`. + +### User Interface (UI) + +```python +def index(): + return rx.hstack( + rx.button( + "Decrement", + color_scheme="ruby", + on_click=State.decrement, + ), + rx.heading(State.count, font_size="2em"), + rx.button( + "Increment", + color_scheme="grass", + on_click=State.increment, + ), + spacing="4", + ) +``` + +This function defines the app's user interface. + +We use different components such as `rx.hstack`, `rx.button`, and `rx.heading` to build the frontend. Components can be nested to create complex layouts, and can be styled using the full power of CSS or [Tailwind CSS](/docs/styling/tailwind). + +Reflex comes with [50+ built-in components](/docs/library) to help you get started. +We are actively adding more components. Also, it's easy to [wrap your own React components](/docs/wrapping-react/overview). + +```python +rx.heading(State.count, font_size="2em"), +``` + +Components can reference the app's state vars. +The `rx.heading` component displays the current value of the counter by referencing `State.count`. +All components that reference state will reactively update whenever the state changes. + +```python +rx.button( + "Decrement", + color_scheme="ruby", + on_click=State.decrement, +), +``` + +Components interact with the state by binding events triggers to event handlers. +For example, `on_click` is an event that is triggered when a user clicks a component. + +The first button in our app binds its `on_click` event to the `State.decrement` event handler. Similarly the second button binds `on_click` to `State.increment`. + +In other words, the sequence goes like this: + +- User clicks "increment" on the UI. +- `on_click` event is triggered. +- Event handler `State.increment` is called. +- `State.count` is incremented. +- UI updates to reflect the new value of `State.count`. + +### Add pages + +Next we define our app and add the counter component to the base route. + +```python +app = rx.App() +app.add_page(index) +``` + +## Next Steps + +🎉 And that's it! + +We've created a simple, yet fully interactive web app in pure Python. + +By continuing with our documentation, you will learn how to build awesome apps with Reflex. Use the sidebar to navigate through the sections, or search (`Ctrl+K` or `Cmd+K`) to quickly find a page. + +For a glimpse of the possibilities, check out these resources: + +- For a more real-world example, check out either the [dashboard tutorial](/docs/getting_started/dashboard_tutorial) or the [chatapp tutorial](/docs/getting_started/chatapp_tutorial). +- Check out our open-source [templates](/docs/getting_started/open_source_templates)! +- We have an AI Builder that can generate full Reflex apps or help with your existing app! Check it out at [Reflex Build](https://build.reflex.dev/)! +- Deploy your app with a single command using [Reflex Cloud](https://reflex.dev/docs/hosting/deploy-quick-start/)! + +If you want to learn more about how Reflex works, check out the [How Reflex Works](/docs/advanced_onboarding/how-reflex-works) section. + +## Join our Community + +If you have questions about anything related to Reflex, you're always welcome to ask our community on [GitHub Discussions](https://github.com/orgs/reflex-dev/discussions), [Discord](https://discord.gg/T5WSbC2YtQ), [Forum](https://forum.reflex.dev), and [X](https://twitter.com/getreflex). diff --git a/docs/getting_started/open_source_templates.md b/docs/getting_started/open_source_templates.md new file mode 100644 index 00000000000..267a31bba1d --- /dev/null +++ b/docs/getting_started/open_source_templates.md @@ -0,0 +1,64 @@ +# Open Source Templates + +Check out what the community is building with Reflex. See 2000+ more public projects on [Github](https://github.com/reflex-dev/reflex/network/dependents). Want to get your app featured? Submit it [here](https://github.com/reflex-dev/templates). Copy the template command and use it during `reflex init` + +```python exec +import reflex as rx + +from pcweb.components.code_card import gallery_app_card +from pcweb.pages.gallery.sidebar import TemplatesState, pagination, sidebar + + +@rx.memo +def skeleton_card() -> rx.Component: + return rx.skeleton( + class_name="box-border shadow-large border rounded-xl w-full h-[280px] overflow-hidden", + loading=True, + ) + + +def component_grid() -> rx.Component: + from pcweb.pages.gallery.apps import gallery_apps_data + + posts = [] + for path, document in list(gallery_apps_data.items()): + posts.append( + rx.cond( + TemplatesState.filtered_templates.contains(document.metadata["title"]), + gallery_app_card(app=document.metadata), + None, + ) + ) + return rx.box( + *posts, + rx.box( + rx.el.h4( + "No templates found", + class_name="text-base font-semibold text-slate-12 text-nowrap", + ), + class_name="flex-col gap-2 flex absolute left-1 top-0 z-[-1] w-full", + ), + class_name="gap-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 w-full relative", + ) + + +def gallery() -> rx.Component: + return rx.el.section( + rx.box( + sidebar(), + rx.box( + component_grid(), + pagination(), + class_name="flex flex-col", + ), + class_name="flex flex-col gap-6 lg:gap-10 w-full", + ), + id="gallery", + class_name="mx-auto", + ) + +``` + +```python eval +gallery() +``` diff --git a/docs/getting_started/project-structure.md b/docs/getting_started/project-structure.md new file mode 100644 index 00000000000..6065441280c --- /dev/null +++ b/docs/getting_started/project-structure.md @@ -0,0 +1,69 @@ +# Project Structure + + +## Directory Structure + +```python exec +app_name = "hello" +``` + +Let's create a new app called `{app_name}` + +```bash +mkdir {app_name} +cd {app_name} +reflex init +``` + +This will create a directory structure like this: + +```bash +{app_name} +├── .web +├── assets +├── {app_name} +│ ├── __init__.py +│ └── {app_name}.py +└── rxconfig.py +``` + +Let's go over each of these directories and files. + +## .web + +This is where the compiled Javascript files will be stored. You will never need to touch this directory, but it can be useful for debugging. + +Each Reflex page will compile to a corresponding `.js` file in the `.web/pages` directory. + +## Assets + +The `assets` directory is where you can store any static assets you want to be publicly available. This includes images, fonts, and other files. + +For example, if you save an image to `assets/image.png` you can display it from your app like this: + +```python +rx.image(src="https://web.reflex-assets.dev/other/image.png") +``` + +j + +## Main Project + +Initializing your project creates a directory with the same name as your app. This is where you will write your app's logic. + +Reflex generates a default app within the `{app_name}/{app_name}.py` file. You can modify this file to customize your app. + +## Configuration + +The `rxconfig.py` file can be used to configure your app. By default it looks something like this: + +```python +import reflex as rx + + +config = rx.Config( + app_name="{app_name}", +) +``` + +We will discuss project structure and configuration in more detail in the [advanced project structure](/docs/advanced_onboarding/code_structure) documentation. diff --git a/docs/images/dalle.gif b/docs/images/dalle.gif deleted file mode 100644 index 74d487849e9..00000000000 Binary files a/docs/images/dalle.gif and /dev/null differ diff --git a/docs/images/dalle_colored_code_example.png b/docs/images/dalle_colored_code_example.png deleted file mode 100644 index 18c8307c545..00000000000 Binary files a/docs/images/dalle_colored_code_example.png and /dev/null differ diff --git a/docs/images/reflex.png b/docs/images/reflex.png deleted file mode 100644 index 5812af7d836..00000000000 Binary files a/docs/images/reflex.png and /dev/null differ diff --git a/docs/images/reflex.svg b/docs/images/reflex.svg deleted file mode 100644 index f837234f8b5..00000000000 --- a/docs/images/reflex.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/reflex_dark.svg b/docs/images/reflex_dark.svg deleted file mode 100644 index 0aeffe59105..00000000000 --- a/docs/images/reflex_dark.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/docs/images/reflex_light.svg b/docs/images/reflex_light.svg deleted file mode 100644 index 63876bdd13e..00000000000 --- a/docs/images/reflex_light.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/docs/in/README.md b/docs/in/README.md deleted file mode 100644 index d290a585d8d..00000000000 --- a/docs/in/README.md +++ /dev/null @@ -1,249 +0,0 @@ -
-Reflex लोगो -Reflex लोगो - -
- -### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/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.10+ की आवश्यकता है): - -```bash -pip install reflex -``` - -## 🥳 अपना पहला ऐप बनाएं (Create your first App) - -reflex को इंस्टॉल करने से ही reflex कमांड लाइन टूल भी इंस्टॉल हो जाता है। - -सुनिश्चित करें कि इंस्टॉलेशन सफल थी, एक नया प्रोजेक्ट बनाकर इसे टेस्ट करें। ('my_app_name' की जगह अपने प्रोजेक्ट का नाम रखें): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -यह कमांड आपकी नयी डायरेक्टरी में एक टेम्पलेट ऐप को प्रारंभ करता है। - -आप इस ऐप को development मोड में चला सकते हैं: - -```bash -reflex run -``` - -आपको http://localhost:3000 पर अपने ऐप को चलते हुए देखना चाहिए। - -अब आप my_app_name/my_app_name.py में source कोड को संशोधित कर सकते हैं। Reflex में तेज रिफ्रेश की सुविधा है, इसलिए जब आप अपनी कोड को सहेजते हैं, तो आप अपने बदलावों को तुरंत देख सकते हैं। - -## 🫧 उदाहरण ऐप (Example App) - -एक उदाहरण पर चलते हैं: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node) से एक इमेज उत्पन्न करने के लिए UI। सरलता के लिए, हम सिर्फ [OpenAI API](https://platform.openai.com/docs/api-reference/authentication) को बुलाते हैं, लेकिन आप इसे ML मॉडल से बदल सकते हैं locally। - -  - -
-DALL·E के लिए एक फ्रंटएंड रैपर, छवि उत्पन्न करने की प्रक्रिया में दिखाया गया। -
- -  - -यहाँ पर इसका पूरा कोड है जिससे यह बनाया जा सकता है। यह सब एक ही Python फ़ाइल में किया गया है! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## इसे समझते हैं। - -
-DALL-E ऐप के बैकएंड और फ्रंटएंड भागों के बीच के अंतर की व्याख्या करता है। -
- -### **Reflex UI** - -हम UI के साथ शुरू करेंगे। - -```python -def index(): - return rx.center( - ... - ) -``` - -यह `index` फ़ंक्शन एप्लिकेशन की फ़्रंटएंड को परिभाषित करता है। - -हम फ़्रंटएंड बनाने के लिए `center`, `vstack`, `input`, और `button` जैसे विभिन्न components का उपयोग करते हैं। Components को एक-दूसरे के भीतर डाल सकते हैं विस्तारित लेआउट बनाने के लिए। और आप CSS की पूरी ताक़त के साथ इन्हें स्टाइल करने के लिए कीवर्ड आर्ग्यूमेंट (keyword args) का उपयोग कर सकते हैं। - -रिफ़्लेक्स के पास [60+ built-in components](https://reflex.dev/docs/library) हैं जो आपको शुरुआती मदद के लिए हैं। हम बहुत से components जोड़ रहे हैं, और अपने खुद के components बनाना भी आसान है। [create your own components](https://reflex.dev/docs/wrapping-react/overview/) - -### **स्टेट (State)** - -Reflex आपके UI को आपकी स्टेट (state) के एक फ़ंक्शन के रूप में प्रस्तुत करता है। - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False -``` - -स्टेट (state) ऐप में उन सभी वेरिएबल्स (vars) को परिभाषित करती है जो बदल सकती हैं और उन फ़ंक्शनों को जो उन्हें बदलते हैं। - -यहां स्टेट (state) में `prompt` और `image_url` शामिल हैं। प्रगति और छवि दिखाने के लिए `processing` और `complete` बूलियन भी हैं। - -### **इवेंट हैंडलर (Event Handlers)** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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) को परिभाषित करते हैं जो स्टेट वेरिएबल्स को बदलते हैं। इवेंट हैंडलर्स (event handlers) से reflex में स्टेट (state) को मॉडिफ़ाय किया जा सकता हैं। इन्हें उपयोगकर्ता क्रियाओं (user actions) के प्रति प्रतिक्रिया (response) के रूप में बुलाया जा सकता है, जैसे कि बटन को क्लिक करना या टेक्स्ट बॉक्स में टाइप करना। इन क्रियाओं को इवेंट्स (events) कहा जाता है। - -हमारे DALL·E. ऐप में एक इवेंट हैंडलर `get_image` है जिससे यह OpenAI API से इमेज प्राप्त करता है। इवेंट हैंडलर में `yield` का उपयोग करने कि वजह से UI अपडेट हो जाएगा। अन्यथा UI इवेंट हैंडलर के अंत में अपडेट होगा। - -### **रूटिंग (Routing)** - -आखिरकार, हम अपने एप्लिकेशन को परिभाषित करते हैं। - -```python -app = rx.App() -``` - -हम अपने एप्लिकेशन के रूट से इंडेक्स कॉम्पोनेंट तक एक पेज को जोड़ते हैं। हम एक शीर्षक भी जोड़ते हैं जो पेज प्रीव्यू/ब्राउज़र टैब में दिखाई देगा। - -```python -app.add_page(index, title="DALL-E") -``` - -आप और पेज जोड़कर एक मल्टी-पेज एप्लिकेशन बना सकते हैं। - -## 📑 संसाधन (Resources) - -
- -📑 [दस्तावेज़](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [ब्लॉग](https://reflex.dev/blog)   |   📱 [कॉम्पोनेंट लाइब्रेरी](https://reflex.dev/docs/library)   |   🖼️ [टेम्पलेट्स](https://reflex.dev/templates/)   |   🛸 [तैनाती](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ स्टेटस (Status) - -Reflex दिसंबर 2022 में Pynecone नाम से शुरू हुआ। - -2025 की शुरुआत से, [Reflex Cloud](https://cloud.reflex.dev) लॉन्च किया गया है जो Reflex ऐप्स के लिए सर्वोत्तम होस्टिंग अनुभव प्रदान करता है। हम इसे विकसित करना और अधिक सुविधाएँ लागू करना जारी रखेंगे। - -Reflex में हर सप्ताह नए रिलीज़ और फीचर्स आ रहे हैं! सुनिश्चित करें कि :star: स्टार और :eyes: वॉच इस रेपोजिटरी को अपडेट रहने के लिए। - -## (योगदान) Contributing - -हम हर तरह के योगदान का स्वागत करते हैं! रिफ्लेक्स कम्यूनिटी में शुरुआत करने के कुछ अच्छे तरीके नीचे दिए गए हैं। - -- **Join Our Discord** (डिस्कॉर्ड सर्वर से जुड़ें): हमारा [Discord](https://discord.gg/T5WSbC2YtQ) रिफ्लेक्स प्रोजेक्ट पर सहायता प्राप्त करने और आप कैसे योगदान दे सकते हैं, इस पर चर्चा करने के लिए सबसे अच्छी जगह है। -- **GitHub Discussions** (गिटहब चर्चाएँ): उन सुविधाओं के बारे में बात करने का एक शानदार तरीका जिन्हें आप जोड़ना चाहते हैं या ऐसी चीज़ें जो भ्रमित करने वाली हैं/स्पष्टीकरण की आवश्यकता है। -- **GitHub Issues** (गिटहब समस्याएं): [Issues](https://github.com/reflex-dev/reflex/issues) बग की रिपोर्ट करने का एक शानदार तरीका है। इसके अतिरिक्त, आप किसी मौजूदा समस्या को हल करने का प्रयास कर सकते हैं और एक पीआर सबमिट कर सकते हैं। - -हम सक्रिय रूप से योगदानकर्ताओं की तलाश कर रहे हैं, चाहे आपका कौशल स्तर या अनुभव कुछ भी हो। योगदान करने के लिए [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) देखें। - -## हमारे सभी योगदानकर्ताओं का धन्यवाद: - - - - - -## लाइसेंस (License) - -रिफ्लेक्स ओपन-सोर्स है और [अपाचे लाइसेंस 2.0](/LICENSE) के तहत लाइसेंस प्राप्त है। diff --git a/docs/it/README.md b/docs/it/README.md deleted file mode 100644 index 441c079b0ab..00000000000 --- a/docs/it/README.md +++ /dev/null @@ -1,250 +0,0 @@ -
-Reflex Logo -
- -### **✨ App web performanti e personalizzabili in puro Python. Distribuisci in pochi secondi. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![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) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex è una libreria per sviluppare applicazioni web full-stack in Python puro. - -Caratteristiche principali: - -- **Python Puro** - Scrivi il frontend e il backend della tua app interamente in Python, senza bisogno di imparare Javascript. -- **Flessibilità Totale** - Reflex è facile da iniziare, ma può anche gestire app complesse. -- **Distribuzione Istantanea** - Dopo lo sviluppo, distribuisci la tua app con un [singolo comando](https://reflex.dev/docs/hosting/deploy-quick-start/) o ospitala sul tuo server. - -Consulta la nostra [pagina di architettura](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) per scoprire come funziona Reflex sotto il cofano. - -## ⚙️ Installazione - -Apri un terminale ed esegui (Richiede Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 Crea la tua prima app - -Installando `reflex` si installa anche lo strumento da riga di comando `reflex`. - -Verifica che l'installazione sia stata eseguita correttamente creando un nuovo progetto. (Sostituisci `nome_app` con il nome del tuo progetto): - -```bash -mkdir nome_app -cd nome_app -reflex init -``` - -Questo comando inizializza un'app template nella tua nuova directory. - -Puoi eseguire questa app in modalità sviluppo con: - -```bash -reflex run -``` - -Dovresti vedere la tua app in esecuzione su http://localhost:3000. - -Ora puoi modificare il codice sorgente in `nome_app/nome_app.py`. Reflex offre aggiornamenti rapidi, così puoi vedere le tue modifiche istantaneamente quando salvi il tuo codice. - -## 🫧 Esempio App - -Esaminiamo un esempio: creare un'interfaccia utente per la generazione di immagini attorno a [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Per semplicità, chiamiamo semplicemente l'[API OpenAI](https://platform.openai.com/docs/api-reference/authentication), ma potresti sostituirla con un modello ML eseguito localmente. - -  - -
-Un wrapper frontend per DALL·E, mostrato nel processo di generazione di un'immagine. -
- -  - -Ecco il codice completo per crearlo. Tutto fatto in un unico file Python! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """Lo stato dell'app.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Ottieni l'immagine dal prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Vuoto") - - self.processing, self.complete = True, False - yield - 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", font_size="1.5em"), - rx.input( - placeholder="Inserisci un prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Genera Immagine", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Aggiungi stato e pagina all'app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Analizziamolo. - -
-Spiegazione delle differenze tra le parti backend e frontend dell'app DALL-E. -
- -### **Reflex UI** - -Cominciamo con l'UI. - -```python -def index(): - return rx.center( - ... - ) -``` - -Questa funzione `index` definisce il frontend dell'app. - -Utilizziamo diversi componenti come `center`, `vstack`, `input`, e `button` per costruire il frontend. I componenti possono essere annidati gli uni negli altri per creare layout complessi. E puoi utilizzare argomenti chiave per stilizzarli con tutta la potenza di CSS. - -Reflex offre [più di 60 componenti integrati](https://reflex.dev/docs/library) per aiutarti a iniziare. Stiamo attivamente aggiungendo più componenti ed è facile [creare i tuoi componenti](https://reflex.dev/docs/wrapping-react/overview/). - -### **Stato (State)** - -Reflex rappresenta la tua UI come una funzione del tuo stato. - -```python -class State(rx.State): - """Lo stato dell'app.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -Lo stato definisce tutte le variabili (chiamate vars) in un'app che possono cambiare e le funzioni che le cambiano. - -Qui lo stato è composto da un `prompt` e `image_url`. Ci sono anche i booleani `processing` e `complete` per indicare quando disabilitare il pulsante (durante la generazione dell'immagine) e quando mostrare l'immagine risultante. - -### **Gestori di Eventi (Event Handlers)** - -```python -def get_image(self): - """Ottieni l'immagine dal prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Vuoto") - - self.processing, self.complete = True, False - yield - 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 lo stato, definiamo funzioni chiamate gestori di eventi che cambiano le vars dello stato. I gestori di eventi sono il modo in cui possiamo modificare lo stato in Reflex. Possono essere chiamati in risposta alle azioni dell'utente, come fare clic su un pulsante o digitare in una casella di testo. Queste azioni vengono chiamate eventi. - -La nostra app DALL·E ha un gestore di eventi, `get_image` con cui ottiene questa immagine dall'API OpenAI. Utilizzando `yield` nel mezzo di un gestore di eventi farà sì che l'UI venga aggiornata. Altrimenti, l'UI verrà aggiornata alla fine del gestore di eventi. - -### **Instradamento (Routing)** - -Infine, definiamo la nostra app. - -```python -app = rx.App() -``` - -Aggiungiamo una pagina dalla radice dell'app al componente dell'indice. Aggiungiamo anche un titolo che apparirà nell'anteprima della pagina/scheda del browser. - -```python -app.add_page(index, title="DALL-E") -``` - -Puoi creare un'app multi-pagina aggiungendo altre pagine. - -## 📑 Risorse - -
- -📑 [Documentazione](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Libreria Componenti](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Distribuzione](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Stato - -Reflex è stato lanciato nel dicembre 2022 con il nome Pynecone. - -A partire dal 2025, [Reflex Cloud](https://cloud.reflex.dev) è stato lanciato per fornire la migliore esperienza di hosting per le app Reflex. Continueremo a svilupparlo e implementare più funzionalità. - -Reflex ha nuove versioni e funzionalità in arrivo ogni settimana! Assicurati di :star: mettere una stella e :eyes: osservare questa repository per rimanere aggiornato. - -## Contribuire - -Diamo il benvenuto a contributi di qualsiasi dimensione! Di seguito sono alcuni modi per iniziare nella comunità Reflex. - -- **Unisciti al nostro Discord**: Il nostro [Discord](https://discord.gg/T5WSbC2YtQ) è posto migliore per ottenere aiuto sul tuo progetto Reflex e per discutere come puoi contribuire. -- **Discussioni su GitHub**: Un ottimo modo per parlare delle funzionalità che desideri aggiungere o di cose che creano confusione o necessitano chiarimenti. -- **GitHub Issues**: Le [Issues](https://github.com/reflex-dev/reflex/issues) sono un ottimo modo per segnalare bug. Inoltre, puoi provare a risolvere un problema esistente e inviare un PR. - -Stiamo attivamente cercando collaboratori, indipendentemente dal tuo livello di abilità o esperienza. Per contribuire, consulta [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## Un Grazie a Tutti i Nostri Contributori: - - - - - -## Licenza - -Reflex è open-source e rilasciato sotto la [Licenza Apache 2.0](/LICENSE). diff --git a/docs/ja/README.md b/docs/ja/README.md deleted file mode 100644 index 0c6be480512..00000000000 --- a/docs/ja/README.md +++ /dev/null @@ -1,250 +0,0 @@ -
-Reflex Logo -
- -### **✨ 即時デプロイが可能な、Pure Python で作ったパフォーマンスと汎用性が高い Web アプリケーション ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex は Python のみでフルスタック Web アプリケーションを作成できるライブラリです。 - -主な特徴: - -- **Pure Python** - Web アプリケーションのフロントエンドとバックエンドを Python のみで実装できるため、Javascript を学ぶ必要がありません。 -- **高い柔軟性** - Reflex は簡単に始められて、複雑なアプリケーションまで作成できます。 -- **即時デプロイ** - ビルド後、すぐにデプロイが可能です。[単純な CLI コマンド](https://reflex.dev/docs/hosting/deploy-quick-start/)を使ったアプリケーションのデプロイや、自身のサーバーへのホストができます。 - -Reflex がどのように動作しているかを知るには、[アーキテクチャページ](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)をご覧ください。 - -## ⚙️ インストール - -ターミナルを開いて以下のコマンドを実行してください。(Python 3.10 以上が必要です。): - -```bash -pip install reflex -``` - -## 🥳 最初のアプリケーションを作ろう - -`reflex`をインストールすると、`reflex`の CLI ツールが自動でインストールされます。 - -新しいプロジェクトを作成して、インストールが成功しているかを確認しましょう。(`my_app_name`を自身のプロジェクト名に書き換えて実行ください。): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -上記のコマンドを実行すると、新しいフォルダにテンプレートアプリを作成します。 - -下記のコマンドを実行すると、開発モードでアプリを開始します。 - -```bash -reflex run -``` - -http://localhost:3000 にアクセスしてアプリの動作を見ることができます。 - -`my_app_name/my_app_name.py`のソースコードを編集してみましょう!Reflex は fast refresh なので、ソースを保存した直後に変更が Web ページに反映されます。 - -## 🫧 実装例 - -実装例を見てみましょう: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node)を中心とした画像生成 UI を作成しました。説明を簡単にするためにここでは[OpenAI API](https://platform.openai.com/docs/api-reference/authentication)を呼んでいますが、ローカルで動作している機械学習モデルに置き換えることも可能です。 - -  - -
-DALL·Eのフロントエンドラッパーです。画像を生成している過程を表示しています。 -
- -  - -画像生成 UI のソースコードの全貌を見てみましょう。下記のように、単一の Python ファイルで作れます! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """アプリのステート""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """プロンプトからイメージを取得する""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# ステートとページをアプリに追加 -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## それぞれの実装を見てみましょう - -
-DALL-E appのフロントエンドとバックエンドのパーツの違いを説明しています。 -
- -### **Reflex UI** - -UI から見てみましょう。 - -```python -def index(): - return rx.center( - ... - ) -``` - -`index`関数において、アプリのフロントエンドを定義しています。 - -フロントエンドを実装するにあたり、`center`、`vstack`、`input`、`button`など異なるコンポーネントを使用しています。コンポーネントはお互いにネストが可能であり、複雑なレイアウトを作成できます。また、keyword args を使うことで、CSS の機能をすべて使ったスタイルが可能です。 - -Reflex は[60 を超える内臓コンポーネント](https://reflex.dev/docs/library)があるため、すぐに始められます。私たちは、積極的にコンポーネントを追加していますが、簡単に[自身のコンポーネントを追加](https://reflex.dev/docs/wrapping-react/overview/)することも可能です。 - -### **ステート** - -Reflex はステートの関数を用いて UI を表示します。 - -```python -class State(rx.State): - """アプリのステート""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -ステートでは、アプリで変更が可能な全ての変数(vars と呼びます)と、vars の変更が可能な関数を定義します。 - -この例では、ステートを`prompt`と`image_url`で構成しています。そして、ブール型の`processing`と`complete`を用いて、ボタンを無効にするタイミング(画像生成中)や生成された画像を表示するタイミングを示しています。 - -### **イベントハンドラ** - -```python -def get_image(self): - """プロンプトからイメージを取得する""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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 -``` - -ステートにおいて、ステートの vars を変更できるイベントハンドラ関数を定義しています。イベントハンドラは Reflex において、ステートの vars を変更する方法です。ボタンクリックやテキストボックスの入力など、ユーザのアクションに応じてイベントハンドラが呼ばれます。 - -DALL·E.アプリには、OpenAI API からイメージを取得する`get_image`関数があります。イベントハンドラの最後で UI の更新がかかるため、関数の途中に`yield`を入れることで先に UI を更新しています。 - -### **ルーティング** - -最後に、アプリを定義します。 - -```python -app = rx.App() -``` - -アプリにページを追加し、ドキュメントルートを index コンポーネントにルーティングしています。更に、ページのプレビューやブラウザタブに表示されるタイトルを記載しています。 - -```python -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)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ ステータス - -2022 年 12 月に、Reflex は Pynecone という名前でローンチしました。 - -2025 年から [Reflex Cloud](https://cloud.reflex.dev) がローンチされ、Reflex アプリケーションの最高のホスティング体験を提供しています。私たちは引き続き開発を続け、より多くの機能を実装していきます。 - -Reflex は毎週、新しいリリースや機能追加を行っています!最新情報を逃さないために、 :star: Star や :eyes: Watch をお願いします。 - -## コントリビュート - -様々なサイズのコントリビュートを歓迎しています!Reflex コミュニティに入るための方法を、いくつかリストアップします。 - -- **Discord に参加**: [Discord](https://discord.gg/T5WSbC2YtQ)は、Reflex プロジェクトの相談や、コントリビュートについての話し合いをするための、最適な場所です。 -- **GitHub Discussions**: GitHub Discussions では、追加したい機能や、複雑で解明が必要な事柄についての議論に適している場所です。 -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)はバグの報告に適している場所です。また、課題を解決した PR のサブミットにチャレンジしていただくことも、可能です。 - -スキルや経験に関わらず、私たちはコントリビュータを積極的に探しています。コントリビュートするために、[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)をご覧ください。 - -## 私たちのコントリビュータに感謝!: - - - - - -## ライセンス - -Reflex はオープンソースであり、[Apache License 2.0](/LICENSE)に基づいてライセンス供与されます。 diff --git a/docs/kr/README.md b/docs/kr/README.md deleted file mode 100644 index 6234a075724..00000000000 --- a/docs/kr/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -### **✨ 순수 Python으로 고성능 사용자 정의 웹앱을 만들어 보세요. 몇 초만에 배포 가능합니다. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex는 순수 Python으로 풀스택 웹 앱을 구축하기 위한 라이브러리입니다. - -주요 기능: - -- **순수 Python** - 앱의 프론트엔드와 백엔드를 모두 Python으로 작성하며, Javascript를 배울 필요가 없습니다. -- **완전한 유연성** - Reflex는 시작하기 쉽지만, 복잡한 앱으로도 확장할 수 있습니다. -- **즉시 배포** - 앱을 빌드한 후 [단일 명령어](https://reflex.dev/docs/hosting/deploy-quick-start/)로 배포하거나 자체 서버에서 호스팅할 수 있습니다. - -Reflex가 내부적으로 어떻게 작동하는지 알아보려면 [아키텍처 페이지](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)를 참조하세요. - -## ⚙️ 설치 - -터미널을 열고 실행하세요. (Python 3.10+ 필요): - -```bash -pip install reflex -``` - -## 🥳 첫 앱 만들기 - -`reflex`를 설치하면, `reflex` 명령어 라인 도구도 설치됩니다. - -새 프로젝트를 생성하여 설치가 성공적인지 확인합니다. (`my_app_name`을 프로젝트 이름으로 변경합니다.): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -이 명령어는 새 디렉토리에 템플릿 앱을 초기화합니다. - -개발 모드에서 이 앱을 실행할 수 있습니다: - -```bash -reflex run -``` - -http://localhost:3000 에서 앱이 실행 됩니다. - -이제 `my_app_name/my_app_name.py`에서 소스코드를 수정할 수 있습니다. Reflex는 빠른 새로고침을 지원하므로 코드를 저장할 때마다 즉시 변경 사항을 볼 수 있습니다. - -## 🫧 예시 앱 - -예시를 살펴보겠습니다: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node)를 중심으로 이미지 생성 UI를 만들어 보겠습니다. 간단하게 하기 위해 [OpenAI API](https://platform.openai.com/docs/api-reference/authentication)를 호출했지만, 이를 로컬에서 실행되는 ML 모델로 대체할 수 있습니다. - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -이것이 완성된 코드입니다. 이 모든 것은 하나의 Python 파일에서 이루어집니다! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## 하나씩 살펴보겠습니다. - -
-Explaining the differences between backend and frontend parts of the DALL-E app. -
- -### **Reflex UI** - -UI부터 시작해봅시다. - -```python -def index(): - return rx.center( - ... - ) -``` - -`index` 함수는 앱의 프론트엔드를 정의합니다. - -`center`, `vstack`, `input`, `button`과 같은 다양한 컴포넌트를 사용하여 프론트엔드를 구축합니다. -컴포넌트들은 복잡한 레이아웃을 만들기 위해 서로 중첩될 수 있습니다. -그리고 키워드 인자를 사용하여 CSS의 모든 기능을 사용하여 스타일을 지정할 수 있습니다. - -Reflex는 시작하기 위한 [60개 이상의 기본 컴포넌트](https://reflex.dev/docs/library)를 제공하고 있습니다. 더 많은 컴포넌트를 추가하고 있으며, [자신만의 컴포넌트를 생성하는 것](https://reflex.dev/docs/wrapping-react/overview/)도 쉽습니다. - -### **State** - -Reflex는 UI를 State 함수로 표현합니다. - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False -``` - -state는 앱에서 변경될 수 있는 모든 변수(vars로 불림)와 이러한 변수를 변경하는 함수를 정의합니다. - -여기서 state는 `prompt`와 `image_url`로 구성됩니다. 또한 `processing`과 `complete`라는 불리언 값이 있습니다. 이 값들은 이미지 생성 중 버튼을 비활성화할 때와, 결과 이미지를 표시할 때를 나타냅니다. - -### **Event Handlers** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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 내에서, state vars를 변경하는 이벤트 핸들러라고 불리는 함수를 정의합니다. 이벤트 핸들러는 Reflex에서 state를 변경하는 방법입니다. 버튼을 클릭하거나 텍스트 상자에 입력하는 것과 같이 사용자 동작에 응답하여 호출될 수 있습니다. 이러한 동작을 이벤트라고 합니다. - -DALL·E. 앱에는 OpenAI API에서 이미지를 가져오는 `get_image` 이벤트 핸들러가 있습니다. 이벤트 핸들러의 중간에 `yield`를 사용하면 UI가 업데이트됩니다. 그렇지 않으면 UI는 이벤트 핸들러의 끝에서 업데이트됩니다. - -### **Routing** - -마지막으로, 앱을 정의합니다. - -```python -app = rx.App() -``` - -앱의 루트에서 index 컴포넌트로 페이지를 추가합니다. 또한 페이지 미리보기/브라우저 탭에 표시될 제목도 추가합니다. - -```python -app.add_page(index, title="DALL-E") -``` - -여러 페이지를 추가하여 멀티 페이지 앱을 만들 수 있습니다. - -## 📑 자료 - -
- -📑 [문서](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [블로그](https://reflex.dev/blog)   |   📱 [컴포넌트 라이브러리](https://reflex.dev/docs/library)   |   🖼️ [템플릿](https://reflex.dev/templates/)   |   🛸 [배포](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ 상태 - -Reflex는 2022년 12월 Pynecone이라는 이름으로 출시되었습니다. - -2025년부터 [Reflex Cloud](https://cloud.reflex.dev)가 출시되어 Reflex 앱을 위한 최상의 호스팅 경험을 제공합니다. 우리는 계속해서 개발하고 더 많은 기능을 구현할 예정입니다. - -Reflex는 매주 새로운 릴리즈와 기능을 제공합니다! 최신 정보를 확인하려면 :star: Star와 :eyes: Watch를 눌러 이 저장소를 확인하세요. - -## 기여 - -우리는 모든 기여를 환영합니다! 아래는 Reflex 커뮤니티에 참여하는 좋은 방법입니다. - -- **Discord 참여**: 우리의 [Discord](https://discord.gg/T5WSbC2YtQ)는 Reflex 프로젝트에 대한 도움을 받고 기여하는 방법을 논의하는 최고의 장소입니다. -- **GitHub Discussions**: 추가하고 싶은 기능이나 혼란스럽거나 해결이 필요한 것들에 대해 이야기하는 좋은 방법입니다. -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)는 버그를 보고하는 훌륭한 방법입니다. 또한, 기존의 이슈를 해결하고 PR을 제출할 수 있습니다. - -우리는 능력이나 경험에 상관없이 적극적으로 기여자를 찾고 있습니다. 기여하려면 [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)를 확인하세요. - -## 모든 기여자들에게 감사드립니다: - - - - - -## License - -Reflex는 오픈소스이며 [Apache License 2.0](/LICENSE)로 라이선스가 부여됩니다. diff --git a/docs/library/data-display/avatar.md b/docs/library/data-display/avatar.md new file mode 100644 index 00000000000..6956f5dbf5b --- /dev/null +++ b/docs/library/data-display/avatar.md @@ -0,0 +1,142 @@ +--- +components: + - rx.avatar +Avatar: | + lambda **props: rx.hstack(rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", **props), rx.avatar(fallback="RX", **props), spacing="3") +--- + +# Avatar + +```python exec +import reflex as rx +``` + +The Avatar component is used to represent a user, and display their profile pictures or fallback texts such as initials. + +## Basic Example + +To create an avatar component with an image, pass the image URL as the `src` prop. + +```python demo +rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg") +``` + +To display a text such as initials, set the `fallback` prop without passing the `src` prop. + +```python demo +rx.avatar(fallback="RX") +``` + +## Styling + + +### Size + +The `size` prop controls the size and spacing of the avatar. The acceptable size is from `"1"` to `"9"`, with `"3"` being the default. + +```python demo +rx.flex( + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", size="1"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", size="2"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", size="3"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", size="4"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", size="5"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", size="6"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", size="7"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", size="8"), + spacing="1", +) +``` + +### Variant + +The `variant` prop controls the visual style of the avatar fallback text. The variant can be `"solid"` or `"soft"`. The default is `"soft"`. + +```python demo +rx.flex( + rx.avatar(fallback="RX", variant="solid"), + rx.avatar(fallback="RX", variant="soft"), + rx.avatar(fallback="RX"), + spacing="2", +) +``` + +### Color Scheme + +The `color_scheme` prop sets a specific color to the fallback text, ignoring the global theme. + +```python demo +rx.flex( + rx.avatar(fallback="RX", color_scheme="indigo"), + rx.avatar(fallback="RX", color_scheme="cyan"), + rx.avatar(fallback="RX", color_scheme="orange"), + rx.avatar(fallback="RX", color_scheme="crimson"), + spacing="2", +) +``` + +### High Contrast + +The `high_contrast` prop increases color contrast of the fallback text with the background. + +```python demo +rx.grid( + rx.avatar(fallback="RX", variant="solid"), + rx.avatar(fallback="RX", variant="solid", high_contrast=True), + rx.avatar(fallback="RX", variant="soft"), + rx.avatar(fallback="RX", variant="soft", high_contrast=True), + rows="2", + spacing="2", + flow="column", +) +``` + +### Radius + +The `radius` prop sets specific radius value, ignoring the global theme. It can take values `"none" | "small" | "medium" | "large" | "full"`. + +```python demo +rx.grid( + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", radius="none"), + rx.avatar(fallback="RX", radius="none"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", radius="small"), + rx.avatar(fallback="RX", radius="small"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", radius="medium"), + rx.avatar(fallback="RX", radius="medium"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", radius="large"), + rx.avatar(fallback="RX", radius="large"), + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RX", radius="full"), + rx.avatar(fallback="RX", radius="full"), + rows="2", + spacing="2", + flow="column", +) +``` + +### Fallback + +The `fallback` prop indicates the rendered text when the `src` cannot be loaded. + +```python demo +rx.flex( + rx.avatar(fallback="RX"), + rx.avatar(fallback="PC"), + spacing="2", +) +``` + +## Final Example + +As part of a user profile page, the Avatar component is used to display the user's profile picture, with the fallback text showing the user's initials. Text components displays the user's full name and username handle and a Button component shows the edit profile button. + +```python demo +rx.flex( + rx.avatar(src="https://web.reflex-assets.dev/other/logo.jpg", fallback="RU", size="9"), + rx.text("Reflex User", weight="bold", size="4"), + rx.text("@reflexuser", color_scheme="gray"), + rx.button("Edit Profile", color_scheme="indigo", variant="solid"), + direction="column", + spacing="1", +) +``` diff --git a/docs/library/data-display/badge.md b/docs/library/data-display/badge.md new file mode 100644 index 00000000000..3c4a340fe67 --- /dev/null +++ b/docs/library/data-display/badge.md @@ -0,0 +1,125 @@ +--- +components: + - rx.badge + +Badge: | + lambda **props: rx.badge("Basic Badge", **props) +--- + +# Badge + +```python exec +import reflex as rx +``` + +Badges are used to highlight an item's status for quick recognition. + +## Basic Example + +To create a badge component with only text inside, pass the text as an argument. + +```python demo +rx.badge("New") +``` + +## Styling + + +### Size + +The `size` prop controls the size and padding of a badge. It can take values of `"1" | "2"`, with default being `"1"`. + +```python demo +rx.flex( + rx.badge("New"), + rx.badge("New", size="1"), + rx.badge("New", size="2"), + align="center", + spacing="2", +) +``` + +### Variant + +The `variant` prop controls the visual style of the badge. The supported variant types are `"solid" | "soft" | "surface" | "outline"`. The variant default is `"soft"`. + +```python demo +rx.flex( + rx.badge("New", variant="solid"), + rx.badge("New", variant="soft"), + rx.badge("New"), + rx.badge("New", variant="surface"), + rx.badge("New", variant="outline"), + spacing="2", +) +``` + +### Color Scheme + +The `color_scheme` prop sets a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.badge("New", color_scheme="indigo"), + rx.badge("New", color_scheme="cyan"), + rx.badge("New", color_scheme="orange"), + rx.badge("New", color_scheme="crimson"), + spacing="2", +) +``` + +### High Contrast + +The `high_contrast` prop increases color contrast of the fallback text with the background. + +```python demo +rx.flex( + rx.flex( + rx.badge("New", variant="solid"), + rx.badge("New", variant="soft"), + rx.badge("New", variant="surface"), + rx.badge("New", variant="outline"), + spacing="2", + ), + rx.flex( + rx.badge("New", variant="solid", high_contrast=True), + rx.badge("New", variant="soft", high_contrast=True), + rx.badge("New", variant="surface", high_contrast=True), + rx.badge("New", variant="outline", high_contrast=True), + spacing="2", + ), + direction="column", + spacing="2", +) +``` + +### Radius + +The `radius` prop sets specific radius value, ignoring the global theme. It can take values `"none" | "small" | "medium" | "large" | "full"`. + +```python demo +rx.flex( + rx.badge("New", radius="none"), + rx.badge("New", radius="small"), + rx.badge("New", radius="medium"), + rx.badge("New", radius="large"), + rx.badge("New", radius="full"), + spacing="3", +) +``` + +## Final Example + +A badge may contain more complex elements within it. This example uses a `flex` component to align an icon and the text correctly, using the `gap` prop to +ensure a comfortable spacing between the two. + +```python demo +rx.badge( + rx.flex( + rx.icon(tag="arrow_up"), + rx.text("8.8%"), + spacing="1", + ), + color_scheme="grass", +) +``` diff --git a/docs/library/data-display/callout-ll.md b/docs/library/data-display/callout-ll.md new file mode 100644 index 00000000000..8cb58950e1d --- /dev/null +++ b/docs/library/data-display/callout-ll.md @@ -0,0 +1,139 @@ +--- +components: + - rx.callout.root + - rx.callout.icon + - rx.callout.text +--- + +```python exec +import reflex as rx +``` + +# Callout + +A `callout` is a short message to attract user's attention. + +```python demo +rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), +) +``` + +The `callout` component is made up of a `callout.root`, which groups `callout.icon` and `callout.text` parts. This component is based on the `div` element and supports common margin props. + +The `callout.icon` provides width and height for the `icon` associated with the `callout`. This component is based on the `div` element. See the [**icon** component for all icons that are available.](/docs/library/data-display/icon/) + +The `callout.text` renders the callout text. This component is based on the `p` element. + +## As alert + +```python demo +rx.callout.root( + rx.callout.icon(rx.icon(tag="triangle_alert")), + rx.callout.text("Access denied. Please contact the network administrator to view this page."), + color_scheme="red", + role="alert", +) +``` + +## Style + +### Size + +Use the `size` prop to control the size. + +```python demo +rx.flex( + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + size="3", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + size="2", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + size="1", + ), + direction="column", + spacing="3", + align="start", +) +``` + +### Variant + +Use the `variant` prop to control the visual style. It is set to `soft` by default. + +```python demo +rx.flex( + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + variant="soft", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + variant="surface", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + variant="outline", + ), + direction="column", + spacing="3", +) +``` + +### Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + color_scheme="blue", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + color_scheme="green", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + color_scheme="red", + ), + direction="column", + spacing="3", +) +``` + +### High Contrast + +Use the `high_contrast` prop to add additional contrast. + +```python demo +rx.flex( + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + high_contrast=True, + ), + direction="column", + spacing="3", +) +``` diff --git a/docs/library/data-display/callout.md b/docs/library/data-display/callout.md new file mode 100644 index 00000000000..ab16f930b48 --- /dev/null +++ b/docs/library/data-display/callout.md @@ -0,0 +1,95 @@ +--- +components: + - rx.callout + - rx.callout.root + - rx.callout.icon + - rx.callout.text + +Callout: | + lambda **props: rx.callout("Basic Callout", icon="search", **props) + +CalloutRoot: | + lambda **props: rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + **props + ) +--- + +```python exec +import reflex as rx +``` + +# Callout + +A `callout` is a short message to attract user's attention. + +```python demo +rx.callout("You will need admin privileges to install and access this application.", icon="info") +``` + +The `icon` prop allows an icon to be passed to the `callout` component. See the [**icon** component for all icons that are available.](/docs/library/data-display/icon) + +## As alert + +```python demo +rx.callout("Access denied. Please contact the network administrator to view this page.", icon="triangle_alert", color_scheme="red", role="alert") +``` + +## Style + +### Size + +Use the `size` prop to control the size. + +```python demo +rx.flex( + rx.callout("You will need admin privileges to install and access this application.", icon="info", size="3",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", size="2",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", size="1",), + direction="column", + spacing="3", + align="start", +) +``` + +### Variant + +Use the `variant` prop to control the visual style. It is set to `soft` by default. + +```python demo +rx.flex( + rx.callout("You will need admin privileges to install and access this application.", icon="info", variant="soft",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", variant="surface",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", variant="outline",), + direction="column", + spacing="3", +) +``` + +### Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.callout("You will need admin privileges to install and access this application.", icon="info", color_scheme="blue",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", color_scheme="green",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", color_scheme="red",), + direction="column", + spacing="3", +) +``` + +### High Contrast + +Use the `high_contrast` prop to add additional contrast. + +```python demo +rx.flex( + rx.callout("You will need admin privileges to install and access this application.", icon="info",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", high_contrast=True,), + direction="column", + spacing="3", +) +``` diff --git a/docs/library/data-display/code_block.md b/docs/library/data-display/code_block.md new file mode 100644 index 00000000000..d7e299aa2d9 --- /dev/null +++ b/docs/library/data-display/code_block.md @@ -0,0 +1,25 @@ +--- +components: + - rx.code_block +--- + +```python exec +import reflex as rx +``` + +# Code Block + +The Code Block component can be used to display code easily within a website. +Put in a multiline string with the correct spacing and specify and language to show the desired code. + +```python demo +rx.code_block( + """def fib(n): + if n <= 1: + return n + else: + return(fib(n-1) + fib(n-2))""", + language="python", + show_line_numbers=True, +) +``` diff --git a/docs/library/data-display/data_list.md b/docs/library/data-display/data_list.md new file mode 100644 index 00000000000..cd866a00f2c --- /dev/null +++ b/docs/library/data-display/data_list.md @@ -0,0 +1,91 @@ +--- +components: + - rx.data_list.root + - rx.data_list.item + - rx.data_list.label + - rx.data_list.value +DataListRoot: | + lambda **props: rx.data_list.root( + rx.foreach( + [["Status", "Authorized"], ["ID", "U-474747"], ["Name", "Developer Success"], ["Email", "foo@reflex.dev"]], + lambda item: rx.data_list.item(rx.data_list.label(item[0]), rx.data_list.value(item[1])), + ), + **props, + ) +DataListItem: | + lambda **props: rx.data_list.root( + rx.foreach( + [["Status", "Authorized"], ["ID", "U-474747"], ["Name", "Developer Success"], ["Email", "foo@reflex.dev"]], + lambda item: rx.data_list.item(rx.data_list.label(item[0]), rx.data_list.value(item[1]), **props), + ), + ) +DataListLabel: | + lambda **props: rx.data_list.root( + rx.foreach( + [["Status", "Authorized"], ["ID", "U-474747"], ["Name", "Developer Success"], ["Email", "foo@reflex.dev"]], + lambda item: rx.data_list.item(rx.data_list.label(item[0], **props), rx.data_list.value(item[1])), + ), + ) +DataListValue: | + lambda **props: rx.data_list.root( + rx.foreach( + [["Status", "Authorized"], ["ID", "U-474747"], ["Name", "Developer Success"], ["Email", "foo@reflex.dev"]], + lambda item: rx.data_list.item(rx.data_list.label(item[0]), rx.data_list.value(item[1], **props)), + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Data List + +The `DataList` component displays key-value pairs and is particularly helpful for showing metadata. + +A `DataList` needs to be initialized using `rx.data_list.root()` and currently takes in data list items: `rx.data_list.item` + +```python demo +rx.card( + rx.data_list.root( + rx.data_list.item( + rx.data_list.label("Status"), + rx.data_list.value( + rx.badge( + "Authorized", + variant="soft", + radius="full", + ) + ), + align="center", + ), + rx.data_list.item( + rx.data_list.label("ID"), + rx.data_list.value(rx.code("U-474747")), + ), + rx.data_list.item( + rx.data_list.label("Name"), + rx.data_list.value("Developer Success"), + align="center", + ), + rx.data_list.item( + rx.data_list.label("Email"), + rx.data_list.value( + rx.link( + "success@reflex.dev", + href="mailto:success@reflex.dev", + ), + ), + ), + rx.data_list.item( + rx.data_list.label("Company"), + rx.data_list.value( + rx.link( + "Reflex", + href="https://reflex.dev", + ), + ), + ), + ), + ), +``` diff --git a/docs/library/data-display/icon.md b/docs/library/data-display/icon.md new file mode 100644 index 00000000000..d2190117c6d --- /dev/null +++ b/docs/library/data-display/icon.md @@ -0,0 +1,194 @@ +--- +components: + - rx.lucide.Icon +--- + +```python exec +import reflex as rx +from pcweb.components.icons.lucide.lucide import lucide_icons +``` + +# Icon + +The Icon component is used to display an icon from a library of icons. This implementation is based on the [Lucide Icons](https://lucide.dev/icons) where you can find a list of all available icons. + +## Icons List + +```python eval +lucide_icons() +``` + +## Basic Example + +To display an icon, specify the `tag` prop from the list of available icons. +Passing the tag as the first children is also supported and will be assigned to the `tag` prop. + +The `tag` is expected to be in `snake_case` format, but `kebab-case` is also supported to allow copy-paste from [https://lucide.dev/icons](https://lucide.dev/icons). + +```python demo +rx.flex( + rx.icon("calendar"), + rx.icon(tag="calendar"), + gap="2", +) +``` + +## Dynamic Icons + +There are two ways to use dynamic icons in Reflex: + +### Using rx.match + +If you have a specific subset of icons you want to use dynamically, you can define an `rx.match` with them: + +```python +def dynamic_icon_with_match(icon_name): + return rx.match( + icon_name, + ("plus", rx.icon("plus")), + ("minus", rx.icon("minus")), + ("equal", rx.icon("equal")), + ) +``` + +```python exec +def dynamic_icon_with_match(icon_name): + return rx.match( + icon_name, + ("plus", rx.icon("plus")), + ("minus", rx.icon("minus")), + ("equal", rx.icon("equal")), + ) +``` + +### Using Dynamic Icon Tags + +Reflex also supports using dynamic values directly as the `tag` prop in `rx.icon()`. This allows you to use any icon from the Lucide library dynamically at runtime. + +```python exec +class DynamicIconState(rx.State): + current_icon: str = "heart" + + def change_icon(self): + icons = ["heart", "star", "bell", "calendar", "settings"] + import random + self.current_icon = random.choice(icons) +``` + +```python demo +rx.vstack( + rx.heading("Dynamic Icon Example"), + rx.icon(DynamicIconState.current_icon, size=30, color="red"), + rx.button("Change Icon", on_click=DynamicIconState.change_icon), + spacing="4", + align="center", +) +``` + +Under the hood, when a dynamic value is passed as the `tag` prop to `rx.icon()`, Reflex automatically uses a special `DynamicIcon` component that can load icons at runtime. + +```md alert +When using dynamic icons, make sure the icon names are valid. Invalid icon names will cause runtime errors. +``` + +## Styling + +Icon from Lucide can be customized with the following props `stroke_width`, `size` and `color`. + +### Stroke Width + +```python demo +rx.flex( + rx.icon("moon", stroke_width=1), + rx.icon("moon", stroke_width=1.5), + rx.icon("moon", stroke_width=2), + rx.icon("moon", stroke_width=2.5), + gap="2" +) +``` + +### Size + +```python demo +rx.flex( + rx.icon("zoom_in", size=15), + rx.icon("zoom_in", size=20), + rx.icon("zoom_in", size=25), + rx.icon("zoom_in", size=30), + align="center", + gap="2", +) +``` + +### Color + +Here is an example using basic colors in icons. + +```python demo +rx.flex( + rx.icon("zoom_in", size=18, color="indigo"), + rx.icon("zoom_in", size=18, color="cyan"), + rx.icon("zoom_in", size=18, color="orange"), + rx.icon("zoom_in", size=18, color="crimson"), + gap="2", +) +``` + +A radix color with a scale may also be specified using `rx.color()` as seen below. + +```python demo +rx.flex( + rx.icon("zoom_in", size=18, color=rx.color("purple", 1)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 2)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 3)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 4)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 5)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 6)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 7)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 8)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 9)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 10)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 11)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 12)), + gap="2", +) +``` + +Here is another example using the `accent` color with scales. The `accent` is the most dominant color in your theme. + +```python demo +rx.flex( + rx.icon("zoom_in", size=18, color=rx.color("accent", 1)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 2)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 3)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 4)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 5)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 6)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 7)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 8)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 9)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 10)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 11)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 12)), + gap="2", +) +``` + +## Final Example + +Icons can be used as child components of many other components. For example, adding a magnifying glass icon to a search bar. + +```python demo +rx.badge( + rx.flex( + rx.icon("search", size=18), + rx.text("Search documentation...", size="3", weight="medium"), + direction="row", + gap="1", + align="center", + ), + size="2", + radius="full", + color_scheme="gray", +) +``` diff --git a/docs/library/data-display/list.md b/docs/library/data-display/list.md new file mode 100644 index 00000000000..b5dfcf8de72 --- /dev/null +++ b/docs/library/data-display/list.md @@ -0,0 +1,70 @@ +--- +components: + - rx.list.item + - rx.list.ordered + - rx.list.unordered +--- + +```python exec +import reflex as rx +``` + +# List + +A `list` is a component that is used to display a list of items, stacked vertically by default. A `list` can be either `ordered` or `unordered`. It is based on the `flex` component and therefore inherits all of its props. + +`list.unordered` has bullet points to display the list items. + +```python demo +rx.list.unordered( + rx.list.item("Example 1"), + rx.list.item("Example 2"), + rx.list.item("Example 3"), +) +``` + +`list.ordered` has numbers to display the list items. + +```python demo +rx.list.ordered( + rx.list.item("Example 1"), + rx.list.item("Example 2"), + rx.list.item("Example 3"), +) +``` + +`list.unordered()` and `list.ordered()` can have no bullet points or numbers by setting the `list_style_type` prop to `none`. +This is effectively the same as using the `list()` component. + +```python demo +rx.hstack( + rx.list( + rx.list.item("Example 1"), + rx.list.item("Example 2"), + rx.list.item("Example 3"), + ), + rx.list.unordered( + rx.list.item("Example 1"), + rx.list.item("Example 2"), + rx.list.item("Example 3"), + list_style_type="none", + ) +) +``` + +Lists can also be used with icons. + +```python demo +rx.list( + rx.list.item( + rx.icon("circle_check_big", color="green"), " Allowed", + ), + rx.list.item( + rx.icon("octagon_x", color="red"), " Not", + ), + rx.list.item( + rx.icon("settings", color="grey"), " Settings" + ), + list_style_type="none", +) +``` diff --git a/docs/library/data-display/moment.md b/docs/library/data-display/moment.md new file mode 100644 index 00000000000..c6ab9c3e6f9 --- /dev/null +++ b/docs/library/data-display/moment.md @@ -0,0 +1,157 @@ +--- +components: + - rx.moment +--- + +# Moment + +Displaying date and relative time to now sometimes can be more complicated than necessary. + +To make it easy, Reflex is wrapping [react-moment](https://www.npmjs.com/package/react-moment) under `rx.moment`. + +```python exec +import reflex as rx +from reflex.utils.serializers import serialize_datetime +``` + +## Examples + +Using a date from a state var as a value, we will display it in a few different +way using `rx.moment`. + +The `date_now` state var is initialized when the site was deployed. The +button below can be used to update the var to the current datetime, which will +be reflected in the subsequent examples. + +```python demo exec +from datetime import datetime, timezone + +class MomentState(rx.State): + date_now: datetime = datetime.now(timezone.utc) + + @rx.event + def update(self): + self.date_now = datetime.now(timezone.utc) + + +def moment_update_example(): + return rx.button("Update", rx.moment(MomentState.date_now), on_click=MomentState.update) +``` + +### Display the date as-is: + +```python demo +rx.moment(MomentState.date_now) +``` + +### Humanized interval + +Sometimes we don't want to display just a raw date, but we want something more instinctive to read. That's when we can use `from_now` and `to_now`. + +```python demo +rx.moment(MomentState.date_now, from_now=True) +``` + +```python demo +rx.moment(MomentState.date_now, to_now=True) +``` + +You can also set a duration (in milliseconds) with `from_now_during` where the date will display as relative, then after that, it will be displayed as defined in `format`. + +```python demo +rx.moment(MomentState.date_now, from_now_during=100000) # after 100 seconds, date will display normally +``` + +### Formatting dates + +```python demo +rx.moment(MomentState.date_now, format="YYYY-MM-DD") +``` + +```python demo +rx.moment(MomentState.date_now, format="HH:mm:ss") +``` + +### Offset Date + +With the props `add` and `subtract`, you can pass an `rx.MomentDelta` object to modify the displayed date without affecting the stored date in your state. + +#### Add + +```python demo +rx.vstack( + rx.moment(MomentState.date_now, add=rx.MomentDelta(years=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(quarters=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(months=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(weeks=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(days=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(hours=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(minutes=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(seconds=2), format="YYYY-MM-DD - HH:mm:ss"), +) +``` + +#### Subtract + +```python demo +rx.vstack( + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(years=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(quarters=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(months=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(weeks=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(days=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(hours=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(minutes=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(seconds=2), format="YYYY-MM-DD - HH:mm:ss"), +) +``` + +### Timezones + +You can also set dates to display in a specific timezone: + +```python demo +rx.vstack( + rx.moment(MomentState.date_now, tz="America/Los_Angeles"), + rx.moment(MomentState.date_now, tz="Europe/Paris"), + rx.moment(MomentState.date_now, tz="Asia/Tokyo"), +) +``` + +### Client-side periodic update + +If a date is not passed to `rx.moment`, it will use the client's current time. + +If you want to update the date every second, you can use the `interval` prop. + +```python demo +rx.moment(interval=1000, format="HH:mm:ss") +``` + +Even better, you can actually link an event handler to the `on_change` prop that will be called every time the date is updated: + +```python demo exec +class MomentLiveState(rx.State): + updating: bool = False + + @rx.event + def on_update(self, date): + return rx.toast(f"Date updated: {date}") + + @rx.event + def set_updating(self, value: bool): + self.updating = value + +def moment_live_example(): + return rx.hstack( + rx.moment( + format="HH:mm:ss", + interval=rx.cond(MomentLiveState.updating, 5000, 0), + on_change=MomentLiveState.on_update, + ), + rx.switch( + is_checked=MomentLiveState.updating, + on_change=MomentLiveState.set_updating, + ), + ) +``` diff --git a/docs/library/data-display/progress.md b/docs/library/data-display/progress.md new file mode 100644 index 00000000000..aa3a1cdd691 --- /dev/null +++ b/docs/library/data-display/progress.md @@ -0,0 +1,55 @@ +--- +components: + - rx.progress + +Progress: | + lambda **props: rx.progress(value=50, **props) +--- + +# Progress + +Progress is used to display the progress status for a task that takes a long time or consists of several steps. + +```python exec +import reflex as rx +``` + +## Basic Example + +`rx.progress` expects the `value` prop to set the progress value. +`width` is default to 100%, the width of its parent component. + +```python demo +rx.vstack( + rx.progress(value=0), + rx.progress(value=50), + rx.progress(value=100), + width="50%", +) +``` + +For a dynamic progress, you can assign a state variable to the `value` prop instead of a constant value. + +```python demo exec +import asyncio + +class ProgressState(rx.State): + value: int = 0 + + @rx.event(background=True) + async def start_progress(self): + async with self: + self.value = 0 + while self.value < 100: + await asyncio.sleep(0.1) + async with self: + self.value += 1 + + +def live_progress(): + return rx.hstack( + rx.progress(value=ProgressState.value), + rx.button("Start", on_click=ProgressState.start_progress), + width="50%" + ) +``` diff --git a/docs/library/data-display/scroll_area.md b/docs/library/data-display/scroll_area.md new file mode 100644 index 00000000000..e2f0829d001 --- /dev/null +++ b/docs/library/data-display/scroll_area.md @@ -0,0 +1,236 @@ +--- +components: + - rx.scroll_area + +ScrollArea: | + lambda **props: rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and aesthetics. Although in a non-technical sense "legible" and "readable"are often used synonymously, typographically they are separate but related concepts.""", + size="5", + ), + rx.text( + """Legibility describes how easily individual characters can be distinguished from one another. It is described by Walter Tracy as "the quality of being decipherable and recognisable". For instance, if a "b" and an "h", or a "3" and an "8", are difficult to distinguish at small sizes, this is a problem of legibility.""", + size="5", + ), + direction="column", + spacing="4", + height="100px", + width="50%", + ), + **props + ) +--- + +```python exec +import random +import reflex as rx +``` + +# Scroll Area + +Custom styled, cross-browser scrollable area using native functionality. + +## Basic Example + +```python demo +rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and + aesthetics. Although in a non-technical sense “legible” and “readable” + are often used synonymously, typographically they are separate but + related concepts.""", + ), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as “the + quality of being decipherable and recognisable”. For instance, if a “b” + and an “h”, or a “3” and an “8”, are difficult to distinguish at small + sizes, this is a problem of legibility.""", + ), + rx.text( + """Typographers are concerned with legibility insofar as it is their job to + select the correct font to use. Brush Script is an example of a font + containing many characters that might be difficult to distinguish. The + selection of cases influences the legibility of typography because using + only uppercase letters (all-caps) reduces legibility.""", + ), + direction="column", + spacing="4", + ), + type="always", + scrollbars="vertical", + style={"height": 180}, + +) + +``` + +## Control the scrollable axes + +Use the `scrollbars` prop to limit scrollable axes. This prop can take values `"vertical" | "horizontal" | "both"`. + +```python demo +rx.grid( + rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and + aesthetics. Although in a non-technical sense "legible" and "readable" + are often used synonymously, typographically they are separate but + related concepts.""", + size="2", trim="both", + ), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", padding_right="48px", direction="column", spacing="4", + ), + type="always", + scrollbars="vertical", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and + aesthetics. Although in a non-technical sense "legible" and "readable" + are often used synonymously, typographically they are separate but + related concepts.""", + size="2", trim="both", + ), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", spacing="4", style={"width": 700}, + ), + type="always", + scrollbars="horizontal", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and + aesthetics. Although in a non-technical sense "legible" and "readable" + are often used synonymously, typographically they are separate but + related concepts.""", + size="2", trim="both", + ), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", spacing="4", style={"width": 400}, + ), + type="always", + scrollbars="both", + style={"height": 150}, + ), + columns="3", + spacing="2", +) +``` + +## Setting the type of the Scrollbars + +The `type` prop describes the nature of scrollbar visibility. + +`auto` means that scrollbars are visible when content is overflowing on the corresponding orientation. + +`always` means that scrollbars are always visible regardless of whether the content is overflowing. + +`scroll` means that scrollbars are visible when the user is scrolling along its corresponding orientation. + +`hover` when the user is scrolling along its corresponding orientation and when the user is hovering over the scroll area. + +```python demo +rx.grid( + rx.scroll_area( + rx.flex( + rx.text("type = 'auto'", weight="bold"), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", direction="column", spacing="4", + ), + type="auto", + scrollbars="vertical", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text("type = 'always'", weight="bold"), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", direction="column", spacing="4", + ), + type="always", + scrollbars="vertical", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text("type = 'scroll'", weight="bold"), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", direction="column", spacing="4", + ), + type="scroll", + scrollbars="vertical", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text("type = 'hover'", weight="bold"), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", direction="column", spacing="4", + ), + type="hover", + scrollbars="vertical", + style={"height": 150}, + ), + columns="4", + spacing="2", +) + +``` diff --git a/docs/library/data-display/spinner.md b/docs/library/data-display/spinner.md new file mode 100644 index 00000000000..686a076d06b --- /dev/null +++ b/docs/library/data-display/spinner.md @@ -0,0 +1,54 @@ +--- +components: + - rx.spinner +--- + +# Spinner + +Spinner is used to display an animated loading indicator when a task is in progress. + +```python exec +import reflex as rx +``` + +```python demo +rx.spinner() +``` + +## Basic Examples + +Spinner can have different sizes. + +```python demo +rx.vstack( + rx.hstack( + rx.spinner(size="1"), + rx.spinner(size="2"), + rx.spinner(size="3"), + align="center", + gap="1em" + ) +) +``` + +## Demo with buttons + +Buttons have their own loading prop that automatically composes a spinner. + +```python demo +rx.button("Bookmark", loading=True) +``` + +## Spinner inside a button + +If you have an icon inside the button, you can use the button's disabled state and wrap the icon in a standalone rx.spinner to achieve a more sophisticated design. + +```python demo +rx.button( + rx.spinner( + loading=True + ), + "Bookmark", + disabled=True +) +``` diff --git a/docs/library/disclosure/accordion.md b/docs/library/disclosure/accordion.md new file mode 100644 index 00000000000..4a2a9eda9bb --- /dev/null +++ b/docs/library/disclosure/accordion.md @@ -0,0 +1,359 @@ +--- +components: + - rx.accordion.root + - rx.accordion.item + +AccordionRoot: | + lambda **props: rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + width="300px", + **props, + ) + +AccordionItem: | + lambda **props: rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content", **props), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", **props, + ), + rx.accordion.item(header="Third item", content="The third accordion item's content", **props), + width="300px", + ) +--- + +```python exec +import reflex as rx +``` + +# Accordion + +An accordion is a vertically stacked set of interactive headings that each reveal an associated section of content. +The accordion component is made up of `accordion`, which is the root of the component and takes in an `accordion.item`, +which contains all the contents of the collapsible section. + +## Basic Example + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + width="300px", +) +``` + +## Styling + +### Type + +We use the `type` prop to determine whether multiple items can be opened at once. The allowed values for this prop are +`single` and `multiple` where `single` will only open one item at a time. The default value for this prop is `single`. + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + type="multiple", +) +``` + +### Default Value + +We use the `default_value` prop to specify which item should open by default. The value for this prop should be any of the +unique values set by an `accordion.item`. + +```python demo +rx.flex( + rx.accordion.root( + rx.accordion.item( + header="First Item", + content="The first accordion item's content", + value="item_1", + ), + rx.accordion.item( + header="Second Item", + content="The second accordion item's content", + value="item_2", + ), + rx.accordion.item( + header="Third item", + content="The third accordion item's content", + value="item_3", + ), + width="300px", + default_value="item_2", + ), + direction="row", + spacing="2" +) +``` + +### Collapsible + +We use the `collapsible` prop to allow all items to close. If set to `False`, an opened item cannot be closed. + +```python demo +rx.flex( + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item(header="Second Item", content="The second accordion item's content"), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item(header="Second Item", content="The second accordion item's content"), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=False, + width="300px", + ), + direction="row", + spacing="2" +) +``` + +### Disable + +We use the `disabled` prop to prevent interaction with the accordion and all its items. + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item(header="Second Item", content="The second accordion item's content"), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + disabled=True, +) +``` + +### Orientation + +We use `orientation` prop to set the orientation of the accordion to `vertical` or `horizontal`. By default, the orientation +will be set to `vertical`. Note that, the orientation prop won't change the visual orientation but the +functional orientation of the accordion. This means that for vertical orientation, the up and down arrow keys moves focus between the next or previous item, +while for horizontal orientation, the left or right arrow keys moves focus between items. + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + orientation="vertical", +) +``` + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + orientation="horizontal", +) +``` + +### Variant + +```python demo +rx.flex( + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="classic", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="soft", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="outline", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="surface", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="ghost", + ), + direction="row", + spacing="2" +) +``` + +### Color Scheme + +We use the `color_scheme` prop to assign a specific color to the accordion background, ignoring the global theme. + +```python demo +rx.flex( + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + color_scheme="grass", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + color_scheme="green", + ), + direction="row", + spacing="2" +) +``` + +### Value + +We use the `value` prop to specify the controlled value of the accordion item that we want to activate. +This property should be used in conjunction with the `on_value_change` event argument. + +```python demo exec +class AccordionState(rx.State): + """The app state.""" + value: str = "item_1" + item_selected: str + + @rx.event + def change_value(self, value): + self.value = value + self.item_selected = f"{value} selected" + + +def index() -> rx.Component: + return rx.theme( + rx.container( + rx.text(AccordionState.item_selected), + rx.flex( + rx.accordion.root( + rx.accordion.item( + header="Is it accessible?", + content=rx.button("Test button"), + value="item_1", + ), + rx.accordion.item( + header="Is it unstyled?", + content="Yes. It's unstyled by default, giving you freedom over the look and feel.", + value="item_2", + ), + rx.accordion.item( + header="Is it finished?", + content="It's still in beta, but it's ready to use in production.", + value="item_3", + ), + collapsible=True, + width="300px", + value=AccordionState.value, + on_value_change=lambda value: AccordionState.change_value(value), + ), + direction="column", + spacing="2", + ), + padding="2em", + text_align="center", + ) + ) +``` + +## AccordionItem + +The accordion item contains all the parts of a collapsible section. + +## Styling + +### Value + +```python demo +rx.accordion.root( + rx.accordion.item( + header="First Item", + content="The first accordion item's content", + value="item_1", + ), + rx.accordion.item( + header="Second Item", + content="The second accordion item's content", + value="item_2", + ), + rx.accordion.item( + header="Third item", + content="The third accordion item's content", + value="item_3", + ), + collapsible=True, + width="300px", +) +``` + +### Disable + +```python demo +rx.accordion.root( + rx.accordion.item( + header="First Item", + content="The first accordion item's content", + disabled=True, + ), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + color_scheme="blue", +) +``` diff --git a/docs/library/disclosure/segmented_control.md b/docs/library/disclosure/segmented_control.md new file mode 100644 index 00000000000..9740c556d21 --- /dev/null +++ b/docs/library/disclosure/segmented_control.md @@ -0,0 +1,56 @@ +--- +components: + - rx.segmented_control.root + - rx.segmented_control.item +--- + +```python exec +import reflex as rx + +class SegmentedState(rx.State): + """The app state.""" + + control: str = "test" + + @rx.event + def set_control(self, value: str | list[str]): + self.control = value + + +``` + +# Segmented Control + +Segmented Control offers a clear and accessible way to switch between predefined values and views, e.g., "Inbox," "Drafts," and "Sent." + +With Segmented Control, you can make mutually exclusive choices, where only one option can be active at a time, clear and accessible. Without Segmented Control, end users might have to deal with controls like dropdowns or multiple buttons that don't clearly convey state or group options together visually. + +## Basic Example + +The `Segmented Control` component is made up of a `rx.segmented_control.root` which groups `rx.segmented_control.item`. + +The `rx.segmented_control.item` components define the individual segments of the control, each with a label and a unique value. + +```python demo +rx.vstack( + rx.segmented_control.root( + rx.segmented_control.item("Home", value="home"), + rx.segmented_control.item("About", value="about"), + rx.segmented_control.item("Test", value="test"), + on_change=SegmentedState.set_control, + value=SegmentedState.control, + ), + rx.card( + rx.text(SegmentedState.control, align="left"), + rx.text(SegmentedState.control, align="center"), + rx.text(SegmentedState.control, align="right"), + width="100%", + ), +) +``` + +**In the example above:** + +`on_change` is used to specify a callback function that will be called when the user selects a different segment. In this case, the `SegmentedState.setvar("control")` function is used to update the `control` state variable when the user changes the selected segment. + +`value` prop is used to specify the currently selected segment, which is bound to the `SegmentedState.control` state variable. diff --git a/docs/library/disclosure/tabs.md b/docs/library/disclosure/tabs.md new file mode 100644 index 00000000000..05fdb8a67be --- /dev/null +++ b/docs/library/disclosure/tabs.md @@ -0,0 +1,330 @@ +--- +components: + - rx.tabs.root + - rx.tabs.list + - rx.tabs.trigger + - rx.tabs.content + +only_low_level: + - True + +TabsRoot: | + lambda **props: rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Account", value="account"), + rx.tabs.trigger("Documents", value="documents"), + rx.tabs.trigger("Settings", value="settings"), + ), + rx.box( + rx.tabs.content( + rx.text("Make changes to your account"), + value="account", + ), + rx.tabs.content( + rx.text("Update your documents"), + value="documents", + ), + rx.tabs.content( + rx.text("Edit your personal profile"), + value="settings", + ), + ), + **props, + ) + +TabsList: | + lambda **props: rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Account", value="account"), + rx.tabs.trigger("Documents", value="documents"), + rx.tabs.trigger("Settings", value="settings"), + **props, + ), + rx.box( + rx.tabs.content( + rx.text("Make changes to your account"), + value="account", + ), + rx.tabs.content( + rx.text("Update your documents"), + value="documents", + ), + rx.tabs.content( + rx.text("Edit your personal profile"), + value="settings", + ), + ), + ) + +TabsTrigger: | + lambda **props: rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Account", value="account", **props,), + rx.tabs.trigger("Documents", value="documents"), + rx.tabs.trigger("Settings", value="settings"), + ), + rx.box( + rx.tabs.content( + rx.text("Make changes to your account"), + value="account", + ), + rx.tabs.content( + rx.text("Update your documents"), + value="documents", + ), + rx.tabs.content( + rx.text("Edit your personal profile"), + value="settings", + ), + ), + ) + +TabsContent: | + lambda **props: rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Account", value="account"), + rx.tabs.trigger("Documents", value="documents"), + rx.tabs.trigger("Settings", value="settings"), + ), + rx.box( + rx.tabs.content( + rx.text("Make changes to your account"), + value="account", + **props, + ), + rx.tabs.content( + rx.text("Update your documents"), + value="documents", + **props, + ), + rx.tabs.content( + rx.text("Edit your personal profile"), + value="settings", + **props, + ), + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Tabs + +Tabs are a set of layered sections of content—known as tab panels that are displayed one at a time. +They facilitate the organization and navigation between sets of content that share a connection and exist at a similar level of hierarchy. + +## Basic Example + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), +) + +``` + +The `tabs` component is made up of a `rx.tabs.root` which groups `rx.tabs.list` and `rx.tabs.content` parts. + +## Styling + +### Default value + +We use the `default_value` prop to set a default active tab, this will select the specified tab by default. + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), + default_value="tab2", +) +``` + +### Orientation + +We use `orientation` prop to set the orientation of the tabs component to `vertical` or `horizontal`. By default, the orientation +will be set to `horizontal`. Setting this value will change both the visual orientation and the functional orientation. + +```md alert info +The functional orientation means for `vertical`, the `up` and `down` arrow keys moves focus between the next or previous tab, +while for `horizontal`, the `left` and `right` arrow keys moves focus between tabs. +``` + +```md alert warning +# When using vertical orientation, make sure to assign a tabs.content for each trigger. + +Defining triggers without content will result in a visual bug where the width of the triggers list isn't constant. +``` + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), + default_value="tab1", + orientation="vertical", +) +``` + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), + default_value="tab1", + orientation="horizontal", +) +``` + +### Value + +We use the `value` prop to specify the controlled value of the tab that we want to activate. This property should be used in conjunction with the `on_change` event argument. + +```python demo exec +class TabsState(rx.State): + """The app state.""" + + value = "tab1" + tab_selected = "" + + @rx.event + def change_value(self, val): + self.tab_selected = f"{val} clicked!" + self.value = val + + +def index() -> rx.Component: + return rx.container( + rx.flex( + rx.text(f"{TabsState.value} clicked !"), + rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2"), + ), + rx.tabs.content( + rx.text("items on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("items on tab 2"), + value="tab2", + ), + default_value="tab1", + value=TabsState.value, + on_change=lambda x: TabsState.change_value(x), + ), + direction="column", + spacing="2", + ), + padding="2em", + font_size="2em", + text_align="center", + ) +``` + +## Tablist + +The Tablist is used to list the respective tabs to the tab component + +## Tab Trigger + +This is the button that activates the tab's associated content. It is typically used in the `Tablist` + +## Styling + +### Value + +We use the `value` prop to assign a unique value that associates the trigger with content. + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2"), + rx.tabs.trigger("Tab 3", value="tab3") + ), +) +``` + +### Disable + +We use the `disabled` prop to disable the tab. + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2"), + rx.tabs.trigger("Tab 3", value="tab3", disabled=True) + ), +) +``` + +## Tabs Content + +Contains the content associated with each trigger. + +## Styling + +### Value + +We use the `value` prop to assign a unique value that associates the content with a trigger. + +```python +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), + default_value="tab1", + orientation="vertical", +) +``` diff --git a/docs/library/dynamic-rendering/auto_scroll.md b/docs/library/dynamic-rendering/auto_scroll.md new file mode 100644 index 00000000000..a2294ca539c --- /dev/null +++ b/docs/library/dynamic-rendering/auto_scroll.md @@ -0,0 +1,70 @@ +```python exec +import reflex as rx +``` + +# Auto Scroll + +The `rx.auto_scroll` component is a div that automatically scrolls to the bottom when new content is added. This is useful for chat interfaces, logs, or any container where new content is dynamically added and you want to ensure the most recent content is visible. + +## Basic Usage + +```python demo exec +import reflex as rx + +class AutoScrollState(rx.State): + messages: list[str] = ["Initial message"] + + def add_message(self): + self.messages.append(f"New message #{len(self.messages) + 1}") + +def auto_scroll_example(): + return rx.vstack( + rx.auto_scroll( + rx.foreach( + AutoScrollState.messages, + lambda message: rx.box( + message, + padding="0.5em", + border_bottom="1px solid #eee", + width="100%" + ) + ), + height="200px", + width="300px", + border="1px solid #ddd", + border_radius="md", + ), + rx.button("Add Message", on_click=AutoScrollState.add_message), + width="300px", + align_items="center", + ) +``` + +The `auto_scroll` component automatically scrolls to show the newest content when it's added. In this example, each time you click "Add Message", a new message is added to the list and the container automatically scrolls to display it. + +## When to Use Auto Scroll + +- **Chat applications**: Keep the chat window scrolled to the most recent messages. +- **Log viewers**: Automatically follow new log entries as they appear. +- **Feed interfaces**: Keep the newest content visible in dynamically updating feeds. + +## Props + +`rx.auto_scroll` is based on the `rx.div` component and inherits all of its props. By default, it sets `overflow="auto"` to enable scrolling. + +Some common props you might use with `auto_scroll`: + +- `height`: Set the height of the scrollable container. +- `width`: Set the width of the scrollable container. +- `padding`: Add padding inside the container. +- `border`: Add a border around the container. +- `border_radius`: Round the corners of the container. + +## How It Works + +The component tracks when new content is added and maintains the scroll position in two scenarios: + +1. When the user is already near the bottom of the content (within 50 pixels), it will scroll to the bottom when new content is added. +2. When the container didn't have a scrollbar before but does now (due to new content), it will automatically scroll to the bottom. + +This behavior ensures that users can scroll up to view older content without being forced back to the bottom, while still automatically following new content in most cases. diff --git a/docs/library/dynamic-rendering/cond.md b/docs/library/dynamic-rendering/cond.md new file mode 100644 index 00000000000..1fbd4dab8e8 --- /dev/null +++ b/docs/library/dynamic-rendering/cond.md @@ -0,0 +1,161 @@ +```python exec +import reflex as rx +``` + +# Cond + +This component is used to conditionally render components. + +The cond component takes a condition and two components. +If the condition is `True`, the first component is rendered, otherwise the second component is rendered. + +```python demo exec +class CondState(rx.State): + show: bool = True + + @rx.event + def change(self): + self.show = not (self.show) + + +def cond_example(): + return rx.vstack( + rx.button("Toggle", on_click=CondState.change), + rx.cond(CondState.show, rx.text("Text 1", color="blue"), rx.text("Text 2", color="red")), + ) +``` + +The second component is optional and can be omitted. +If it is omitted, nothing is rendered if the condition is `False`. + +```python demo exec +class CondOptionalState(rx.State): + show_optional: bool = True + + @rx.event + def toggle_optional(self): + self.show_optional = not (self.show_optional) + + +def cond_optional_example(): + return rx.vstack( + rx.button("Toggle", on_click=CondOptionalState.toggle_optional), + rx.cond(CondOptionalState.show_optional, rx.text("This text appears when condition is True", color="green")), + rx.text("This text is always visible", color="gray"), + ) +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=6040&end=6463 +# Video: Conditional Rendering +``` + +## Negation + +You can use the logical operator `~` to negate a condition. + +```python +rx.vstack( + rx.button("Toggle", on_click=CondState.change), + rx.cond(CondState.show, rx.text("Text 1", color="blue"), rx.text("Text 2", color="red")), + rx.cond(~CondState.show, rx.text("Text 1", color="blue"), rx.text("Text 2", color="red")), +) +``` + +## Multiple Conditions + +It is also possible to make up complex conditions using the `logical or` (|) and `logical and` (&) operators. + +Here we have an example using the var operators `>=`, `<=`, `&`. We define a condition that if a person has an age between 18 and 65, including those ages, they are able to work, otherwise they cannot. + +We could equally use the operator `|` to represent a `logical or` in one of our conditions. + +```python demo exec +import random + +class CondComplexState(rx.State): + age: int = 19 + + @rx.event + def change(self): + self.age = random.randint(0, 100) + + +def cond_complex_example(): + return rx.vstack( + rx.button("Toggle", on_click=CondComplexState.change), + rx.text(f"Age: {CondComplexState.age}"), + rx.cond( + (CondComplexState.age >= 18) & (CondComplexState.age <=65), + rx.text("You can work!", color="green"), + rx.text("You cannot work!", color="red"), + ), + ) + +``` + +## Nested Conditional + +We can also nest `cond` components within each other to create more complex logic. In python we can have an `if` statement that then has several `elif` statements before finishing with an `else`. This is also possible in reflex using nested `cond` components. In this example we check whether a number is positive, negative or zero. + +Here is the python logic using `if` statements: + +```python +number = 0 + +if number > 0: + print("Positive number") + +elif number == 0: + print('Zero') +else: + print('Negative number') +``` + +This reflex code that is logically identical: + +```python demo exec +import random + + +class NestedState(rx.State): + + num: int = 0 + + def change(self): + self.num = random.randint(-10, 10) + + +def cond_nested_example(): + return rx.vstack( + rx.button("Toggle", on_click=NestedState.change), + rx.cond( + NestedState.num > 0, + rx.text(f"{NestedState.num} is Positive!", color="orange"), + rx.cond( + NestedState.num == 0, + rx.text(f"{NestedState.num} is Zero!", color="blue"), + rx.text(f"{NestedState.num} is Negative!", color="red"), + ) + ), + ) + +``` + +Here is a more advanced example where we have three numbers and we are checking which of the three is the largest. If any two of them are equal then we return that `Some of the numbers are equal!`. + +The reflex code that follows is logically identical to doing the following in python: + +```python +a = 8 +b = 10 +c = 2 + +if((a>b and a>c) and (a != b and a != c)): + print(a, " is the largest!") +elif((b>a and b>c) and (b != a and b != c)): + print(b, " is the largest!") +elif((c>a and c>b) and (c != a and c != b)): + print(c, " is the largest!") +else: + print("Some of the numbers are equal!") +``` diff --git a/docs/library/dynamic-rendering/foreach.md b/docs/library/dynamic-rendering/foreach.md new file mode 100644 index 00000000000..7b107e86c3e --- /dev/null +++ b/docs/library/dynamic-rendering/foreach.md @@ -0,0 +1,199 @@ +```python exec +import reflex as rx +``` + +# Foreach + +The `rx.foreach` component takes an iterable(list, tuple or dict) and a function that renders each item in the list. +This is useful for dynamically rendering a list of items defined in a state. + +```md alert warning +# `rx.foreach` is specialized for usecases where the iterable is defined in a state var. + +For an iterable where the content doesn't change at runtime, i.e a constant, using a list/dict comprehension instead of `rx.foreach` is preferred. +``` + +```python demo exec +from typing import List +class ForeachState(rx.State): + color: List[str] = ["red", "green", "blue", "yellow", "orange", "purple"] + +def colored_box(color: str): + return rx.box( + rx.text(color), + bg=color + ) + +def foreach_example(): + return rx.grid( + rx.foreach( + ForeachState.color, + colored_box + ), + columns="2", + ) +``` + +The function can also take an index as a second argument. + +```python demo exec +def colored_box_index(color: str, index: int): + return rx.box( + rx.text(index), + bg=color + ) + +def foreach_example_index(): + return rx.grid( + rx.foreach( + ForeachState.color, + lambda color, index: colored_box_index(color, index) + ), + columns="2", + ) +``` + +Nested foreach components can be used to render nested lists. + +When indexing into a nested list, it's important to declare the list's type as Reflex requires it for type checking. +This ensures that any potential frontend JS errors are caught before the user can encounter them. + +```python demo exec +from typing import List + +class NestedForeachState(rx.State): + numbers: List[List[str]] = [["1", "2", "3"], ["4", "5", "6"], ["7", "8", "9"]] + +def display_row(row): + return rx.hstack( + rx.foreach( + row, + lambda item: rx.box( + item, + border="1px solid black", + padding="0.5em", + ) + ), + ) + +def nested_foreach_example(): + return rx.vstack( + rx.foreach( + NestedForeachState.numbers, + display_row + ) + ) +``` + +Below is a more complex example of foreach within a todo list. + +```python demo exec +from typing import List +class ListState(rx.State): + items: List[str] = ["Write Code", "Sleep", "Have Fun"] + new_item: str + + @rx.event + def set_new_item(self, new_item: str): + self.new_item = new_item + + @rx.event + def add_item(self): + self.items += [self.new_item] + + def finish_item(self, item: str): + self.items = [i for i in self.items if i != item] + +def get_item(item): + return rx.list.item( + rx.hstack( + rx.button( + on_click=lambda: ListState.finish_item(item), + height="1.5em", + background_color="white", + border="1px solid blue", + ), + rx.text(item, font_size="1.25em"), + ), + ) + +def todo_example(): + return rx.vstack( + rx.heading("Todos"), + rx.input(on_blur=ListState.set_new_item, placeholder="Add a todo...", bg = "white"), + rx.button("Add", on_click=ListState.add_item, bg = "white"), + rx.divider(), + rx.list.ordered( + rx.foreach( + ListState.items, + get_item, + ), + ), + bg = "#ededed", + padding = "1em", + border_radius = "0.5em", + shadow = "lg" + ) +``` + +## Dictionaries + +Items in a dictionary can be accessed as list of key-value pairs. +Using the color example, we can slightly modify the code to use dicts as shown below. + +```python demo exec +from typing import List +class SimpleDictForeachState(rx.State): + color_chart: dict[int, str] = { + 1 : "blue", + 2: "red", + 3: "green" + } + +def display_color(color: List): + # color is presented as a list key-value pair([1, "blue"],[2, "red"], [3, "green"]) + return rx.box(rx.text(color[0]), bg=color[1]) + + +def foreach_dict_example(): + return rx.grid( + rx.foreach( + SimpleDictForeachState.color_chart, + display_color + ), + columns = "2" + ) +``` + +Now let's show a more complex example with dicts using the color example. +Assuming we want to display a dictionary of secondary colors as keys and their constituent primary colors as values, we can modify the code as below: + +```python demo exec +from typing import List, Dict +class ComplexDictForeachState(rx.State): + color_chart: Dict[str, List[str]] = { + "purple": ["red", "blue"], + "orange": ["yellow", "red"], + "green": ["blue", "yellow"] + } + +def display_colors(color: List): + return rx.vstack( + rx.text(color[0], color=color[0]), + rx.hstack( + rx.foreach( + color[1], lambda x: rx.box(rx.text(x, color="black"), bg=x) + ) + + ) + ) + +def foreach_complex_dict_example(): + return rx.grid( + rx.foreach( + ComplexDictForeachState.color_chart, + display_colors + ), + columns="2" + ) +``` diff --git a/docs/library/dynamic-rendering/match.md b/docs/library/dynamic-rendering/match.md new file mode 100644 index 00000000000..d258d68d64b --- /dev/null +++ b/docs/library/dynamic-rendering/match.md @@ -0,0 +1,295 @@ +```python exec +import reflex as rx +``` + +# Match + +The `rx.match` feature in Reflex serves as a powerful alternative to `rx.cond` for handling conditional statements. +While `rx.cond` excels at conditionally rendering two components based on a single condition, +`rx.match` extends this functionality by allowing developers to handle multiple conditions and their associated components. +This feature is especially valuable when dealing with intricate conditional logic or nested scenarios, +where the limitations of `rx.cond` might lead to less readable and more complex code. + +With `rx.match`, developers can not only handle multiple conditions but also perform structural pattern matching, +making it a versatile tool for managing various scenarios in Reflex applications. + +## Basic Usage + +The `rx.match` function provides a clear and expressive syntax for handling multiple +conditions and their corresponding components: + +```python +rx.match( + condition, + (case_1, component_1), + (case_2, component_2), + ... + default_component, +) + +``` + +- `condition`: The value to match against. +- `(case_i, component_i)`: A Tuple of matching cases and their corresponding return components. +- `default_component`: A special case for the default component when the condition isn't matched by any of the match cases. + +Example + +```python demo exec +from typing import List + +import reflex as rx + + +class MatchState(rx.State): + cat_breed: str = "" + animal_options: List[str] = ["persian", "siamese", "maine coon", "ragdoll", "pug", "corgi"] + + @rx.event + def set_cat_breed(self, breed: str): + self.cat_breed = breed + +def match_demo(): + return rx.flex( + rx.match( + MatchState.cat_breed, + ("persian", rx.text("Persian cat selected.")), + ("siamese", rx.text("Siamese cat selected.")), + ("maine coon", rx.text("Maine Coon cat selected.")), + ("ragdoll", rx.text("Ragdoll cat selected.")), + rx.text("Unknown cat breed selected.") + ), + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.foreach(MatchState.animal_options, lambda x: rx.select.item(x, value=x)) + ), + ), + value=MatchState.cat_breed, + on_change=MatchState.set_cat_breed, + + ), + direction= "column", + gap= "2" + ) +``` + +## Default Case + +The default case in `rx.match` serves as a universal handler for scenarios where none of +the specified match cases aligns with the given match condition. Here are key considerations +when working with the default case: + +- **Placement in the Match Function**: The default case must be the last non-tuple argument in the `rx.match` component. + All match cases should be enclosed in tuples; any non-tuple value is automatically treated as the default case. For example: + +```python +rx.match( + MatchState.cat_breed, + ("persian", rx.text("persian cat selected")), + rx.text("Unknown cat breed selected."), + ("siamese", rx.text("siamese cat selected")), + ) +``` + +The above code snippet will result in an error due to the misplaced default case. + +- **Single Default Case**: Only one default case is allowed in the `rx.match` component. + Attempting to specify multiple default cases will lead to an error. For instance: + +```python +rx.match( + MatchState.cat_breed, + ("persian", rx.text("persian cat selected")), + ("siamese", rx.text("siamese cat selected")), + rx.text("Unknown cat breed selected."), + rx.text("Another unknown cat breed selected.") + ) +``` + +- **Optional Default Case for Component Return Values**: If the match cases in a `rx.match` component + return components, the default case can be optional. In this scenario, if a default case is + not provided, `rx.fragment` will be implicitly assigned as the default. For example: + +```python +rx.match( + MatchState.cat_breed, + ("persian", rx.text("persian cat selected")), + ("siamese", rx.text("siamese cat selected")), + ) +``` + +In this case, `rx.fragment` is the default case. However, not providing a default case for non-component +return values will result in an error: + +```python +rx.match( + MatchState.cat_breed, + ("persian", "persian cat selected"), + ("siamese", "siamese cat selected"), + ) +``` + +The above code snippet will result in an error as a default case must be explicitly +provided in this scenario. + +## Handling Multiple Conditions + +`rx.match` excels in efficiently managing multiple conditions and their corresponding components, +providing a cleaner and more readable alternative compared to nested `rx.cond` structures. + +Consider the following example: + +```python demo exec +from typing import List + +import reflex as rx + + +class MultiMatchState(rx.State): + animal_breed: str = "" + animal_options: List[str] = ["persian", "siamese", "maine coon", "pug", "corgi", "mustang", "rahvan", "football", "golf"] + + @rx.event + def set_animal_breed(self, breed: str): + self.animal_breed = breed + +def multi_match_demo(): + return rx.flex( + rx.match( + MultiMatchState.animal_breed, + ("persian", "siamese", "maine coon", rx.text("Breeds of cats.")), + ("pug", "corgi", rx.text("Breeds of dogs.")), + ("mustang", "rahvan", rx.text("Breeds of horses.")), + rx.text("Unknown animal breed") + ), + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.foreach(MultiMatchState.animal_options, lambda x: rx.select.item(x, value=x)) + ), + ), + value=MultiMatchState.animal_breed, + on_change=MultiMatchState.set_animal_breed, + + ), + direction= "column", + gap= "2" + ) +``` + +In a match case tuple, you can specify multiple conditions. The last value of the match case +tuple is automatically considered as the return value. It's important to note that a match case +tuple should contain a minimum of two elements: a match case and a return value. +The following code snippet will result in an error: + +```python +rx.match( + MatchState.cat_breed, + ("persian",), + ("maine coon", rx.text("Maine Coon cat selected")), + ) +``` + +## Usage as Props + +Similar to `rx.cond`, `rx.match` can be used as prop values, allowing dynamic behavior for UI components: + +```python demo exec +import reflex as rx + + +class MatchPropState(rx.State): + value: int = 0 + + @rx.event + def incr(self): + self.value += 1 + + @rx.event + def decr(self): + self.value -= 1 + + +def match_prop_demo_(): + return rx.flex( + rx.button("decrement", on_click=MatchPropState.decr, background_color="red"), + rx.badge( + MatchPropState.value, + color_scheme= rx.match( + MatchPropState.value, + (1, "red"), + (2, "blue"), + (6, "purple"), + (10, "orange"), + "green" + ), + size="2", + ), + rx.button("increment", on_click=MatchPropState.incr), + align_items="center", + direction= "row", + gap= "3" + ) +``` + +In the example above, the background color property of the box component containing `State.value` changes to red when +`state.value` is 1, blue when its 5, green when its 5, orange when its 15 and black for any other value. + +The example below also shows handling multiple conditions with the match component as props. + +```python demo exec +import reflex as rx + + +class MatchMultiPropState(rx.State): + value: int = 0 + + @rx.event + def incr(self): + self.value += 1 + + @rx.event + def decr(self): + self.value -= 1 + + +def match_multi_prop_demo_(): + return rx.flex( + rx.button("decrement", on_click=MatchMultiPropState.decr, background_color="red"), + rx.badge( + MatchMultiPropState.value, + color_scheme= rx.match( + MatchMultiPropState.value, + (1, 3, 9, "red"), + (2, 4, 5, "blue"), + (6, 8, 12, "purple"), + (10, 15, 20, 25, "orange"), + "green" + ), + size="2", + ), + rx.button("increment", on_click=MatchMultiPropState.incr), + align_items="center", + direction= "row", + gap= "3" + ) +``` + +```md alert warning +# Usage with Structural Pattern Matching + +The `rx.match` component is designed for structural pattern matching. If the value of your match condition evaluates to a boolean (True or False), it is recommended to use `rx.cond` instead. + +Consider the following example, which is more suitable for `rx.cond`:\* +``` + +```python +rx.cond( + MatchPropState.value == 10, + "true value", + "false value" +) +``` diff --git a/docs/library/forms/button.md b/docs/library/forms/button.md new file mode 100644 index 00000000000..7793fe81e99 --- /dev/null +++ b/docs/library/forms/button.md @@ -0,0 +1,63 @@ +--- +components: + - rx.button + +Button: | + lambda **props: rx.button("Basic Button", **props) +--- + +```python exec +import reflex as rx +``` + +# Button + +Buttons are essential elements in your application's user interface that users can click to trigger events. + +## Basic Example + +The `on_click` trigger is called when the button is clicked. + +```python demo exec +class CountState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def decrement(self): + self.count -= 1 + +def counter(): + return rx.flex( + rx.button( + "Decrement", + color_scheme="red", + on_click=CountState.decrement, + ), + rx.heading(CountState.count), + rx.button( + "Increment", + color_scheme="grass", + on_click=CountState.increment, + ), + spacing="3", + ) +``` + +### Loading and Disabled + +The `loading` prop is used to indicate that the action triggered by the button is currently in progress. When set to `True`, the button displays a loading spinner, providing visual feedback to the user that the action is being processed. This also prevents multiple clicks while the button is in the loading state. By default, `loading` is set to `False`. + +The `disabled` prop also prevents the button from being but does not provide a spinner. + +```python demo +rx.flex( + rx.button("Regular"), + rx.button("Loading", loading=True), + rx.button("Disabled", disabled=True), + spacing="2", +) +``` diff --git a/docs/library/forms/checkbox.md b/docs/library/forms/checkbox.md new file mode 100644 index 00000000000..ef00cff7026 --- /dev/null +++ b/docs/library/forms/checkbox.md @@ -0,0 +1,73 @@ +--- +components: + - rx.checkbox + +HighLevelCheckbox: | + lambda **props: rx.checkbox("Basic Checkbox", **props) +--- + +```python exec +import reflex as rx +``` + +# Checkbox + +## Basic Example + +The `on_change` trigger is called when the `checkbox` is clicked. + +```python demo exec +class CheckboxState(rx.State): + checked: bool = False + + @rx.event + def set_checked(self, value: bool): + self.checked = value + +def checkbox_example(): + return rx.vstack( + rx.heading(CheckboxState.checked), + rx.checkbox(on_change=CheckboxState.set_checked), + ) +``` + +The `input` prop is used to set the `checkbox` as a controlled component. + +```python demo exec +class FormCheckboxState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + print(form_data) + self.form_data = form_data + + +def form_checkbox_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.hstack( + rx.checkbox( + name="checkbox", + label="Accept terms and conditions", + ), + rx.button("Submit", type="submit"), + width="100%", + ), + on_submit=FormCheckboxState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormCheckboxState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` diff --git a/docs/library/forms/form-ll.md b/docs/library/forms/form-ll.md new file mode 100644 index 00000000000..8e5dfdd6f4b --- /dev/null +++ b/docs/library/forms/form-ll.md @@ -0,0 +1,464 @@ +--- +components: + - rx.form.root + - rx.form.field + - rx.form.control + - rx.form.label + - rx.form.message + - rx.form.submit + +FormRoot: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + **props, + ) + +FormField: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", match="typeMismatch"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + **props, + ), + reset_on_submit=True, + ) + +FormMessage: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", **props,), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + on_submit=lambda form_data: rx.window_alert(form_data.to_string()), + reset_on_submit=True, + ) +--- + +# Form + +```python exec +import reflex as rx +import reflex.components.radix.primitives as rdxp +``` + +```md warning info +# Low Level Form is Experimental + +Please use the High Level Form for now for production. +``` + +Forms are used to collect information from your users. Forms group the inputs and submit them together. + +## Basic Example + +Here is an example of a form collecting an email address, with built-in validation on the email. If email entered is invalid, the form cannot be submitted. Note that the `form.submit` button is not automatically disabled. It is still clickable, but does not submit the form data. After successful submission, an alert window shows up and the form is cleared. There are a few `flex` containers used in the example to control the layout of the form components. + +```python demo +rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", match="typeMismatch"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + on_submit=lambda form_data: rx.window_alert(form_data.to_string()), + reset_on_submit=True, +) +``` + +In this example, the `rx.input` has an attribute `type="email"` and the `form.message` has the attribute `match="typeMismatch"`. Those are required for the form to validate the input by its type. The prop `as_child="True"` is required when using other components to construct a Form component. This example has used `rx.input` to construct the Form Control and `button` the Form Submit. + +## Form Anatomy + +```python eval +rx._x.code_block( + """form.root( + form.field( + form.label(...), + form.control(...), + form.message(...), + ), + form.submit(...), +)""", + language="python", +) +``` + +A Form Root (`form.root`) contains all the parts of a form. The Form Field (`form.field`), Form Submit (`form.submit`), etc should all be inside a Form Root. A Form Field can contain a Form Label (`form.label`), a Form Control (`form.control`), and a Form Message (`form.message`). A Form Label is a label element. A Form Control is where the user enters the input or makes selections. By default, the Form Control is a input. Using other form components to construct the Form Control is supported. To do that, set the prop `as_child=True` on the Form Control. + +```md alert info +The current version of Radix Forms does not support composing **Form Control** with other Radix form primitives such as **Checkbox**, **Select**, etc. +``` + +The Form Message is a validation message which is automatically wired (functionality and accessibility). When the Form Control determines the input is invalid, the Form Message is shown. The `match` prop is to enable [client side validation](#client-side-validation). To perform [server side validation](#server-side-validation), **both** the `force_match` prop of the Form Control and the `server_invalid` prop of the Form Field are set together. + +The Form Submit is by default a button that submits the form. To use another button component as a Form Submit, include that button as a child inside `form.submit` and set the prop `as_child=True`. + +The `on_submit` prop of the Form Root accepts an event handler. It is called with the submitted form data dictionary. To clear the form after submission, set the `reset_on_submit=True` prop. + +## Data Submission + +As previously mentioned, the various pieces of data in the form are submitted together as a dictionary. The form control or the input components must have the `name` attribute. This `name` is the key to get the value from the form data dictionary. If no validation is needed, the form type components such as Checkbox, Radio Groups, TextArea can be included directly under the Form Root instead of inside a Form Control. + +```python demo exec +import reflex as rx +import reflex.components.radix.primitives as rdxp + +class RadixFormSubmissionState(rx.State): + form_data: dict + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + @rx.var + def form_data_keys(self) -> list: + return list(self.form_data.keys()) + + @rx.var + def form_data_values(self) -> list: + return list(self.form_data.values()) + + +def radix_form_submission_example(): + return rx.flex( + rx.form.root( + rx.flex( + rx.flex( + rx.checkbox( + default_checked=True, + name="box1", + ), + rx.text("box1 checkbox"), + direction="row", + spacing="2", + align="center", + ), + rx.radio.root( + rx.flex( + rx.radio.item(value="1"), + "1", + direction="row", + align="center", + spacing="2", + ), + rx.flex( + rx.radio.item(value="2"), + "2", + direction="row", + align="center", + spacing="2", + ), + rx.flex( + rx.radio.item(value="3"), + "3", + direction="row", + align="center", + spacing="2", + ), + default_value="1", + name="box2", + ), + rx.input( + placeholder="box3 textfield input", + name="box3", + ), + rx.select.root( + rx.select.trigger( + placeholder="box4 select", + ), + rx.select.content( + rx.select.group( + rx.select.item( + "Orange", + value="orange" + ), + rx.select.item( + "Apple", + value="apple" + ), + ), + ), + name="box4", + ), + rx.flex( + rx.switch( + default_checked=True, + name="box5", + ), + "box5 switch", + spacing="2", + align="center", + direction="row", + ), + rx.flex( + rx.slider( + default_value=[40], + width="100%", + name="box6", + ), + "box6 slider", + direction="row", + spacing="2", + align="center", + ), + rx.text_area( + placeholder="Enter for box7 textarea", + name="box7", + ), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="4", + ), + on_submit=RadixFormSubmissionState.handle_submit, + ), + rx.divider(size="4"), + rx.text( + "Results", + weight="bold", + ), + rx.foreach(RadixFormSubmissionState.form_data_keys, + lambda key, idx: rx.text(key, " : ", RadixFormSubmissionState.form_data_values[idx]) + ), + direction="column", + spacing="4", + ) +``` + +## Validation + +Server side validation is done through **Computed Vars** on the State. The **Var** should return a boolean flag indicating when input is invalid. Set that **Var** on both the `server_invalid` prop of `form.field` and the `force_match` prop of `form.message`. There is an example how to do that in the [Final Example](#final-example). + +## Final Example + +The final example shows a form that collects username and email during sign-up and validates them using server side validation. When server side validation fails, messages are displayed in red to show what is not accepted in the form, and the submit button is disabled. After submission, the collected form data is displayed in texts below the form and the form is cleared. + +```python demo exec +import re +import reflex as rx +import reflex.components.radix.primitives as rdxp + +class RadixFormState(rx.State): + # These track the user input real time for validation + user_entered_username: str + user_entered_email: str + + # These are the submitted data + username: str + email: str + + mock_username_db: list[str] = ["reflex", "admin"] + + # Add explicit setters + def set_user_entered_username(self, value: str): + self.user_entered_username = value + + def set_user_entered_email(self, value: str): + self.user_entered_email = value + + def set_username(self, value: str): + self.username = value + + def set_email(self, value: str): + self.email = value + + @rx.var + def invalid_email(self) -> bool: + return not re.match(r"[^@]+@[^@]+\.[^@]+", self.user_entered_email) + + @rx.var + def username_empty(self) -> bool: + return not self.user_entered_username.strip() + + @rx.var + def username_is_taken(self) -> bool: + return self.user_entered_username in self.mock_username_db + + @rx.var + def input_invalid(self) -> bool: + return self.invalid_email or self.username_is_taken or self.username_empty + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.username = form_data.get("username") + self.email = form_data.get("email") + +def radix_form_example(): + return rx.flex( + rx.form.root( + rx.flex( + rx.form.field( + rx.flex( + rx.form.label("Username"), + rx.form.control( + rx.input( + placeholder="Username", + # workaround: `name` seems to be required when on_change is set + on_change=RadixFormState.set_user_entered_username, + name="username", + ), + as_child=True, + ), + # server side validation message can be displayed inside a rx.cond + rx.cond( + RadixFormState.username_empty, + rx.form.message( + "Username cannot be empty", + color="var(--red-11)", + ), + ), + # server side validation message can be displayed by `force_match` prop + rx.form.message( + "Username already taken", + # this is a workaround: + # `force_match` does not work without `match` + # This case does not want client side validation + # and intentionally not set `required` on the input + # so "valueMissing" is always false + match="valueMissing", + force_match=RadixFormState.username_is_taken, + color="var(--red-11)", + ), + direction="column", + spacing="2", + align="stretch", + ), + name="username", + server_invalid=RadixFormState.username_is_taken, + ), + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + on_change=RadixFormState.set_user_entered_email, + name="email", + ), + as_child=True, + ), + rx.form.message( + "A valid Email is required", + match="valueMissing", + force_match=RadixFormState.invalid_email, + color="var(--red-11)", + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + server_invalid=RadixFormState.invalid_email, + ), + rx.form.submit( + rx.button( + "Submit", + disabled=RadixFormState.input_invalid, + ), + as_child=True, + ), + direction="column", + spacing="4", + width="25em", + ), + on_submit=RadixFormState.handle_submit, + reset_on_submit=True, + ), + rx.divider(size="4"), + rx.text( + "Username submitted: ", + rx.text( + RadixFormState.username, + weight="bold", + color="var(--accent-11)", + ), + ), + rx.text( + "Email submitted: ", + rx.text( + RadixFormState.email, + weight="bold", + color="var(--accent-11)", + ), + ), + direction="column", + spacing="4", + ) +``` diff --git a/docs/library/forms/form.md b/docs/library/forms/form.md new file mode 100644 index 00000000000..bc771b7559f --- /dev/null +++ b/docs/library/forms/form.md @@ -0,0 +1,239 @@ +--- +components: + - rx.form + - rx.form.root + - rx.form.field + - rx.form.control + - rx.form.label + - rx.form.message + - rx.form.submit + +FormRoot: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + **props, + ) + +FormField: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", match="typeMismatch"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + **props, + ), + reset_on_submit=True, + ) + +FormLabel: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email", **props,), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", match="typeMismatch"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + ), + reset_on_submit=True, + ) + +FormMessage: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", **props,), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + on_submit=lambda form_data: rx.window_alert(form_data.to_string()), + reset_on_submit=True, + ) +--- + +```python exec +import reflex as rx +``` + +# Form + +Forms are used to collect user input. The `rx.form` component is used to group inputs and submit them together. + +The form component's children can be form controls such as `rx.input`, `rx.checkbox`, `rx.slider`, `rx.textarea`, `rx.radio_group`, `rx.select` or `rx.switch`. The controls should have a `name` attribute that is used to identify the control in the form data. The `on_submit` event trigger submits the form data as a dictionary to the `handle_submit` event handler. + +The form is submitted when the user clicks the submit button or presses enter on the form controls. + +```python demo exec +class FormState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def form_example(): + return rx.vstack( + rx.form( + rx.vstack( + rx.input(placeholder="First Name", name="first_name"), + rx.input(placeholder="Last Name", name="last_name"), + rx.hstack( + rx.checkbox("Checked", name="check"), + rx.switch("Switched", name="switch"), + ), + rx.button("Submit", type="submit"), + ), + on_submit=FormState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.heading("Results"), + rx.text(FormState.form_data.to_string()), + ) +``` + +```md alert warning +# When using the form you must include a button or input with `type='submit'`. +``` + +```md alert info +# Using `name` vs `id`. + +When using the `name` attribute in form controls like `rx.switch`, `rx.radio_group`, and `rx.checkbox`, these controls will only be included in the form data if their values are set (e.g., if the checkbox is checked, the switch is toggled, or a radio option is selected). + +If you need these controls to be passed in the form data even when their values are not set, you can use the `id` attribute instead of name. The id attribute ensures that the control is always included in the submitted form data, regardless of whether its value is set or not. +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=5287&end=6040 +# Video: Forms +``` + +## Dynamic Forms + +Forms can be dynamically created by iterating through state vars using `rx.foreach`. + +This example allows the user to add new fields to the form prior to submit, and all +fields will be included in the form data passed to the `handle_submit` function. + +```python demo exec +class DynamicFormState(rx.State): + form_data: dict = {} + form_fields: list[str] = ["first_name", "last_name", "email"] + + @rx.var(cache=True) + def form_field_placeholders(self) -> list[str]: + return [ + " ".join(w.capitalize() for w in field.split("_")) + for field in self.form_fields + ] + + @rx.event + def add_field(self, form_data: dict): + new_field = form_data.get("new_field") + if not new_field: + return + field_name = new_field.strip().lower().replace(" ", "_") + self.form_fields.append(field_name) + + @rx.event + def handle_submit(self, form_data: dict): + self.form_data = form_data + + +def dynamic_form(): + return rx.vstack( + rx.form( + rx.vstack( + rx.foreach( + DynamicFormState.form_fields, + lambda field, idx: rx.input( + placeholder=DynamicFormState.form_field_placeholders[idx], + name=field, + ), + ), + rx.button("Submit", type="submit"), + ), + on_submit=DynamicFormState.handle_submit, + reset_on_submit=True, + ), + rx.form( + rx.hstack( + rx.input(placeholder="New Field", name="new_field"), + rx.button("+", type="submit"), + ), + on_submit=DynamicFormState.add_field, + reset_on_submit=True, + ), + rx.divider(), + rx.heading("Results"), + rx.text(DynamicFormState.form_data.to_string()), + ) +``` diff --git a/docs/library/forms/input-ll.md b/docs/library/forms/input-ll.md new file mode 100644 index 00000000000..1604614d44b --- /dev/null +++ b/docs/library/forms/input-ll.md @@ -0,0 +1,127 @@ +--- +components: + - rx.input + - rx.input.slot +--- + +```python exec +import reflex as rx +``` + +# Input + +A text field is an input field that users can type into. This component uses Radix's [text field](https://www.radix-ui.com/themes/docs/components/text-field) component. + +## Overview + +The TextField component is used to capture user input and can include an optional slot for buttons and icons. It is based on the
element and supports common margin props. + +## Basic Example + +```python demo +rx.input( + rx.input.slot( + rx.icon(tag="search"), + + ), + placeholder="Search here...", +) +``` + +## Stateful Example with Blur Event + +```python demo exec +class TextfieldBlur1(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, text: str): + self.text = text + +def blur_example1(): + return rx.vstack( + rx.heading(TextfieldBlur1.text), + rx.input( + rx.input.slot( + rx.icon(tag="search"), + ), + placeholder="Search here...", + on_blur=TextfieldBlur1.set_text, + ) + ) +``` + +## Controlled Example + +```python demo exec +class TextfieldControlled1(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, text: str): + self.text = text + +def controlled_example1(): + return rx.vstack( + rx.heading(TextfieldControlled1.text), + rx.input( + rx.input.slot( + rx.icon(tag="search"), + ), + placeholder="Search here...", + value=TextfieldControlled1.text, + on_change=TextfieldControlled1.set_text, + ), + ) +``` + +# Real World Example + +```python demo exec + +def song(title, initials: str, genre: str): + return rx.card(rx.flex( + rx.flex( + rx.avatar(fallback=initials), + rx.flex( + rx.text(title, size="2", weight="bold"), + rx.text(genre, size="1", color_scheme="gray"), + direction="column", + spacing="1", + ), + direction="row", + align_items="left", + spacing="1", + ), + rx.flex( + rx.icon(tag="chevron_right"), + align_items="center", + ), + justify="between", + )) + +def search(): + return rx.card( + rx.flex( + rx.input( + rx.input.slot( + rx.icon(tag="search"), + ), + placeholder="Search songs...", + ), + rx.flex( + song("The Less I Know", "T", "Rock"), + song("Breathe Deeper", "ZB", "Rock"), + song("Let It Happen", "TF", "Rock"), + song("Borderline", "ZB", "Pop"), + song("Lost In Yesterday", "TO", "Rock"), + song("Is It True", "TO", "Rock"), + direction="column", + spacing="1", + ), + direction="column", + spacing="3", + ), + style={"maxWidth": 500}, +) +``` diff --git a/docs/library/forms/input.md b/docs/library/forms/input.md new file mode 100644 index 00000000000..cca93566f4a --- /dev/null +++ b/docs/library/forms/input.md @@ -0,0 +1,163 @@ +--- +components: + - rx.input + - rx.input.slot + +Input: | + lambda **props: rx.input(placeholder="Search the docs", **props) + +TextFieldSlot: | + lambda **props: rx.input( + rx.input.slot( + rx.icon(tag="search", height="16", width="16"), + **props, + ), + placeholder="Search the docs", + ) +--- + +```python exec +import reflex as rx +``` + +# Input + +The `input` component is an input field that users can type into. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=1517&end=1869 +# Video: Input +``` + +## Basic Example + +The `on_blur` event handler is called when focus has left the `input` for example, it’s called when the user clicks outside of a focused text input. + +```python demo exec +class TextfieldBlur(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, value: str): + self.text = value + +def blur_example(): + return rx.vstack( + rx.heading(TextfieldBlur.text), + rx.input( + placeholder="Search here...", + on_blur=TextfieldBlur.set_text, + ), + ) +``` + +The `on_change` event handler is called when the `value` of `input` has changed. + +```python demo exec +class TextfieldControlled(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, value: str): + self.text = value + +def controlled_example(): + return rx.vstack( + rx.heading(TextfieldControlled.text), + rx.input( + placeholder="Search here...", + value=TextfieldControlled.text, + on_change=TextfieldControlled.set_text, + ), + ) +``` + +Behind the scenes, the input component is implemented as a debounced input to avoid sending individual state updates per character to the backend while the user is still typing. This allows a state variable to directly control the `value` prop from the backend without the user experiencing input lag. + +## Input Types + +The `type` prop controls how the input is rendered (e.g. plain text, password, file picker). + +It accepts the same values as the native HTML `` attribute, such as: + +- `"text"` (default) +- `"password"` +- `"email"` +- `"number"` +- `"file"` +- `"checkbox"` +- `"radio"` +- `"date"` +- `"time"` +- `"url"` +- `"color"` + +and several others. See the [MDN reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types) for the full list. + +```python demo +rx.vstack( + rx.input(placeholder="Username", type="text"), + rx.input(placeholder="Password", type="password"), + rx.input(type="date"), +) +``` + +## Submitting a form using input + +The `name` prop is needed to submit with its owning form as part of a name/value pair. + +When the `required` prop is `True`, it indicates that the user must input text before the owning form can be submitted. + +The `type` is set here to `password`. The element is presented as a one-line plain text editor control in which the text is obscured so that it cannot be read. The `type` prop can take any value of `email`, `file`, `password`, `text` and several others. Learn more [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + +```python demo exec +class FormInputState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def form_input1(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.hstack( + rx.input( + name="input", + placeholder="Enter text...", + type="text", + required=True, + ), + rx.button("Submit", type="submit"), + width="100%", + ), + on_submit=FormInputState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormInputState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` + +To learn more about how to use forms in the [Form](/docs/library/forms/form) docs. + +## Setting a value without using a State var + +Set the value of the specified reference element, without needing to link it up to a State var. This is an alternate way to modify the value of the `input`. + +```python demo +rx.hstack( + rx.input(id="input1"), + rx.button("Erase", on_click=rx.set_value("input1", "")), +) +``` diff --git a/docs/library/forms/radio_group.md b/docs/library/forms/radio_group.md new file mode 100644 index 00000000000..593f2d4da4e --- /dev/null +++ b/docs/library/forms/radio_group.md @@ -0,0 +1,100 @@ +--- +components: + - rx.radio_group + - rx.radio_group.root + - rx.radio_group.item + +HighLevelRadioGroup: | + lambda **props: rx.radio_group(["1", "2", "3", "4", "5"], **props) + +RadioGroupRoot: | + lambda **props: rx.radio_group.root( + rx.radio_group.item(value="1"), + rx.radio_group.item(value="2"), + rx.radio_group.item(value="3"), + rx.radio_group.item(value="4"), + rx.radio_group.item(value="5"), + **props + ) + +RadioGroupItem: | + lambda **props: rx.radio_group.root( + rx.radio_group.item(value="1", **props), + rx.radio_group.item(value="2", **props), + rx.radio_group.item(value="3",), + rx.radio_group.item(value="4",), + rx.radio_group.item(value="5",), + ) +--- + +```python exec +import reflex as rx +``` + +# Radio Group + +A set of interactive radio buttons where only one can be selected at a time. + +## Basic example + +```python demo exec +class RadioGroupState(rx.State): + item: str = "No Selection" + + @rx.event + def set_item(self, item: str): + self.item = item + +def radio_group_state_example(): + return rx.vstack( + rx.badge(RadioGroupState.item, color_scheme="green"), + rx.radio(["1", "2", "3"], on_change=RadioGroupState.set_item, direction="row"), + ) +``` + +## Submitting a form using Radio Group + +The `name` prop is used to name the group. It is submitted with its owning form as part of a name/value pair. + +When the `required` prop is `True`, it indicates that the user must check a radio item before the owning form can be submitted. + +```python demo exec +class FormRadioState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def radio_form_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.vstack( + rx.radio_group( + ["Option 1", "Option 2", "Option 3"], + name="radio_choice", + direction="row", + ), + rx.button("Submit", type="submit"), + width="100%", + spacing="4", + ), + on_submit=FormRadioState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormRadioState.form_data.to_string()), + ), + align_items="left", + width="100%", + spacing="4", + ), + width="50%", + ) +``` diff --git a/docs/library/forms/select-ll.md b/docs/library/forms/select-ll.md new file mode 100644 index 00000000000..8d279842b72 --- /dev/null +++ b/docs/library/forms/select-ll.md @@ -0,0 +1,303 @@ +--- +components: + - rx.select + - rx.select.root + - rx.select.trigger + - rx.select.content + - rx.select.group + - rx.select.item + - rx.select.label + - rx.select.separator +--- + +```python exec +import random +import reflex as rx +import reflex.components.radix.primitives as rdxp +``` + +# Select + +Displays a list of options for the user to pick from, triggered by a button. + +## Basic Example + +```python demo +rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.label("Fruits"), + rx.select.item("Orange", value="orange"), + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape", disabled=True), + ), + rx.select.separator(), + rx.select.group( + rx.select.label("Vegetables"), + rx.select.item("Carrot", value="carrot"), + rx.select.item("Potato", value="potato"), + ), + ), + default_value="apple", +) +``` + +## Usage + +## Disabling + +It is possible to disable individual items in a `select` using the `disabled` prop associated with the `rx.select.item`. + +```python demo +rx.select.root( + rx.select.trigger(placeholder="Select a Fruit"), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape", disabled=True), + rx.select.item("Pineapple", value="pineapple"), + ), + ), +) +``` + +To prevent the user from interacting with select entirely, set the `disabled` prop to `True` on the `rx.select.root` component. + +```python demo +rx.select.root( + rx.select.trigger(placeholder="This is Disabled"), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + ), + disabled=True, +) +``` + +## Setting Defaults + +It is possible to set several default values when constructing a `select`. + +The `placeholder` prop in the `rx.select.trigger` specifies the content that will be rendered when `value` or `default_value` is empty or not set. + +```python demo +rx.select.root( + rx.select.trigger(placeholder="pick a fruit"), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + ), +) +``` + +The `default_value` in the `rx.select.root` specifies the value of the `select` when initially rendered. +The `default_value` should correspond to the `value` of a child `rx.select.item`. + +```python demo +rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + ), + default_value="apple", +) +``` + +## Fully controlled + +The `on_change` event trigger is fired when the value of the select changes. +In this example the `rx.select_root` `value` prop specifies which item is selected, and this +can also be controlled using state and a button without direct interaction with the select component. + +```python demo exec +class SelectState2(rx.State): + + values: list[str] = ["apple", "grape", "pear"] + + value: str = "" + + def set_value(self, value: str): + self.value = value + + @rx.event + def choose_randomly(self): + """Change the select value var.""" + original_value = self.value + while self.value == original_value: + self.value = random.choice(self.values) + + +def select_example2(): + return rx.vstack( + rx.select.root( + rx.select.trigger(placeholder="No Selection"), + rx.select.content( + rx.select.group( + rx.foreach(SelectState2.values, lambda x: rx.select.item(x, value=x)) + ), + ), + value=SelectState2.value, + on_change=SelectState2.set_value, + + ), + rx.button("Choose Randomly", on_click=SelectState2.choose_randomly), + rx.button("Reset", on_click=SelectState2.set_value("")), + ) +``` + +The `open` prop and `on_open_change` event trigger work similarly to `value` and `on_change` to control the open state of the select. +If `on_open_change` handler does not alter the `open` prop, the select will not be able to be opened or closed by clicking on the +`select_trigger`. + +```python demo exec +class SelectState8(rx.State): + is_open: bool = False + + @rx.event + def set_is_open(self, value: bool): + self.is_open = value + +def select_example8(): + return rx.flex( + rx.select.root( + rx.select.trigger(placeholder="No Selection"), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + ), + open=SelectState8.is_open, + on_open_change=SelectState8.set_is_open, + ), + rx.button("Toggle", on_click=SelectState8.set_is_open(~SelectState8.is_open)), + spacing="2", + ) +``` + +### Submitting a Form with Select + +When a select is part of a form, the `name` prop of the `rx.select.root` sets the key that will be submitted with the form data. + +The `value` prop of `rx.select.item` provides the value to be associated with the `name` key when the form is submitted with that item selected. + +When the `required` prop of the `rx.select.root` is `True`, it indicates that the user must select a value before the form may be submitted. + +```python demo exec +class FormSelectState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def form_select(): + return rx.flex( + rx.form.root( + rx.flex( + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.label("Fruits"), + rx.select.item("Orange", value="orange"), + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + rx.select.separator(), + rx.select.group( + rx.select.label("Vegetables"), + rx.select.item("Carrot", value="carrot"), + rx.select.item("Potato", value="potato"), + ), + ), + default_value="apple", + name="select", + ), + rx.button("Submit"), + width="100%", + direction="column", + spacing="2", + ), + on_submit=FormSelectState.handle_submit, + reset_on_submit=True, + ), + rx.divider(size="4"), + rx.heading("Results"), + rx.text(FormSelectState.form_data.to_string()), + width="100%", + direction="column", + spacing="2", + ) +``` + +## Real World Example + +```python demo +rx.card( + rx.flex( + rx.image(src="https://web.reflex-assets.dev/other/reflex_banner.png", width="100%", height="auto"), + rx.flex( + rx.heading("Reflex Swag", size="4", margin_bottom="4px"), + rx.heading("$99", size="6", margin_bottom="4px"), + direction="row", justify="between", + width="100%", + ), + rx.text("Reflex swag with a sense of nostalgia, as if they carry whispered tales of past adventures", size="2", margin_bottom="4px"), + rx.divider(size="4"), + rx.flex( + rx.flex( + rx.text("Color", size="2", margin_bottom="4px", color_scheme="gray"), + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("Light", value="light"), + rx.select.item("Dark", value="dark"), + ), + ), + default_value="light", + ), + direction="column", + ), + rx.flex( + rx.text("Size", size="2", margin_bottom="4px", color_scheme="gray"), + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("24", value="24"), + rx.select.item("26", value="26"), + rx.select.item("28", value="28", disabled=True), + rx.select.item("30", value="30"), + rx.select.item("32", value="32"), + rx.select.item("34", value="34"), + rx.select.item("36", value="36"), + ), + ), + default_value="30", + ), + direction="column", + ), + rx.button(rx.icon(tag="plus"), "Add"), + align="end", + justify="between", + spacing="2", + width="100%", + ), + width="15em", + direction="column", + spacing="2", + ), +) +``` diff --git a/docs/library/forms/select.md b/docs/library/forms/select.md new file mode 100644 index 00000000000..a54318e40c0 --- /dev/null +++ b/docs/library/forms/select.md @@ -0,0 +1,201 @@ +--- +components: + - rx.select + - rx.select.root + - rx.select.trigger + - rx.select.content + - rx.select.group + - rx.select.item + - rx.select.label + - rx.select.separator + +HighLevelSelect: | + lambda **props: rx.select(["apple", "grape", "pear"], default_value="pear", **props) + +SelectRoot: | + lambda **props: rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("apple", value="apple"), + rx.select.item("grape", value="grape"), + rx.select.item("pear", value="pear"), + ), + ), + default_value="pear", + **props + ) + +SelectTrigger: | + lambda **props: rx.select.root( + rx.select.trigger(**props), + rx.select.content( + rx.select.group( + rx.select.item("apple", value="apple"), + rx.select.item("grape", value="grape"), + rx.select.item("pear", value="pear"), + ), + ), + default_value="pear", + ) + +SelectContent: | + lambda **props: rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("apple", value="apple"), + rx.select.item("grape", value="grape"), + rx.select.item("pear", value="pear"), + ), + **props, + ), + default_value="pear", + ) + +SelectItem: | + lambda **props: rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("apple", value="apple", **props), + rx.select.item("grape", value="grape"), + rx.select.item("pear", value="pear"), + ), + ), + default_value="pear", + ) +--- + +```python exec +import random +import reflex as rx +``` + +# Select + +Displays a list of options for the user to pick from—triggered by a button. + +```python demo exec +class SelectState(rx.State): + value: str = "apple" + + @rx.event + def change_value(self, value: str): + """Change the select value var.""" + self.value = value + + +def select_intro(): + return rx.center( + rx.select( + ["apple", "grape", "pear"], + value=SelectState.value, + on_change=SelectState.change_value, + ), + rx.badge(SelectState.value), + ) +``` + +```python demo exec +class SelectState3(rx.State): + + values: list[str] = ["apple", "grape", "pear"] + + value: str = "apple" + + def set_value(self, value: str): + self.value = value + + @rx.event + def change_value(self): + """Change the select value var.""" + self.value = random.choice(self.values) + + +def select_example3(): + return rx.vstack( + rx.select( + SelectState3.values, + value=SelectState3.value, + on_change=SelectState3.set_value, + ), + rx.button("Change Value", on_click=SelectState3.change_value), + + ) +``` + +The `on_open_change` event handler acts in a similar way to the `on_change` and is called when the open state of the select changes. + +```python demo +rx.select( + ["apple", "grape", "pear"], + on_change=rx.window_alert("on_change event handler called"), +) + +``` + +### Submitting a form using select + +The `name` prop is needed to submit with its owning form as part of a name/value pair. + +When the `required` prop is `True`, it indicates that the user must select a value before the owning form can be submitted. + +```python demo exec +class FormSelectState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def select_form_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.flex( + rx.select(["apple", "grape", "pear"], default_value="apple", name="select", required=True), + rx.button("Submit", flex="1", type="submit"), + width="100%", + spacing="3", + ), + on_submit=FormSelectState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormSelectState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` + +### Using Select within a Drawer component + +If using within a [Drawer](/docs/library/overlay/drawer) component, set the `position` prop to `"popper"` to ensure the select menu is displayed correctly. + +```python demo +rx.drawer.root( + rx.drawer.trigger(rx.button("Open Drawer")), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.drawer.close(rx.box(rx.button("Close"))), + rx.select(["apple", "grape", "pear"], position="popper"), + ), + width="20em", + padding="2em", + background_color=rx.color("gray", 1), + ), + ), + direction="left", +) +``` diff --git a/docs/library/forms/slider.md b/docs/library/forms/slider.md new file mode 100644 index 00000000000..6c35d49ebd3 --- /dev/null +++ b/docs/library/forms/slider.md @@ -0,0 +1,132 @@ +--- +components: + - rx.slider + +Slider: | + lambda **props: rx.center(rx.slider(default_value=40, height="100%", **props), height="4em", width="100%") +--- + +```python exec +import reflex as rx +``` + +# Slider + +Provides user selection from a range of values. The + +## Basic Example + +The slider can be controlled by a single value or a range of values. Slider can be hooked to state to control its value. Passing a list of two values creates a range slider. + +```python demo exec +class SliderState(rx.State): + value: int = 50 + + @rx.event + def set_end(self, value: list[int | float]): + self.value = value[0] + +def slider_intro(): + return rx.vstack( + rx.heading(SliderState.value), + rx.slider(on_value_commit=SliderState.set_end), + width="100%", + ) +``` + +## Range Slider + +Range slider is created by passing a list of two values to the `default_value` prop. The list should contain two values that are in the range of the slider. + +```python demo exec +class RangeSliderState(rx.State): + value_start: int = 25 + value_end: int = 75 + + @rx.event + def set_end(self, value: list[int | float]): + self.value_start = value[0] + self.value_end = value[1] + +def range_slider_intro(): + return rx.vstack( + rx.hstack( + rx.heading(RangeSliderState.value_start), + rx.heading(RangeSliderState.value_end), + ), + rx.slider( + default_value=[25, 75], + min_=0, + max=100, + size="1", + on_value_commit=RangeSliderState.set_end, + ), + width="100%", + ) +``` + +## Live Updating Slider + +You can use the `on_change` prop to update the slider value as you interact with it. The `on_change` prop takes a function that will be called with the new value of the slider. + +Here we use the `throttle` method to limit the rate at which the function is called, which is useful to prevent excessive updates. In this example, the slider value is updated every 100ms. + +```python demo exec +class LiveSliderState(rx.State): + value: int = 50 + + @rx.event + def set_end(self, value: list[int | float]): + self.value = value[0] + +def live_slider_intro(): + return rx.vstack( + rx.heading(LiveSliderState.value), + rx.slider( + default_value=50, + min_=0, + max=100, + on_change=LiveSliderState.set_end.throttle(100), + ), + width="100%", + ) +``` + +## Slider in forms + +Here we show how to use a slider in a form. We use the `name` prop to identify the slider in the form data. The form data is then passed to the `handle_submit` method to be processed. + +```python demo exec +class FormSliderState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def slider_form_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.hstack( + rx.slider(default_value=40, name="slider"), + rx.button("Submit", type="submit"), + width="100%", + ), + on_submit=FormSliderState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormSliderState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` diff --git a/docs/library/forms/switch.md b/docs/library/forms/switch.md new file mode 100644 index 00000000000..f97180de61b --- /dev/null +++ b/docs/library/forms/switch.md @@ -0,0 +1,106 @@ +--- +components: + - rx.switch + +Switch: | + lambda **props: rx.switch(**props) +--- + +```python exec +import reflex as rx +``` + +# Switch + +A toggle switch alternative to the checkbox. + +## Basic Example + +Here is a basic example of a switch. We use the `on_change` trigger to toggle the value in the state. + +```python demo exec +class SwitchState(rx.State): + value: bool = False + + @rx.event + def set_end(self, value: bool): + self.value = value + +def switch_intro(): + return rx.center( + rx.switch(on_change=SwitchState.set_end), + rx.badge(SwitchState.value), + ) +``` + +## Control the value + +The `checked` prop is used to control the state of the switch. The event `on_change` is called when the state of the switch changes, when the `change_checked` event handler is called. + +The `disabled` prop when `True`, prevents the user from interacting with the switch. In our example below, even though the second switch is `disabled` we are still able to change whether it is checked or not using the `checked` prop. + +```python demo exec +class ControlSwitchState(rx.State): + + checked = True + + @rx.event + def change_checked(self, checked: bool): + """Change the switch checked var.""" + self.checked = checked + + +def control_switch_example(): + return rx.hstack( + rx.switch( + checked=ControlSwitchState.checked, + on_change=ControlSwitchState.change_checked, + ), + rx.switch( + checked=ControlSwitchState.checked, + on_change=ControlSwitchState.change_checked, + disabled=True, + ), + ) +``` + +## Switch in forms + +The `name` of the switch is needed to submit with its owning form as part of a name/value pair. When the `required` prop is `True`, it indicates that the user must check the switch before the owning form can be submitted. + +The `value` prop is only used for form submission, use the `checked` prop to control state of the `switch`. + +```python demo exec +class FormSwitchState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def switch_form_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.hstack( + rx.switch(name="switch"), + rx.button("Submit", type="submit"), + width="100%", + ), + on_submit=FormSwitchState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormSwitchState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` diff --git a/docs/library/forms/text_area.md b/docs/library/forms/text_area.md new file mode 100644 index 00000000000..de4f1b6a198 --- /dev/null +++ b/docs/library/forms/text_area.md @@ -0,0 +1,88 @@ +--- +components: + - rx.text_area + +TextArea: | + lambda **props: rx.text_area(**props) +--- + +```python exec +import reflex as rx +``` + +# Text Area + +A text area is a multi-line text input field. + +## Basic Example + +The text area component can be controlled by a single value. The `on_blur` prop can be used to update the value when the text area loses focus. + +```python demo exec +class TextAreaBlur(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, text: str): + self.text = text + +def blur_example(): + return rx.vstack( + rx.heading(TextAreaBlur.text), + rx.text_area( + placeholder="Type here...", + on_blur=TextAreaBlur.set_text, + ), + ) +``` + +## Text Area in forms + +Here we show how to use a text area in a form. We use the `name` prop to identify the text area in the form data. The form data is then passed to the `submit_feedback` method to be processed. + +```python demo exec +class TextAreaFeedbackState(rx.State): + feedback: str = "" + submitted: bool = False + + @rx.event + def set_feedback(self, value: str): + self.feedback = value + + @rx.event + def submit_feedback(self, form_data: dict): + self.submitted = True + + @rx.event + def reset_form(self): + self.feedback = "" + self.submitted = False + +def feedback_form(): + return rx.cond( + TextAreaFeedbackState.submitted, + rx.card( + rx.vstack( + rx.text("Thank you for your feedback!"), + rx.button("Submit another response", on_click=TextAreaFeedbackState.reset_form), + ), + ), + rx.card( + rx.form( + rx.flex( + rx.text("Are you enjoying Reflex?"), + rx.text_area( + placeholder="Write your feedback…", + value=TextAreaFeedbackState.feedback, + on_change=TextAreaFeedbackState.set_feedback, + resize="vertical", + ), + rx.button("Send", type="submit"), + direction="column", + spacing="3", + ), + on_submit=TextAreaFeedbackState.submit_feedback, + ), + ), + ) +``` diff --git a/docs/library/forms/upload.md b/docs/library/forms/upload.md new file mode 100644 index 00000000000..73b4bde769d --- /dev/null +++ b/docs/library/forms/upload.md @@ -0,0 +1,522 @@ +--- +components: + - rx.upload + - rx.upload.root + +Upload: | + lambda **props: rx.center(rx.upload(id="my_upload", **props)) +--- + +```python exec +import reflex as rx +``` + +# File Upload + +Reflex makes it simple to add file upload functionality to your app. You can let users select files, store them on your server, and display or process them as needed. Below is a minimal example that demonstrates how to upload files, save them to disk, and display uploaded images using application state. + +## Basic File Upload Example + +You can let users upload files and keep track of them in your app’s state. The example below allows users to upload files, saves them using the backend, and then displays the uploaded files as images. + +```python +import reflex as rx +class State(rx.State): + uploaded_files: list[str] = [] + + @rx.event + async def handle_upload(self, files: list[rx.UploadFile]): + for file in files: + data = await file.read() + path = rx.get_upload_dir() / file.name + with path.open("wb") as f: + f.write(data) + self.uploaded_files.append(file.name) + +def upload_component(): + return rx.vstack( + rx.upload(id="upload"), + rx.button("Upload", on_click=State.handle_upload(rx.upload_files("upload"))), + rx.foreach(State.uploaded_files, lambda f: rx.image(src=rx.get_upload_url(f))), + ) +``` + +## How File Upload Works + +Selecting a file will add it to the browser file list, which can be rendered +on the frontend using the `rx.selected_files(id)` special Var. To clear the +selected files, you can use another special Var `rx.clear_selected_files(id)` as +an event handler. + +To upload the file(s), you need to bind an event handler and pass the special +`rx.upload_files(upload_id=id)` event arg to it. + +## File Storage Functions + +Reflex provides two key functions for handling uploaded files: + +### rx.get_upload_dir() + +- **Purpose**: Returns a `Path` object pointing to the server-side directory where uploaded files should be saved +- **Usage**: Used in backend event handlers to determine where to save uploaded files +- **Default Location**: `./uploaded_files` (can be customized via `REFLEX_UPLOADED_FILES_DIR` environment variable) +- **Type**: Returns `pathlib.Path` + +### rx.get_upload_url(filename) + +- **Purpose**: Returns the URL string that can be used in frontend components to access uploaded files +- **Usage**: Used in frontend components (like `rx.image`, `rx.video`) to display uploaded files +- **URL Format**: `/_upload/filename` +- **Type**: Returns `str` + +### Key Differences + +- **rx.get_upload_dir()** -> Backend file path for saving files +- **rx.get_upload_url()** -> Frontend URL for displaying files + +### Basic Upload Pattern + +Here is the standard pattern for handling file uploads: + +```python +import reflex as rx + +def create_unique_filename(file_name: str): + import random + import string + + filename = "".join(random.choices(string.ascii_letters + string.digits, k=10)) + return filename + "_" + file_name + +class State(rx.State): + uploaded_files: list[str] = [] + + @rx.event + async def handle_upload(self, files: list[rx.UploadFile]): + """Handle file upload with proper directory management.""" + for file in files: + # Read the file data + upload_data = await file.read() + + # Get the upload directory (backend path) + upload_dir = rx.get_upload_dir() + + # Ensure the directory exists + upload_dir.mkdir(parents=True, exist_ok=True) + + # Create unique filename to prevent conflicts + unique_filename = create_unique_filename(file.name) + + # Create full file path + file_path = upload_dir / unique_filename + + # Save the file + with file_path.open("wb") as f: + f.write(upload_data) + + # Store filename for frontend display + self.uploaded_files.append(unique_filename) + +def upload_component(): + return rx.vstack( + rx.upload( + rx.text("Drop files here or click to select"), + id="file_upload", + border="2px dashed #ccc", + padding="2em", + ), + rx.button( + "Upload Files", + on_click=State.handle_upload(rx.upload_files(upload_id="file_upload")), + ), + # Display uploaded files using rx.get_upload_url() + rx.foreach( + State.uploaded_files, + lambda filename: rx.image(src=rx.get_upload_url(filename)) + ), + ) + +``` + +### Multiple File Upload + +Below is an example of how to allow multiple file uploads (in this case images). + +```python demo box +rx.image(src="https://web.reflex-assets.dev/other/upload.gif") +``` + +```python +class State(rx.State): + """The app state.""" + + # The images to show. + img: list[str] + + @rx.event + async def handle_upload(self, files: list[rx.UploadFile]): + """Handle the upload of file(s). + + Args: + files: The uploaded files. + """ + for file in files: + upload_data = await file.read() + outfile = rx.get_upload_dir() / file.name + + # Save the file. + with outfile.open("wb") as file_object: + file_object.write(upload_data) + + # Update the img var. + self.img.append(file.name) + + +color = "rgb(107,99,246)" + + +def index(): + """The main view.""" + return rx.vstack( + rx.upload( + rx.vstack( + rx.button("Select File", color=color, bg="white", border=f"1px solid {color}"), + rx.text("Drag and drop files here or click to select files"), + ), + id="upload1", + border=f"1px dotted {color}", + padding="5em", + ), + rx.hstack(rx.foreach(rx.selected_files("upload1"), rx.text)), + rx.button( + "Upload", + on_click=State.handle_upload(rx.upload_files(upload_id="upload1")), + ), + rx.button( + "Clear", + on_click=rx.clear_selected_files("upload1"), + ), + rx.foreach(State.img, lambda img: rx.image(src=rx.get_upload_url(img))), + padding="5em", + ) +``` + +### Uploading a Single File (Video) + +Below is an example of how to allow only a single file upload and render (in this case a video). + +```python demo box +rx.el.video(src="https://web.reflex-assets.dev/other/upload_single_video.webm", auto_play=True, controls=True, loop=True) +``` + +```python +class State(rx.State): + """The app state.""" + + # The video to show. + video: str + + @rx.event + async def handle_upload( + self, files: list[rx.UploadFile] + ): + """Handle the upload of file(s). + + Args: + files: The uploaded files. + """ + current_file = files[0] + upload_data = await current_file.read() + outfile = rx.get_upload_dir() / current_file.name + + # Save the file. + with outfile.open("wb") as file_object: + file_object.write(upload_data) + + # Update the video var. + self.video = current_file.name + + +color = "rgb(107,99,246)" + + +def index(): + """The main view.""" + return rx.vstack( + rx.upload( + rx.vstack( + rx.button( + "Select File", + color=color, + bg="white", + border=f"1px solid \{color}", + ), + rx.text( + "Drag and drop files here or click to select files" + ), + ), + id="upload1", + max_files=1, + border=f"1px dotted {color}", + padding="5em", + ), + rx.text(rx.selected_files("upload1")), + rx.button( + "Upload", + on_click=State.handle_upload( + rx.upload_files(upload_id="upload1") + ), + ), + rx.button( + "Clear", + on_click=rx.clear_selected_files("upload1"), + ), + rx.cond( + State.video, + rx.video(src=rx.get_upload_url(State.video)), + ), + padding="5em", + ) +``` + +### Customizing the Upload + +In the example below, the upload component accepts a maximum number of 5 files of specific types. +It also disables the use of the space or enter key in uploading files. + +To use a one-step upload, bind the event handler to the `rx.upload` component's +`on_drop` trigger. + +```python +class State(rx.State): + """The app state.""" + + # The images to show. + img: list[str] + + async def handle_upload(self, files: list[rx.UploadFile]): + """Handle the upload of file(s). + + Args: + files: The uploaded files. + """ + for file in files: + upload_data = await file.read() + outfile = rx.get_upload_dir() / file.name + + # Save the file. + with outfile.open("wb") as file_object: + file_object.write(upload_data) + + # Update the img var. + self.img.append(file.name) + + +color = "rgb(107,99,246)" + + +def index(): + """The main view.""" + return rx.vstack( + rx.upload( + rx.vstack( + rx.button("Select File", color=color, bg="white", border=f"1px solid {color}"), + rx.text("Drag and drop files here or click to select files"), + ), + id="upload2", + multiple=True, + accept = { + "application/pdf": [".pdf"], + "image/png": [".png"], + "image/jpeg": [".jpg", ".jpeg"], + "image/gif": [".gif"], + "image/webp": [".webp"], + "text/html": [".html", ".htm"], + }, + max_files=5, + disabled=False, + no_keyboard=True, + on_drop=State.handle_upload(rx.upload_files(upload_id="upload2")), + border=f"1px dotted {color}", + padding="5em", + ), + rx.grid( + rx.foreach( + State.img, + lambda img: rx.vstack( + rx.image(src=rx.get_upload_url(img)), + rx.text(img), + ), + ), + columns="2", + spacing="1", + ), + padding="5em", + ) +``` + +### Unstyled Upload Component + +To use a completely unstyled upload component and apply your own customization, use `rx.upload.root` instead: + +```python demo +rx.upload.root( + rx.box( + rx.icon( + tag="cloud_upload", + style={"width": "3rem", "height": "3rem", "color": "#2563eb", "marginBottom": "0.75rem"}, + ), + rx.hstack( + rx.text( + "Click to upload", + style={"fontWeight": "bold", "color": "#1d4ed8"}, + ), + " or drag and drop", + style={"fontSize": "0.875rem", "color": "#4b5563"}, + ), + rx.text( + "SVG, PNG, JPG or GIF (MAX. 5MB)", + style={"fontSize": "0.75rem", "color": "#6b7280", "marginTop": "0.25rem"}, + ), + style={ + "display": "flex", + "flexDirection": "column", + "alignItems": "center", + "justifyContent": "center", + "padding": "1.5rem", + "textAlign": "center", + }, + ), + id="my_upload", + style={ + "maxWidth": "24rem", + "height": "16rem", + "borderWidth": "2px", + "borderStyle": "dashed", + "borderColor": "#60a5fa", + "borderRadius": "0.75rem", + "cursor": "pointer", + "transitionProperty": "background-color", + "transitionDuration": "0.2s", + "transitionTimingFunction": "ease-in-out", + "display": "flex", + "alignItems": "center", + "justifyContent": "center", + "boxShadow": "0 1px 2px rgba(0, 0, 0, 0.05)", + }, +) +``` + +## Handling the Upload + +Your event handler should be an async function that accepts a single argument, +`files: list[UploadFile]`, which will contain [FastAPI UploadFile](https://fastapi.tiangolo.com/tutorial/request-files) instances. +You can read the files and save them anywhere as shown in the example. + +In your UI, you can bind the event handler to a trigger, such as a button +`on_click` event or upload `on_drop` event, and pass in the files using +`rx.upload_files()`. + +### Saving the File + +By convention, Reflex provides the function `rx.get_upload_dir()` to get the directory where uploaded files may be saved. The upload dir comes from the environment variable `REFLEX_UPLOADED_FILES_DIR`, or `./uploaded_files` if not specified. + +The backend of your app will mount this uploaded files directory on `/_upload` without restriction. Any files uploaded via this mechanism will automatically be publicly accessible. To get the URL for a file inside the upload dir, use the `rx.get_upload_url(filename)` function in a frontend component. + +```md alert info +# When using the Reflex hosting service, the uploaded files directory is not persistent and will be cleared on every deployment. For persistent storage of uploaded files, it is recommended to use an external service, such as S3. +``` + +### Directory Structure and URLs + +By default, Reflex creates the following structure: + +```text +your_project/ +├── uploaded_files/ # rx.get_upload_dir() points here +│ ├── image1.png +│ ├── document.pdf +│ └── video.mp4 +└── ... +``` + +The files are automatically served at: + +- `/_upload/image1.png` ← `rx.get_upload_url("image1.png")` +- `/_upload/document.pdf` ← `rx.get_upload_url("document.pdf")` +- `/_upload/video.mp4` ← `rx.get_upload_url("video.mp4")` + +## Cancellation + +The `id` provided to the `rx.upload` component can be passed to the special event handler `rx.cancel_upload(id)` to stop uploading on demand. Cancellation can be triggered directly by a frontend event trigger, or it can be returned from a backend event handler. + +## Progress + +The `rx.upload_files` special event arg also accepts an `on_upload_progress` event trigger which will be fired about every second during the upload operation to report the progress of the upload. This can be used to update a progress bar or other UI elements to show the user the progress of the upload. + +```python +class UploadExample(rx.State): + uploading: bool = False + progress: int = 0 + total_bytes: int = 0 + + @rx.event + async def handle_upload(self, files: list[rx.UploadFile]): + for file in files: + self.total_bytes += len(await file.read()) + + @rx.event + def handle_upload_progress(self, progress: dict): + self.uploading = True + self.progress = round(progress["progress"] * 100) + if self.progress >= 100: + self.uploading = False + + @rx.event + def cancel_upload(self): + self.uploading = False + return rx.cancel_upload("upload3") + + +def upload_form(): + return rx.vstack( + rx.upload( + rx.text("Drag and drop files here or click to select files"), + id="upload3", + border="1px dotted rgb(107,99,246)", + padding="5em", + ), + rx.vstack(rx.foreach(rx.selected_files("upload3"), rx.text)), + rx.progress(value=UploadExample.progress, max=100), + rx.cond( + ~UploadExample.uploading, + rx.button( + "Upload", + on_click=UploadExample.handle_upload( + rx.upload_files( + upload_id="upload3", + on_upload_progress=UploadExample.handle_upload_progress, + ), + ), + ), + rx.button("Cancel", on_click=UploadExample.cancel_upload), + ), + rx.text("Total bytes uploaded: ", UploadExample.total_bytes), + align="center", + ) +``` + +The `progress` dictionary contains the following keys: + +```javascript +\{ + 'loaded': 36044800, + 'total': 54361908, + 'progress': 0.6630525183185255, + 'bytes': 20447232, + 'rate': None, + 'estimated': None, + 'event': \{'isTrusted': True}, + 'upload': True +} +``` diff --git a/docs/library/graphing/charts/areachart.md b/docs/library/graphing/charts/areachart.md new file mode 100644 index 00000000000..c857dc3a704 --- /dev/null +++ b/docs/library/graphing/charts/areachart.md @@ -0,0 +1,444 @@ +--- +components: + - rx.recharts.AreaChart + - rx.recharts.Area +--- + +# Area Chart + +```python exec +import reflex as rx +import random +``` + +A Recharts area chart displays quantitative data using filled areas between a line connecting data points and the axis. + +## Basic Example + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_simple(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width = "100%", + height = 250, + ) +``` + +## Syncing Charts + +The `sync_id` prop allows you to sync two graphs. In the example, it is set to "1" for both charts, indicating that they should be synchronized. This means that any interactions (such as brushing) performed on one chart will be reflected in the other chart. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_sync(): + return rx.vstack( + rx.recharts.bar_chart( + rx.recharts.graphing_tooltip(), + rx.recharts.bar( + data_key="uv", stroke="#8884d8", fill="#8884d8" + ), + rx.recharts.bar( + data_key="pv", stroke="#82ca9d", fill="#82ca9d", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + sync_id="1", + width = "100%", + height = 200, + ), + rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", stroke="#8884d8", fill="#8884d8" + ), + rx.recharts.line( + data_key="pv", type_="monotone", stroke="#ff7300", + ), + + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.graphing_tooltip(), + rx.recharts.brush( + data_key="name", height=30, stroke="#8884d8" + ), + data=data, + sync_id="1", + width = "100%", + height = 250, + ), + width="100%", + ) +``` + +## Stacking Charts + +The `stack_id` prop allows you to stack multiple graphs on top of each other. In the example, it is set to "1" for both charts, indicating that they should be stacked together. This means that the bars or areas of the charts will be vertically stacked, with the values of each chart contributing to the total height of the stacked areas or bars. + +This is similar to the `sync_id` prop, but instead of synchronizing the interaction between the charts, it just stacks the charts on top of each other. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_stack(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + stack_id="1", + ), + rx.recharts.area( + data_key="pv", + stroke=rx.color("green", 9), + fill=rx.color("green", 8), + stack_id="1", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + stack_offset="none", + margin={"top": 5, "right": 5, "bottom": 5, "left": 5}, + width = "100%", + height = 300, + ) +``` + +## Multiple Axis + +Multiple axes can be used for displaying different data series with varying scales or units on the same chart. This allows for a more comprehensive comparison and analysis of the data. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_multi_axis(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", stroke="#8884d8", fill="#8884d8", x_axis_id="primary", y_axis_id="left", + ), + rx.recharts.area( + data_key="pv", x_axis_id="secondary", y_axis_id="right", type_="monotone", stroke="#82ca9d", fill="#82ca9d" + ), + rx.recharts.x_axis(data_key="name", x_axis_id="primary"), + rx.recharts.x_axis(data_key="name", x_axis_id="secondary", orientation="top"), + rx.recharts.y_axis(data_key="uv", y_axis_id="left"), + rx.recharts.y_axis(data_key="pv", y_axis_id="right", orientation="right"), + rx.recharts.graphing_tooltip(), + rx.recharts.legend(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Layout + +Use the `layout` prop to set the orientation to either `"horizontal"` (default) or `"vertical"`. + +```md alert info +# Include margins around your graph to ensure proper spacing and enhance readability. By default, provide margins on all sides of the chart to create a visually appealing and functional representation of your data. +``` + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_vertical(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke=rx.color("accent", 8), + fill=rx.color("accent", 3), + ), + rx.recharts.x_axis(type_="number"), + rx.recharts.y_axis(data_key="name", type_="category"), + data=data, + layout="vertical", + height = 300, + width = "100%", + ) +``` + +## Stateful Example + +Here is an example of an area graph with a `State`. Here we have defined a function `randomize_data`, which randomly changes the data for both graphs when the first defined `area` is clicked on using `on_click=AreaState.randomize_data`. + +```python demo exec +class AreaState(rx.State): + data = data + curve_type = "" + + @rx.event + def randomize_data(self): + for i in range(len(self.data)): + self.data[i]["uv"] = random.randint(0, 10000) + self.data[i]["pv"] = random.randint(0, 10000) + self.data[i]["amt"] = random.randint(0, 10000) + + def change_curve_type(self, type_input): + self.curve_type = type_input + +def area_stateful(): + return rx.vstack( + rx.hstack( + rx.text("Curve Type:"), + rx.select( + [ + 'basis', + 'natural', + 'step' + ], + on_change = AreaState.change_curve_type, + default_value = 'basis', + ), + ), + rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + on_click=AreaState.randomize_data, + type_ = AreaState.curve_type, + ), + rx.recharts.area( + data_key="pv", + stroke="#82ca9d", + fill="#82ca9d", + on_click=AreaState.randomize_data, + type_ = AreaState.curve_type, + ), + rx.recharts.x_axis( + data_key="name", + ), + rx.recharts.y_axis(), + rx.recharts.legend(), + rx.recharts.cartesian_grid(), + data=AreaState.data, + width = "100%", + height=400, + ), + width="100%", + ) +``` diff --git a/docs/library/graphing/charts/barchart.md b/docs/library/graphing/charts/barchart.md new file mode 100644 index 00000000000..9359661b7ae --- /dev/null +++ b/docs/library/graphing/charts/barchart.md @@ -0,0 +1,387 @@ +--- +components: + - rx.recharts.BarChart + - rx.recharts.Bar +--- + +# Bar Chart + +```python exec +import reflex as rx +import random +``` + +A bar chart presents categorical data with rectangular bars with heights or lengths proportional to the values that they represent. + +For a bar chart we must define an `rx.recharts.bar()` component for each set of values we wish to plot. Each `rx.recharts.bar()` component has a `data_key` which clearly states which variable in our data we are tracking. In this simple example we plot `uv` as a bar against the `name` column which we set as the `data_key` in `rx.recharts.x_axis`. + +## Simple Example + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def bar_simple(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width = "100%", + height = 250, + ) +``` + +## Multiple Bars + +Multiple bars can be placed on the same `bar_chart`, using multiple `rx.recharts.bar()` components. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def bar_double(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.bar( + data_key="pv", + stroke=rx.color("green", 9), + fill=rx.color("green", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width = "100%", + height = 250, + ) +``` + +## Ranged Charts + +You can also assign a range in the bar by assigning the data_key in the `rx.recharts.bar` to a list with two elements, i.e. here a range of two temperatures for each date. + +```python demo graphing +range_data = [ + { + "day": "05-01", + "temperature": [ + -1, + 10 + ] + }, + { + "day": "05-02", + "temperature": [ + 2, + 15 + ] + }, + { + "day": "05-03", + "temperature": [ + 3, + 12 + ] + }, + { + "day": "05-04", + "temperature": [ + 4, + 12 + ] + }, + { + "day": "05-05", + "temperature": [ + 12, + 16 + ] + }, + { + "day": "05-06", + "temperature": [ + 5, + 16 + ] + }, + { + "day": "05-07", + "temperature": [ + 3, + 12 + ] + }, + { + "day": "05-08", + "temperature": [ + 0, + 8 + ] + }, + { + "day": "05-09", + "temperature": [ + -3, + 5 + ] + } +] + +def bar_range(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="temperature", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="day"), + rx.recharts.y_axis(), + data=range_data, + width = "100%", + height = 250, + ) +``` + +## Stateful Charts + +Here is an example of a bar graph with a `State`. Here we have defined a function `randomize_data`, which randomly changes the data for both graphs when the first defined `bar` is clicked on using `on_click=BarState.randomize_data`. + +```python demo exec +class BarState(rx.State): + data = data + + @rx.event + def randomize_data(self): + for i in range(len(self.data)): + self.data[i]["uv"] = random.randint(0, 10000) + self.data[i]["pv"] = random.randint(0, 10000) + self.data[i]["amt"] = random.randint(0, 10000) + +def bar_with_state(): + return rx.recharts.bar_chart( + rx.recharts.cartesian_grid( + stroke_dasharray="3 3", + ), + rx.recharts.bar( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.bar( + data_key="pv", + stroke=rx.color("green", 9), + fill=rx.color("green", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.legend(), + on_click=BarState.randomize_data, + data=BarState.data, + width = "100%", + height = 300, + ) +``` + +## Example with Props + +Here's an example demonstrates how to customize the appearance and layout of bars using the `bar_category_gap`, `bar_gap`, `bar_size`, and `max_bar_size` props. These props accept values in pixels to control the spacing and size of the bars. + +```python demo graphing + +data = [ + {'name': 'Page A', 'value': 2400}, + {'name': 'Page B', 'value': 1398}, + {'name': 'Page C', 'value': 9800}, + {'name': 'Page D', 'value': 3908}, + {'name': 'Page E', 'value': 4800}, + {'name': 'Page F', 'value': 3800}, +] + +def bar_features(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + bar_category_gap="15%", + bar_gap=6, + bar_size=100, + max_bar_size=40, + width="100%", + height=300, + ) +``` + +## Vertical Example + +The `layout` prop allows you to set the orientation of the graph to be vertical or horizontal, it is set horizontally by default. + +```md alert info +# Include margins around your graph to ensure proper spacing and enhance readability. By default, provide margins on all sides of the chart to create a visually appealing and functional representation of your data. +``` + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def bar_vertical(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="uv", + stroke=rx.color("accent", 8), + fill=rx.color("accent", 3), + ), + rx.recharts.x_axis(type_="number"), + rx.recharts.y_axis(data_key="name", type_="category"), + data=data, + layout="vertical", + margin={ + "top": 20, + "right": 20, + "left": 20, + "bottom": 20 + }, + width = "100%", + height = 300, + + ) +``` + +To learn how to use the `sync_id`, `stack_id`,`x_axis_id` and `y_axis_id` props check out the of the area chart [documentation](/docs/library/graphing/charts/areachart), where these props are all described with examples. diff --git a/docs/library/graphing/charts/composedchart.md b/docs/library/graphing/charts/composedchart.md new file mode 100644 index 00000000000..c2c97210b3c --- /dev/null +++ b/docs/library/graphing/charts/composedchart.md @@ -0,0 +1,89 @@ +--- +components: + - rx.recharts.ComposedChart +--- + +```python exec +import reflex as rx +``` + +# Composed Chart + +A `composed_chart` is a higher-level component chart that is composed of multiple charts, where other charts are the children of the `composed_chart`. The charts are placed on top of each other in the order they are provided in the `composed_chart` function. + +```md alert info +# To learn more about individual charts, checkout: **[area_chart](/docs/library/graphing/charts/areachart)**, **[line_chart](/docs/library/graphing/charts/linechart)**, or **[bar_chart](/docs/library/graphing/charts/barchart)**. +``` + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def composed(): + return rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + data_key="amt", + bar_size=20, + fill="#413ea0" + ), + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke="#ff7300" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.graphing_tooltip(), + data=data, + height=250, + width="100%", + ) +``` diff --git a/docs/library/graphing/charts/errorbar.md b/docs/library/graphing/charts/errorbar.md new file mode 100644 index 00000000000..f488a2c7bab --- /dev/null +++ b/docs/library/graphing/charts/errorbar.md @@ -0,0 +1,101 @@ +--- +components: + - rx.recharts.ErrorBar +--- + +```python exec +import reflex as rx +``` + +# Error Bar + +An error bar is a graphical representation of the uncertainty or variability of a data point in a chart, depicted as a line extending from the data point parallel to one of the axes. The `data_key`, `width`, `stroke_width`, `stroke`, and `direction` props can be used to customize the appearance and behavior of the error bars, specifying the data source, dimensions, color, and orientation of the error bars. + +```python demo graphing +data = [ + { + "x": 45, + "y": 100, + "z": 150, + "errorY": [ + 30, + 20 + ], + "errorX": 5 + }, + { + "x": 100, + "y": 200, + "z": 200, + "errorY": [ + 20, + 30 + ], + "errorX": 3 + }, + { + "x": 120, + "y": 100, + "z": 260, + "errorY": 20, + "errorX": [ + 5, + 3 + ] + }, + { + "x": 170, + "y": 300, + "z": 400, + "errorY": [ + 15, + 18 + ], + "errorX": 4 + }, + { + "x": 140, + "y": 250, + "z": 280, + "errorY": 23, + "errorX": [ + 6, + 7 + ] + }, + { + "x": 150, + "y": 400, + "z": 500, + "errorY": [ + 21, + 10 + ], + "errorX": 4 + }, + { + "x": 110, + "y": 280, + "z": 200, + "errorY": 21, + "errorX": [ + 5, + 6 + ] + } +] + +def error(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + rx.recharts.error_bar(data_key="errorY", direction="y", width=4, stroke_width=2, stroke="red"), + rx.recharts.error_bar(data_key="errorX", direction="x", width=4, stroke_width=2), + data=data, + fill="#8884d8", + name="A"), + rx.recharts.x_axis(data_key="x", name="x", type_="number"), + rx.recharts.y_axis(data_key="y", name="y", type_="number"), + width = "100%", + height = 300, + ) +``` diff --git a/docs/library/graphing/charts/funnelchart.md b/docs/library/graphing/charts/funnelchart.md new file mode 100644 index 00000000000..69e98f84d26 --- /dev/null +++ b/docs/library/graphing/charts/funnelchart.md @@ -0,0 +1,210 @@ +--- +components: + - rx.recharts.FunnelChart + - rx.recharts.Funnel +--- + +```python exec +import reflex as rx +import random +rx.toast.provider() +``` + +# Funnel Chart + +A funnel chart is a graphical representation used to visualize how data moves through a process. In a funnel chart, the dependent variable’s value diminishes in the subsequent stages of the process. It can be used to demonstrate the flow of users through a business or sales process. + +## Simple Example + +```python demo graphing +data = [ + { + "value": 100, + "name": "Sent", + "fill": "#8884d8" + }, + { + "value": 80, + "name": "Viewed", + "fill": "#83a6ed" + }, + { + "value": 50, + "name": "Clicked", + "fill": "#8dd1e1" + }, + { + "value": 40, + "name": "Add to Cart", + "fill": "#82ca9d" + }, + { + "value": 26, + "name": "Purchased", + "fill": "#a4de6c" + } +] + +def funnel_simple(): + return rx.recharts.funnel_chart( + rx.recharts.funnel( + rx.recharts.label_list( + position="right", + data_key="name", + fill="#000", + stroke="none", + ), + data_key="value", + data=data, + ), + width="100%", + height=250, + ) +``` + +## Event Triggers + +Funnel chart supports `on_click`, `on_mouse_enter`, `on_mouse_leave` and `on_mouse_move` event triggers, allows you to interact with the funnel chart and perform specific actions based on user interactions. + +```python demo graphing +data = [ + { + "value": 100, + "name": "Sent", + "fill": "#8884d8" + }, + { + "value": 80, + "name": "Viewed", + "fill": "#83a6ed" + }, + { + "value": 50, + "name": "Clicked", + "fill": "#8dd1e1" + }, + { + "value": 40, + "name": "Add to Cart", + "fill": "#82ca9d" + }, + { + "value": 26, + "name": "Purchased", + "fill": "#a4de6c" + } +] + +def funnel_events(): + return rx.recharts.funnel_chart( + rx.recharts.funnel( + rx.recharts.label_list( + position="right", + data_key="name", + fill="#000", + stroke="none", + ), + data_key="value", + data=data, + ), + on_click=rx.toast("Clicked on funnel chart"), + on_mouse_enter=rx.toast("Mouse entered"), + on_mouse_leave=rx.toast("Mouse left"), + width="100%", + height=250, + ) +``` + +## Dynamic Data + +Here is an example of a funnel chart with a `State`. Here we have defined a function `randomize_data`, which randomly changes the data when the graph is clicked on using `on_click=FunnelState.randomize_data`. + +```python exec +data = [ + { + "value": 100, + "name": "Sent", + "fill": "#8884d8" + }, + { + "value": 80, + "name": "Viewed", + "fill": "#83a6ed" + }, + { + "value": 50, + "name": "Clicked", + "fill": "#8dd1e1" + }, + { + "value": 40, + "name": "Add to Cart", + "fill": "#82ca9d" + }, + { + "value": 26, + "name": "Purchased", + "fill": "#a4de6c" + } +] +``` + +```python demo exec +class FunnelState(rx.State): + data = data + + @rx.event + def randomize_data(self): + self.data[0]["value"] = 100 + for i in range(len(self.data) - 1): + self.data[i + 1]["value"] = self.data[i][ + "value" + ] - random.randint(0, 20) + + +def funnel_state(): + return rx.recharts.funnel_chart( + rx.recharts.funnel( + rx.recharts.label_list( + position="right", + data_key="name", + fill="#000", + stroke="none", + ), + data_key="value", + data=FunnelState.data, + on_click=FunnelState.randomize_data, + ), + rx.recharts.graphing_tooltip(), + width="100%", + height=250, + ) +``` + +## Changing the Chart Animation + +The `is_animation_active` prop can be used to turn off the animation, but defaults to `True`. `animation_begin` sets the delay before animation starts, `animation_duration` determines how long the animation lasts, and `animation_easing` defines the speed curve of the animation for smooth transitions. + +```python demo graphing +data = [ + {"name": "Visits", "value": 5000, "fill": "#8884d8"}, + {"name": "Cart", "value": 3000, "fill": "#83a6ed"}, + {"name": "Checkout", "value": 2500, "fill": "#8dd1e1"}, + {"name": "Purchase", "value": 1000, "fill": "#82ca9d"}, + ] + +def funnel_animation(): + return rx.recharts.funnel_chart( + rx.recharts.funnel( + data_key="value", + data=data, + animation_begin=300, + animation_duration=9000, + animation_easing="ease-in-out", + ), + rx.recharts.graphing_tooltip(), + rx.recharts.legend(), + width="100%", + height=300, + ) +``` diff --git a/docs/library/graphing/charts/linechart.md b/docs/library/graphing/charts/linechart.md new file mode 100644 index 00000000000..a2dddfd7db3 --- /dev/null +++ b/docs/library/graphing/charts/linechart.md @@ -0,0 +1,308 @@ +--- +components: + - rx.recharts.LineChart + - rx.recharts.Line +--- + +# Line Chart + +```python exec +import random +from typing import Any +import reflex as rx +``` + +A line chart is a type of chart used to show information that changes over time. Line charts are created by plotting a series of several points and connecting them with a straight line. + +## Simple Example + +For a line chart we must define an `rx.recharts.line()` component for each set of values we wish to plot. Each `rx.recharts.line()` component has a `data_key` which clearly states which variable in our data we are tracking. In this simple example we plot `pv` and `uv` as separate lines against the `name` column which we set as the `data_key` in `rx.recharts.x_axis`. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def line_simple(): + return rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + ), + rx.recharts.line( + data_key="uv", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Example with Props + +Our second example uses exactly the same data as our first example, except now we add some extra features to our line graphs. We add a `type_` prop to `rx.recharts.line` to style the lines differently. In addition, we add an `rx.recharts.cartesian_grid` to get a grid in the background, an `rx.recharts.legend` to give us a legend for our graphs and an `rx.recharts.graphing_tooltip` to add a box with text that appears when you pause the mouse pointer on an element in the graph. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def line_features(): + return rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke="#8884d8",), + rx.recharts.line( + data_key="uv", + type_="monotone", + stroke="#82ca9d",), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.graphing_tooltip(), + rx.recharts.legend(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Layout + +The `layout` prop allows you to set the orientation of the graph to be vertical or horizontal. The `margin` prop defines the spacing around the graph, + +```md alert info +# Include margins around your graph to ensure proper spacing and enhance readability. By default, provide margins on all sides of the chart to create a visually appealing and functional representation of your data. +``` + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def line_vertical(): + return rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + stroke=rx.color("accent", 9), + ), + rx.recharts.line( + data_key="uv", + stroke=rx.color("green", 9), + ), + rx.recharts.x_axis(type_="number"), + rx.recharts.y_axis(data_key="name", type_="category"), + layout="vertical", + margin={ + "top": 20, + "right": 20, + "left": 20, + "bottom": 20 + }, + data = data, + height = 300, + width = "100%", + ) +``` + +## Dynamic Data + +Chart data can be modified by tying the `data` prop to a State var. Most other +props, such as `type_`, can be controlled dynamically as well. In the following +example the "Munge Data" button can be used to randomly modify the data, and the +two `select` elements change the line `type_`. Since the data and style is saved +in the per-browser-tab State, the changes will not be visible to other visitors. + +```python demo exec + +initial_data = data + +class LineChartState(rx.State): + data: list[dict[str, Any]] = initial_data + pv_type: str = "monotone" + uv_type: str = "monotone" + + @rx.event + def set_pv_type(self, pv_type: str): + self.pv_type = pv_type + + @rx.event + def set_uv_type(self, uv_type: str): + self.uv_type = uv_type + + @rx.event + def munge_data(self): + for row in self.data: + row["uv"] += random.randint(-500, 500) + row["pv"] += random.randint(-1000, 1000) + +def line_dynamic(): + return rx.vstack( + rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + type_=LineChartState.pv_type, + stroke="#8884d8", + ), + rx.recharts.line( + data_key="uv", + type_=LineChartState.uv_type, + stroke="#82ca9d", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=LineChartState.data, + margin={ + "top": 20, + "right": 20, + "left": 20, + "bottom": 20 + }, + width = "100%", + height = 300, + ), + rx.hstack( + rx.button("Munge Data", on_click=LineChartState.munge_data), + rx.select( + ["monotone", "linear", "step", "stepBefore", "stepAfter"], + value=LineChartState.pv_type, + on_change=LineChartState.set_pv_type + ), + rx.select( + ["monotone", "linear", "step", "stepBefore", "stepAfter"], + value=LineChartState.uv_type, + on_change=LineChartState.set_uv_type + ), + ), + width="100%", + ) +``` + +To learn how to use the `sync_id`, `x_axis_id` and `y_axis_id` props check out the of the area chart [documentation](/docs/library/graphing/charts/areachart), where these props are all described with examples. diff --git a/docs/library/graphing/charts/piechart.md b/docs/library/graphing/charts/piechart.md new file mode 100644 index 00000000000..999780ad72f --- /dev/null +++ b/docs/library/graphing/charts/piechart.md @@ -0,0 +1,216 @@ +--- +components: + - rx.recharts.PieChart + - rx.recharts.Pie +--- + +# Pie Chart + +```python exec +import reflex as rx +``` + +A pie chart is a circular statistical graphic which is divided into slices to illustrate numerical proportion. + +For a pie chart we must define an `rx.recharts.pie()` component for each set of values we wish to plot. Each `rx.recharts.pie()` component has a `data`, a `data_key` and a `name_key` which clearly states which data and which variables in our data we are tracking. In this simple example we plot `value` column as our `data_key` against the `name` column which we set as our `name_key`. +We also use the `fill` prop to set the color of the pie slices. + +```python demo graphing + +data01 = [ + { + "name": "Group A", + "value": 400 + }, + { + "name": "Group B", + "value": 300, + "fill":"#AC0E08FF" + }, + { + "name": "Group C", + "value": 300, + "fill":"rgb(80,40, 190)" + }, + { + "name": "Group D", + "value": 200, + "fill":rx.color("yellow", 10) + }, + { + "name": "Group E", + "value": 278, + "fill":"purple" + }, + { + "name": "Group F", + "value": 189, + "fill":"orange" + } +] + +def pie_simple(): + return rx.recharts.pie_chart( + rx.recharts.pie( + data=data01, + data_key="value", + name_key="name", + fill="#8884d8", + label=True, + ), + width="100%", + height=300, + ) +``` + +We can also add two pies on one chart by using two `rx.recharts.pie` components. + +In this example `inner_radius` and `outer_radius` props are used. They define the doughnut shape of a pie chart: `inner_radius` creates the hollow center (use "0%" for a full pie), while `outer_radius` sets the overall size. The `padding_angle` prop, used on the green pie below, adds space between pie slices, enhancing visibility of individual segments. + +```python demo graphing + +data01 = [ + { + "name": "Group A", + "value": 400 + }, + { + "name": "Group B", + "value": 300 + }, + { + "name": "Group C", + "value": 300 + }, + { + "name": "Group D", + "value": 200 + }, + { + "name": "Group E", + "value": 278 + }, + { + "name": "Group F", + "value": 189 + } +] +data02 = [ + { + "name": "Group A", + "value": 2400 + }, + { + "name": "Group B", + "value": 4567 + }, + { + "name": "Group C", + "value": 1398 + }, + { + "name": "Group D", + "value": 9800 + }, + { + "name": "Group E", + "value": 3908 + }, + { + "name": "Group F", + "value": 4800 + } +] + + +def pie_double(): + return rx.recharts.pie_chart( + rx.recharts.pie( + data=data01, + data_key="value", + name_key="name", + fill="#82ca9d", + inner_radius="60%", + padding_angle=5, + ), + rx.recharts.pie( + data=data02, + data_key="value", + name_key="name", + fill="#8884d8", + outer_radius="50%", + ), + rx.recharts.graphing_tooltip(), + width="100%", + height=300, + ) +``` + +## Dynamic Data + +Chart data tied to a State var causes the chart to automatically update when the +state changes, providing a nice way to visualize data in response to user +interface elements. View the "Data" tab to see the substate driving this +half-pie chart. + +```python demo exec +from typing import Any + + +class PieChartState(rx.State): + resources: list[dict[str, Any]] = [ + dict(type_="🏆", count=1), + dict(type_="🪵", count=1), + dict(type_="🥑", count=1), + dict(type_="🧱", count=1), + ] + + @rx.var(cache=True) + def resource_types(self) -> list[str]: + return [r["type_"] for r in self.resources] + + @rx.event + def increment(self, type_: str): + for resource in self.resources: + if resource["type_"] == type_: + resource["count"] += 1 + break + + @rx.event + def decrement(self, type_: str): + for resource in self.resources: + if resource["type_"] == type_ and resource["count"] > 0: + resource["count"] -= 1 + break + + +def dynamic_pie_example(): + return rx.hstack( + rx.recharts.pie_chart( + rx.recharts.pie( + data=PieChartState.resources, + data_key="count", + name_key="type_", + cx="50%", + cy="50%", + start_angle=180, + end_angle=0, + fill="#8884d8", + label=True, + ), + rx.recharts.graphing_tooltip(), + ), + rx.vstack( + rx.foreach( + PieChartState.resource_types, + lambda type_, i: rx.hstack( + rx.button("-", on_click=PieChartState.decrement(type_)), + rx.text(type_, PieChartState.resources[i]["count"]), + rx.button("+", on_click=PieChartState.increment(type_)), + ), + ), + ), + width="100%", + height="15em", + ) +``` diff --git a/docs/library/graphing/charts/radarchart.md b/docs/library/graphing/charts/radarchart.md new file mode 100644 index 00000000000..699ce1e2e33 --- /dev/null +++ b/docs/library/graphing/charts/radarchart.md @@ -0,0 +1,285 @@ +--- +components: + - rx.recharts.RadarChart + - rx.recharts.Radar +--- + +# Radar Chart + +```python exec +import reflex as rx +from typing import Any +``` + +A radar chart shows multivariate data of three or more quantitative variables mapped onto an axis. + +## Simple Example + +For a radar chart we must define an `rx.recharts.radar()` component for each set of values we wish to plot. Each `rx.recharts.radar()` component has a `data_key` which clearly states which variable in our data we are plotting. In this simple example we plot the `A` column of our data against the `subject` column which we set as the `data_key` in `rx.recharts.polar_angle_axis`. + +```python demo graphing +data = [ + { + "subject": "Math", + "A": 120, + "B": 110, + "fullMark": 150 + }, + { + "subject": "Chinese", + "A": 98, + "B": 130, + "fullMark": 150 + }, + { + "subject": "English", + "A": 86, + "B": 130, + "fullMark": 150 + }, + { + "subject": "Geography", + "A": 99, + "B": 100, + "fullMark": 150 + }, + { + "subject": "Physics", + "A": 85, + "B": 90, + "fullMark": 150 + }, + { + "subject": "History", + "A": 65, + "B": 85, + "fullMark": 150 + } +] + +def radar_simple(): + return rx.recharts.radar_chart( + rx.recharts.radar( + data_key="A", + stroke="#8884d8", + fill="#8884d8", + ), + rx.recharts.polar_grid(), + rx.recharts.polar_angle_axis(data_key="subject"), + rx.recharts.polar_radius_axis(angle=90, domain=[0, 150]), + data=data, + width="100%", + height=300, + ) +``` + +## Multiple Radars + +We can also add two radars on one chart by using two `rx.recharts.radar` components. + +In this plot an `inner_radius` and an `outer_radius` are set which determine the chart's size and shape. The `inner_radius` sets the distance from the center to the innermost part of the chart (creating a hollow center if greater than zero), while the `outer_radius` defines the chart's overall size by setting the distance from the center to the outermost edge of the radar plot. + +```python demo graphing + +data = [ + { + "subject": "Math", + "A": 120, + "B": 110, + "fullMark": 150 + }, + { + "subject": "Chinese", + "A": 98, + "B": 130, + "fullMark": 150 + }, + { + "subject": "English", + "A": 86, + "B": 130, + "fullMark": 150 + }, + { + "subject": "Geography", + "A": 99, + "B": 100, + "fullMark": 150 + }, + { + "subject": "Physics", + "A": 85, + "B": 90, + "fullMark": 150 + }, + { + "subject": "History", + "A": 65, + "B": 85, + "fullMark": 150 + } +] + +def radar_multiple(): + return rx.recharts.radar_chart( + rx.recharts.radar( + data_key="A", + stroke="#8884d8", + fill="#8884d8", + ), + rx.recharts.radar( + data_key="B", + stroke="#82ca9d", + fill="#82ca9d", + fill_opacity=0.6, + ), + rx.recharts.polar_grid(), + rx.recharts.polar_angle_axis(data_key="subject"), + rx.recharts.polar_radius_axis(angle=90, domain=[0, 150]), + rx.recharts.legend(), + data=data, + inner_radius="15%", + outer_radius="80%", + width="100%", + height=300, + ) + +``` + +## Using More Props + +The `dot` prop shows points at each data vertex when true. `legend_type="line"` displays a line in the chart legend. `animation_begin=0` starts the animation immediately, `animation_duration=8000` sets an 8-second animation, and `animation_easing="ease-in"` makes the animation start slowly and speed up. These props control the chart's appearance and animation behavior. + +```python demo graphing + +data = [ + { + "subject": "Math", + "A": 120, + "B": 110, + "fullMark": 150 + }, + { + "subject": "Chinese", + "A": 98, + "B": 130, + "fullMark": 150 + }, + { + "subject": "English", + "A": 86, + "B": 130, + "fullMark": 150 + }, + { + "subject": "Geography", + "A": 99, + "B": 100, + "fullMark": 150 + }, + { + "subject": "Physics", + "A": 85, + "B": 90, + "fullMark": 150 + }, + { + "subject": "History", + "A": 65, + "B": 85, + "fullMark": 150 + } + ] + + +def radar_start_end(): + return rx.recharts.radar_chart( + rx.recharts.radar( + data_key="A", + dot=True, + stroke="#8884d8", + fill="#8884d8", + fill_opacity=0.6, + legend_type="line", + animation_begin=0, + animation_duration=8000, + animation_easing="ease-in", + ), + rx.recharts.polar_grid(), + rx.recharts.polar_angle_axis(data_key="subject"), + rx.recharts.polar_radius_axis(angle=90, domain=[0, 150]), + rx.recharts.legend(), + data=data, + width="100%", + height=300, + ) + +``` + +# Dynamic Data + +Chart data tied to a State var causes the chart to automatically update when the +state changes, providing a nice way to visualize data in response to user +interface elements. View the "Data" tab to see the substate driving this +radar chart of character traits. + +```python demo exec +class RadarChartState(rx.State): + total_points: int = 100 + traits: list[dict[str, Any]] = [ + dict(trait="Strength", value=15), + dict(trait="Dexterity", value=15), + dict(trait="Constitution", value=15), + dict(trait="Intelligence", value=15), + dict(trait="Wisdom", value=15), + dict(trait="Charisma", value=15), + ] + + @rx.var + def remaining_points(self) -> int: + return self.total_points - sum(t["value"] for t in self.traits) + + @rx.var(cache=True) + def trait_names(self) -> list[str]: + return [t["trait"] for t in self.traits] + + @rx.event + def set_trait(self, trait: str, value: int): + for t in self.traits: + if t["trait"] == trait: + available_points = self.remaining_points + t["value"] + value = min(value, available_points) + t["value"] = value + break + +def radar_dynamic(): + return rx.hstack( + rx.recharts.radar_chart( + rx.recharts.radar( + data_key="value", + stroke="#8884d8", + fill="#8884d8", + ), + rx.recharts.polar_grid(), + rx.recharts.polar_angle_axis(data_key="trait"), + data=RadarChartState.traits, + ), + rx.vstack( + rx.foreach( + RadarChartState.trait_names, + lambda trait_name, i: rx.hstack( + rx.text(trait_name, width="7em"), + rx.slider( + default_value=RadarChartState.traits[i]["value"].to(int), + on_change=lambda value: RadarChartState.set_trait(trait_name, value[0]), + width="25vw", + ), + rx.text(RadarChartState.traits[i]['value']), + ), + ), + rx.text("Remaining points: ", RadarChartState.remaining_points), + ), + width="100%", + height="15em", + ) +``` diff --git a/docs/library/graphing/charts/radialbarchart.md b/docs/library/graphing/charts/radialbarchart.md new file mode 100644 index 00000000000..b9c6b93656e --- /dev/null +++ b/docs/library/graphing/charts/radialbarchart.md @@ -0,0 +1,110 @@ +--- +components: + - rx.recharts.RadialBarChart +--- + +# Radial Bar Chart + +```python exec +import reflex as rx +``` + +## Simple Example + +This example demonstrates how to use a `radial_bar_chart` with a `radial_bar`. The `radial_bar_chart` takes in `data` and then the `radial_bar` takes in a `data_key`. A radial bar chart is a circular visualization where data categories are represented by bars extending outward from a central point, with the length of each bar proportional to its value. + +```md alert info +# Fill color supports `rx.color()`, which automatically adapts to dark/light mode changes. +``` + +```python demo graphing +data = [ + {"name": "C", "x": 3, "fill": rx.color("cyan", 9)}, + {"name": "D", "x": 4, "fill": rx.color("blue", 9)}, + {"name": "E", "x": 5, "fill": rx.color("orange", 9)}, + {"name": "F", "x": 6, "fill": rx.color("red", 9)}, + {"name": "G", "x": 7, "fill": rx.color("gray", 9)}, + {"name": "H", "x": 8, "fill": rx.color("green", 9)}, + {"name": "I", "x": 9, "fill": rx.color("accent", 6)}, +] + +def radial_bar_simple(): + return rx.recharts.radial_bar_chart( + rx.recharts.radial_bar( + data_key="x", + min_angle=15, + ), + data=data, + width = "100%", + height = 500, + ) +``` + +## Advanced Example + +The `start_angle` and `end_angle` define the circular arc over which the bars are distributed, while `inner_radius` and `outer_radius` determine the radial extent of the bars from the center. + +```python demo graphing + +data_radial_bar = [ + { + "name": "18-24", + "uv": 31.47, + "pv": 2400, + "fill": "#8884d8" + }, + { + "name": "25-29", + "uv": 26.69, + "pv": 4567, + "fill": "#83a6ed" + }, + { + "name": "30-34", + "uv": -15.69, + "pv": 1398, + "fill": "#8dd1e1" + }, + { + "name": "35-39", + "uv": 8.22, + "pv": 9800, + "fill": "#82ca9d" + }, + { + "name": "40-49", + "uv": -8.63, + "pv": 3908, + "fill": "#a4de6c" + }, + { + "name": "50+", + "uv": -2.63, + "pv": 4800, + "fill": "#d0ed57" + }, + { + "name": "unknown", + "uv": 6.67, + "pv": 4800, + "fill": "#ffc658" + } +] + +def radial_bar_advanced(): + return rx.recharts.radial_bar_chart( + rx.recharts.radial_bar( + data_key="uv", + min_angle=90, + background=True, + label={"fill": '#666', "position": 'insideStart'}, + ), + data=data_radial_bar, + inner_radius="10%", + outer_radius="80%", + start_angle=180, + end_angle=0, + width="100%", + height=300, + ) +``` diff --git a/docs/library/graphing/charts/scatterchart.md b/docs/library/graphing/charts/scatterchart.md new file mode 100644 index 00000000000..c75918ccd33 --- /dev/null +++ b/docs/library/graphing/charts/scatterchart.md @@ -0,0 +1,294 @@ +--- +components: + - rx.recharts.ScatterChart + - rx.recharts.Scatter +--- + +# Scatter Chart + +```python exec +import reflex as rx +``` + +A scatter chart always has two value axes to show one set of numerical data along a horizontal (value) axis and another set of numerical values along a vertical (value) axis. The chart displays points at the intersection of an x and y numerical value, combining these values into single data points. + +## Simple Example + +For a scatter chart we must define an `rx.recharts.scatter()` component for each set of values we wish to plot. Each `rx.recharts.scatter()` component has a `data` prop which clearly states which data source we plot. We also must define `rx.recharts.x_axis()` and `rx.recharts.y_axis()` so that the graph knows what data to plot on each axis. + +```python demo graphing +data01 = [ + { + "x": 100, + "y": 200, + "z": 200 + }, + { + "x": 120, + "y": 100, + "z": 260 + }, + { + "x": 170, + "y": 300, + "z": 400 + }, + { + "x": 170, + "y": 250, + "z": 280 + }, + { + "x": 150, + "y": 400, + "z": 500 + }, + { + "x": 110, + "y": 280, + "z": 200 + } +] + +def scatter_simple(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data01, + fill="#8884d8",), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y"), + width = "100%", + height = 300, + ) +``` + +## Multiple Scatters + +We can also add two scatters on one chart by using two `rx.recharts.scatter()` components, and we can define an `rx.recharts.z_axis()` which represents a third column of data and is represented by the size of the dots in the scatter plot. + +```python demo graphing +data01 = [ + { + "x": 100, + "y": 200, + "z": 200 + }, + { + "x": 120, + "y": 100, + "z": 260 + }, + { + "x": 170, + "y": 300, + "z": 400 + }, + { + "x": 170, + "y": 250, + "z": 280 + }, + { + "x": 150, + "y": 350, + "z": 500 + }, + { + "x": 110, + "y": 280, + "z": 200 + } +] + +data02 = [ + { + "x": 200, + "y": 260, + "z": 240 + }, + { + "x": 240, + "y": 290, + "z": 220 + }, + { + "x": 190, + "y": 290, + "z": 250 + }, + { + "x": 198, + "y": 250, + "z": 210 + }, + { + "x": 180, + "y": 280, + "z": 260 + }, + { + "x": 210, + "y": 220, + "z": 230 + } +] + +def scatter_double(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data01, + fill="#8884d8", + name="A" + ), + rx.recharts.scatter( + data=data02, + fill="#82ca9d", + name="B" + ), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y"), + rx.recharts.z_axis(data_key="z", range=[60, 400], name="score"), + rx.recharts.legend(), + rx.recharts.graphing_tooltip(), + width="100%", + height=300, + ) +``` + +To learn how to use the `x_axis_id` and `y_axis_id` props, check out the Multiple Axis section of the area chart [documentation](/docs/library/graphing/charts/areachart). + +## Dynamic Data + +Chart data tied to a State var causes the chart to automatically update when the +state changes, providing a nice way to visualize data in response to user +interface elements. View the "Data" tab to see the substate driving this +calculation of iterations in the Collatz Conjecture for a given starting number. +Enter a starting number in the box below the chart to recalculate. + +```python demo exec +class ScatterChartState(rx.State): + data: list[dict[str, int]] = [] + + @rx.event + def compute_collatz(self, form_data: dict) -> int: + n = int(form_data.get("start") or 1) + yield rx.set_value("start", "") + self.data = [] + for ix in range(400): + self.data.append({"x": ix, "y": n}) + if n == 1: + break + if n % 2 == 0: + n = n // 2 + else: + n = 3 * n + 1 + + +def scatter_dynamic(): + return rx.vstack( + rx.recharts.scatter_chart( + rx.recharts.scatter( + data=ScatterChartState.data, + fill="#8884d8", + ), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y", type_="number"), + ), + rx.form.root( + rx.input(placeholder="Enter a number", id="start"), + rx.button("Compute", type="submit"), + on_submit=ScatterChartState.compute_collatz, + ), + width="100%", + height="15em", + on_mount=ScatterChartState.compute_collatz({"start": "15"}), + ) +``` + +## Legend Type and Shape + +```python demo exec +class ScatterChartState2(rx.State): + + legend_types: list[str] = ["square", "circle", "cross", "diamond", "star", "triangle", "wye"] + + legend_type: str = "circle" + + shapes: list[str] = ["square", "circle", "cross", "diamond", "star", "triangle", "wye"] + + shape: str = "circle" + + data01 = [ + { + "x": 100, + "y": 200, + "z": 200 + }, + { + "x": 120, + "y": 100, + "z": 260 + }, + { + "x": 170, + "y": 300, + "z": 400 + }, + { + "x": 170, + "y": 250, + "z": 280 + }, + { + "x": 150, + "y": 400, + "z": 500 + }, + { + "x": 110, + "y": 280, + "z": 200 + } + ] + + @rx.event + def set_shape(self, shape: str): + self.shape = shape + + @rx.event + def set_legend_type(self, legend_type: str): + self.legend_type = legend_type + +def scatter_shape(): + return rx.vstack( + rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data01, + fill="#8884d8", + legend_type=ScatterChartState2.legend_type, + shape=ScatterChartState2.shape, + ), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y"), + rx.recharts.legend(), + width = "100%", + height = 300, + ), + rx.hstack( + rx.text("Legend Type: "), + rx.select( + ScatterChartState2.legend_types, + value=ScatterChartState2.legend_type, + on_change=ScatterChartState2.set_legend_type, + ), + rx.text("Shape: "), + rx.select( + ScatterChartState2.shapes, + value=ScatterChartState2.shape, + on_change=ScatterChartState2.set_shape, + ), + ), + width="100%", + ) +``` diff --git a/docs/library/graphing/general/axis.md b/docs/library/graphing/general/axis.md new file mode 100644 index 00000000000..4c2d3d0b75f --- /dev/null +++ b/docs/library/graphing/general/axis.md @@ -0,0 +1,296 @@ +--- +components: + - rx.recharts.XAxis + - rx.recharts.YAxis + - rx.recharts.ZAxis +--- + +```python exec +import reflex as rx +``` + +# Axis + +The Axis component in Recharts is a powerful tool for customizing and configuring the axes of your charts. It provides a wide range of props that allow you to control the appearance, behavior, and formatting of the axis. Whether you're working with an AreaChart, LineChart, or any other chart type, the Axis component enables you to create precise and informative visualizations. + +## Basic Example + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def axis_simple(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis( + data_key="name", + label={"value": 'Pages', "position": "bottom"}, + ), + rx.recharts.y_axis( + data_key="uv", + label={"value": 'Views', "angle": -90, "position": "left"}, + ), + data=data, + width="100%", + height=300, + margin={ + "bottom": 40, + "left": 40, + "right": 40, + }, + ) +``` + +## Multiple Axes + +Multiple axes can be used for displaying different data series with varying scales or units on the same chart. This allows for a more comprehensive comparison and analysis of the data. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def multi_axis(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", stroke="#8884d8", fill="#8884d8", y_axis_id="left", + ), + rx.recharts.area( + data_key="pv", y_axis_id="right", type_="monotone", stroke="#82ca9d", fill="#82ca9d" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(data_key="uv", y_axis_id="left"), + rx.recharts.y_axis(data_key="pv", y_axis_id="right", orientation="right"), + rx.recharts.graphing_tooltip(), + rx.recharts.legend(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Choosing Location of Labels for Axes + +The axes `label` can take several positions. The example below allows you to try out different locations for the x and y axis labels. + +```python demo graphing + +class AxisState(rx.State): + + label_positions: list[str] = ["center", "insideTopLeft", "insideTopRight", "insideBottomRight", "insideBottomLeft", "insideTop", "insideBottom", "insideLeft", "insideRight", "outside", "top", "bottom", "left", "right"] + + label_offsets: list[str] = ["-30", "-20", "-10", "0", "10", "20", "30"] + + x_axis_postion: str = "bottom" + + x_axis_offset: int + + y_axis_postion: str = "left" + + y_axis_offset: int + + @rx.event + @rx.event + def set_y_axis_position(self, position: str): + self.y_axis_position = position + + @rx.event + def set_x_axis_position(self, position: str): + self.x_axis_position = position + + @rx.event + def set_x_axis_offset(self, offset: str): + self.x_axis_offset = int(offset) + + @rx.event + def set_y_axis_offset(self, offset: str): + self.y_axis_offset = int(offset) + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def axis_labels(): + return rx.vstack( + rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis( + data_key="name", + label={"value": 'Pages', "position": AxisState.x_axis_postion, "offset": AxisState.x_axis_offset}, + ), + rx.recharts.y_axis( + data_key="uv", + label={"value": 'Views', "angle": -90, "position": AxisState.y_axis_postion, "offset": AxisState.y_axis_offset}, + ), + data=data, + width="100%", + height=300, + margin={ + "bottom": 40, + "left": 40, + "right": 40, + } + ), + rx.hstack( + rx.text("X Label Position: "), + rx.select( + AxisState.label_positions, + value=AxisState.x_axis_postion, + on_change=AxisState.set_x_axis_postion, + ), + rx.text("X Label Offset: "), + rx.select( + AxisState.label_offsets, + value=AxisState.x_axis_offset.to_string(), + on_change=AxisState.set_x_axis_offset, + ), + rx.text("Y Label Position: "), + rx.select( + AxisState.label_positions, + value=AxisState.y_axis_postion, + on_change=AxisState.set_y_axis_postion, + ), + rx.text("Y Label Offset: "), + rx.select( + AxisState.label_offsets, + value=AxisState.y_axis_offset.to_string(), + on_change=AxisState.set_y_axis_offset, + ), + ), + width="100%", + ) +``` diff --git a/docs/library/graphing/general/brush.md b/docs/library/graphing/general/brush.md new file mode 100644 index 00000000000..9ac8ec6840a --- /dev/null +++ b/docs/library/graphing/general/brush.md @@ -0,0 +1,154 @@ +--- +components: + - rx.recharts.Brush +--- + +# Brush + +```python exec +import reflex as rx +``` + +## Simple Example + +The brush component allows us to view charts that have a large number of data points. To view and analyze them efficiently, the brush provides a slider with two handles that helps the viewer to select some range of data points to be displayed. + +```python demo graphing +data = [ + { "name": '1', "uv": 300, "pv": 456 }, + { "name": '2', "uv": -145, "pv": 230 }, + { "name": '3', "uv": -100, "pv": 345 }, + { "name": '4', "uv": -8, "pv": 450 }, + { "name": '5', "uv": 100, "pv": 321 }, + { "name": '6', "uv": 9, "pv": 235 }, + { "name": '7', "uv": 53, "pv": 267 }, + { "name": '8', "uv": 252, "pv": -378 }, + { "name": '9', "uv": 79, "pv": -210 }, + { "name": '10', "uv": 294, "pv": -23 }, + { "name": '12', "uv": 43, "pv": 45 }, + { "name": '13', "uv": -74, "pv": 90 }, + { "name": '14', "uv": -71, "pv": 130 }, + { "name": '15', "uv": -117, "pv": 11 }, + { "name": '16', "uv": -186, "pv": 107 }, + { "name": '17', "uv": -16, "pv": 926 }, + { "name": '18', "uv": -125, "pv": 653 }, + { "name": '19', "uv": 222, "pv": 366 }, + { "name": '20', "uv": 372, "pv": 486 }, + { "name": '21', "uv": 182, "pv": 512 }, + { "name": '22', "uv": 164, "pv": 302 }, + { "name": '23', "uv": 316, "pv": 425 }, + { "name": '24', "uv": 131, "pv": 467 }, + { "name": '25', "uv": 291, "pv": -190 }, + { "name": '26', "uv": -47, "pv": 194 }, + { "name": '27', "uv": -415, "pv": 371 }, + { "name": '28', "uv": -182, "pv": 376 }, + { "name": '29', "uv": -93, "pv": 295 }, + { "name": '30', "uv": -99, "pv": 322 }, + { "name": '31', "uv": -52, "pv": 246 }, + { "name": '32', "uv": 154, "pv": 33 }, + { "name": '33', "uv": 205, "pv": 354 }, + { "name": '34', "uv": 70, "pv": 258 }, + { "name": '35', "uv": -25, "pv": 359 }, + { "name": '36', "uv": -59, "pv": 192 }, + { "name": '37', "uv": -63, "pv": 464 }, + { "name": '38', "uv": -91, "pv": -2 }, + { "name": '39', "uv": -66, "pv": 154 }, + { "name": '40', "uv": -50, "pv": 186 }, +] + +def brush_simple(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + data_key="pv", + stroke="#82ca9d", + fill="#82ca9d" + ), + rx.recharts.brush(data_key="name", height=30, stroke="#8884d8"), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width="100%", + height=300, + ) +``` + +## Position, Size, and Range + +This example showcases ways to set the Position, Size, and Range. The `gap` prop provides the spacing between stops on the brush when the graph will refresh. The `start_index` and `end_index` props defines the default range of the brush. `traveller_width` prop specifies the width of each handle ("traveller" in recharts lingo). + +```python demo graphing +data = [ + { "name": '1', "uv": 300, "pv": 456 }, + { "name": '2', "uv": -145, "pv": 230 }, + { "name": '3', "uv": -100, "pv": 345 }, + { "name": '4', "uv": -8, "pv": 450 }, + { "name": '5', "uv": 100, "pv": 321 }, + { "name": '6', "uv": 9, "pv": 235 }, + { "name": '7', "uv": 53, "pv": 267 }, + { "name": '8', "uv": 252, "pv": -378 }, + { "name": '9', "uv": 79, "pv": -210 }, + { "name": '10', "uv": 294, "pv": -23 }, + { "name": '12', "uv": 43, "pv": 45 }, + { "name": '13', "uv": -74, "pv": 90 }, + { "name": '14', "uv": -71, "pv": 130 }, + { "name": '15', "uv": -117, "pv": 11 }, + { "name": '16', "uv": -186, "pv": 107 }, + { "name": '17', "uv": -16, "pv": 926 }, + { "name": '18', "uv": -125, "pv": 653 }, + { "name": '19', "uv": 222, "pv": 366 }, + { "name": '20', "uv": 372, "pv": 486 }, + { "name": '21', "uv": 182, "pv": 512 }, + { "name": '22', "uv": 164, "pv": 302 }, + { "name": '23', "uv": 316, "pv": 425 }, + { "name": '24', "uv": 131, "pv": 467 }, + { "name": '25', "uv": 291, "pv": -190 }, + { "name": '26', "uv": -47, "pv": 194 }, + { "name": '27', "uv": -415, "pv": 371 }, + { "name": '28', "uv": -182, "pv": 376 }, + { "name": '29', "uv": -93, "pv": 295 }, + { "name": '30', "uv": -99, "pv": 322 }, + { "name": '31', "uv": -52, "pv": 246 }, + { "name": '32', "uv": 154, "pv": 33 }, + { "name": '33', "uv": 205, "pv": 354 }, + { "name": '34', "uv": 70, "pv": 258 }, + { "name": '35', "uv": -25, "pv": 359 }, + { "name": '36', "uv": -59, "pv": 192 }, + { "name": '37', "uv": -63, "pv": 464 }, + { "name": '38', "uv": -91, "pv": -2 }, + { "name": '39', "uv": -66, "pv": 154 }, + { "name": '40', "uv": -50, "pv": 186 }, +] + +def brush_pos_size_range(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8", + ), + rx.recharts.area( + data_key="pv", + stroke="#82ca9d", + fill="#82ca9d", + ), + rx.recharts.brush( + data_key="name", + traveller_width=15, + start_index=3, + end_index=10, + stroke=rx.color("mauve", 10), + fill=rx.color("mauve", 3), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width="100%", + height=200, + ) + +``` diff --git a/docs/library/graphing/general/cartesiangrid.md b/docs/library/graphing/general/cartesiangrid.md new file mode 100644 index 00000000000..9e52a772f7a --- /dev/null +++ b/docs/library/graphing/general/cartesiangrid.md @@ -0,0 +1,204 @@ +--- +components: + - rx.recharts.CartesianGrid + # - rx.recharts.CartesianAxis +--- + +```python exec +import reflex as rx +``` + +# Cartesian Grid + +The Cartesian Grid is a component in Recharts that provides a visual reference for data points in charts. It helps users to better interpret the data by adding horizontal and vertical lines across the chart area. + +## Simple Example + +The `stroke_dasharray` prop in Recharts is used to create dashed or dotted lines for various chart elements like lines, axes, or grids. It's based on the SVG stroke-dasharray attribute. The `stroke_dasharray` prop accepts a comma-separated string of numbers that define a repeating pattern of dashes and gaps along the length of the stroke. + +- `stroke_dasharray="5,5"`: creates a line with 5-pixel dashes and 5-pixel gaps +- `stroke_dasharray="10,5,5,5"`: creates a more complex pattern with 10-pixel dashes, 5-pixel gaps, 5-pixel dashes, and 5-pixel gaps + +Here's a simple example using it on a Line component: + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def cgrid_simple(): + return rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="4 4"), + data=data, + width = "100%", + height = 300, + ) +``` + +## Hidden Axes + +A `cartesian_grid` component can be used to hide the horizontal and vertical grid lines in a chart by setting the `horizontal` and `vertical` props to `False`. This can be useful when you want to show the grid lines only on one axis or when you want to create a cleaner look for the chart. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def cgrid_hidden(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid( + stroke_dasharray="2 4", + vertical=False, + horizontal=True, + ), + data=data, + width = "100%", + height = 300, + ) +``` + +## Custom Grid Lines + +The `horizontal_points` and `vertical_points` props allow you to specify custom grid lines on the chart, offering fine-grained control over the grid's appearance. + +These props accept arrays of numbers, where each number represents a pixel offset: + +- For `horizontal_points`, the offset is measured from the top edge of the chart +- For `vertical_points`, the offset is measured from the left edge of the chart + +```md alert info +# **Important**: The values provided to these props are not directly related to the axis values. They represent pixel offsets within the chart's rendering area. +``` + +Here's an example demonstrating custom grid lines in a scatter chart: + +```python demo graphing + +data2 = [ + {"x": 100, "y": 200, "z": 200}, + {"x": 120, "y": 100, "z": 260}, + {"x": 170, "y": 300, "z": 400}, + {"x": 170, "y": 250, "z": 280}, + {"x": 150, "y": 400, "z": 500}, + {"x": 110, "y": 280, "z": 200}, + {"x": 200, "y": 150, "z": 300}, + {"x": 130, "y": 350, "z": 450}, + {"x": 90, "y": 220, "z": 180}, + {"x": 180, "y": 320, "z": 350}, + {"x": 140, "y": 230, "z": 320}, + {"x": 160, "y": 180, "z": 240}, +] + +def cgrid_custom(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data2, + fill="#8884d8", + ), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y"), + rx.recharts.cartesian_grid( + stroke_dasharray="3 3", + horizontal_points=[0, 25, 50], + vertical_points=[65, 90, 115], + ), + width = "100%", + height = 200, + ) +``` + +Use these props judiciously to enhance data visualization without cluttering the chart. They're particularly useful for highlighting specific data ranges or creating visual reference points. diff --git a/docs/library/graphing/general/label.md b/docs/library/graphing/general/label.md new file mode 100644 index 00000000000..0ea0caeb7ac --- /dev/null +++ b/docs/library/graphing/general/label.md @@ -0,0 +1,180 @@ +--- +components: + - rx.recharts.Label + - rx.recharts.LabelList +--- + +# Label + +```python exec +import reflex as rx +``` + +Label is a component used to display a single label at a specific position within a chart or axis, while LabelList is a component that automatically renders a list of labels for each data point in a chart series, providing a convenient way to display multiple labels without manually positioning each one. + +## Simple Example + +Here's a simple example that demonstrates how you can customize the label of your axis using `rx.recharts.label`. The `value` prop represents the actual text of the label, the `position` prop specifies where the label is positioned within the axis component, and the `offset` prop is used to fine-tune the label's position. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 5800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def label_simple(): + return rx.recharts.bar_chart( + rx.recharts.cartesian_grid( + stroke_dasharray="3 3" + ), + rx.recharts.bar( + rx.recharts.label_list( + data_key="uv", position="top" + ), + data_key="uv", + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis( + rx.recharts.label( + value="center", + position="center", + offset=30, + ), + rx.recharts.label( + value="inside left", + position="insideLeft", + offset=10, + ), + rx.recharts.label( + value="inside right", + position="insideRight", + offset=10, + ), + height=50, + ), + data=data, + margin={ + "left": 20, + "right": 20, + "top": 20, + "bottom": 20, + }, + width="100%", + height=250, + ) +``` + +## Label List Example + +`rx.recharts.label_list` takes in a `data_key` where we define the data column to plot. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 5800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def label_list(): + return rx.recharts.bar_chart( + rx.recharts.bar( + rx.recharts.label_list(data_key="uv", position="top"), + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + rx.recharts.label_list(data_key="pv", position="top"), + data_key="pv", + stroke="#82ca9d", + fill="#82ca9d" + ), + rx.recharts.x_axis( + data_key="name" + ), + rx.recharts.y_axis(), + margin={"left": 10, "right": 0, "top": 20, "bottom": 10}, + data=data, + width="100%", + height = 300, + ) +``` diff --git a/docs/library/graphing/general/legend.md b/docs/library/graphing/general/legend.md new file mode 100644 index 00000000000..0f60447905c --- /dev/null +++ b/docs/library/graphing/general/legend.md @@ -0,0 +1,171 @@ +--- +components: + - rx.recharts.Legend +--- + +# Legend + +```python exec +import reflex as rx +``` + +A legend tells what each plot represents. Just like on a map, the legend helps the reader understand what they are looking at. For a line graph for example it tells us what each line represents. + +## Simple Example + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def legend_simple(): + return rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + data_key="amt", + bar_size=20, + fill="#413ea0" + ), + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke="#ff7300" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.legend(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Example with Props + +The style and layout of the legend can be customized using a set of props. `width` and `height` set the dimensions of the container that wraps the legend, and `layout` can set the legend to display vertically or horizontally. `align` and `vertical_align` set the position relative to the chart container. The type and size of icons can be set using `icon_size` and `icon_type`. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def legend_props(): + return rx.recharts.composed_chart( + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke=rx.color("accent", 7), + ), + rx.recharts.line( + data_key="amt", + type_="monotone", + stroke=rx.color("green", 7), + ), + rx.recharts.line( + data_key="uv", + type_="monotone", + stroke=rx.color("red", 7), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.legend( + width=60, + height=100, + layout="vertical", + align="right", + vertical_align="top", + icon_size=15, + icon_type="square", + ), + data=data, + width="100%", + height=300, + ) + +``` diff --git a/docs/library/graphing/general/reference.md b/docs/library/graphing/general/reference.md new file mode 100644 index 00000000000..a470d282bc6 --- /dev/null +++ b/docs/library/graphing/general/reference.md @@ -0,0 +1,258 @@ +--- +components: + - rx.recharts.ReferenceLine + - rx.recharts.ReferenceDot + - rx.recharts.ReferenceArea +--- + +# Reference + +```python exec +import reflex as rx +``` + +The Reference components in Recharts, including ReferenceLine, ReferenceArea, and ReferenceDot, are used to add visual aids and annotations to the chart, helping to highlight specific data points, ranges, or thresholds for better data interpretation and analysis. + +## Reference Area + +The `rx.recharts.reference_area` component in Recharts is used to highlight a specific area or range on the chart by drawing a rectangular region. It is defined by specifying the coordinates (x1, x2, y1, y2) and can be used to emphasize important data ranges or intervals on the chart. + +```python demo graphing +data = [ + { + "x": 45, + "y": 100, + "z": 150, + "errorY": [ + 30, + 20 + ], + "errorX": 5 + }, + { + "x": 100, + "y": 200, + "z": 200, + "errorY": [ + 20, + 30 + ], + "errorX": 3 + }, + { + "x": 120, + "y": 100, + "z": 260, + "errorY": 20, + "errorX": [ + 10, + 3 + ] + }, + { + "x": 170, + "y": 300, + "z": 400, + "errorY": [ + 15, + 18 + ], + "errorX": 4 + }, + { + "x": 140, + "y": 250, + "z": 280, + "errorY": 23, + "errorX": [ + 6, + 7 + ] + }, + { + "x": 150, + "y": 400, + "z": 500, + "errorY": [ + 21, + 10 + ], + "errorX": 4 + }, + { + "x": 110, + "y": 280, + "z": 200, + "errorY": 21, + "errorX": [ + 1, + 8 + ] + } +] + +def reference(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data, + fill="#8884d8", + name="A"), + rx.recharts.reference_area(x1= 150, x2=180, y1=150, y2=300, fill="#8884d8", fill_opacity=0.3), + rx.recharts.x_axis(data_key="x", name="x", type_="number"), + rx.recharts.y_axis(data_key="y", name="y", type_="number"), + rx.recharts.graphing_tooltip(), + width = "100%", + height = 300, + ) +``` + +## Reference Line + +The `rx.recharts.reference_line` component in rx.recharts is used to draw a horizontal or vertical line on the chart at a specified position. It helps to highlight important values, thresholds, or ranges on the axis, providing visual reference points for better data interpretation. + +```python demo graphing +data_2 = [ + {"name": "Page A", "uv": 4000, "pv": 2400, "amt": 2400}, + {"name": "Page B", "uv": 3000, "pv": 1398, "amt": 2210}, + {"name": "Page C", "uv": 2000, "pv": 9800, "amt": 2290}, + {"name": "Page D", "uv": 2780, "pv": 3908, "amt": 2000}, + {"name": "Page E", "uv": 1890, "pv": 4800, "amt": 2181}, + {"name": "Page F", "uv": 2390, "pv": 3800, "amt": 2500}, + {"name": "Page G", "uv": 3490, "pv": 4300, "amt": 2100}, +] + +def reference_line(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="pv", stroke=rx.color("accent", 8), fill=rx.color("accent", 7), + ), + rx.recharts.reference_line( + x = "Page C", + stroke = rx.color("accent", 10), + label="Max PV PAGE", + ), + rx.recharts.reference_line( + y = 9800, + stroke = rx.color("green", 10), + label="Max" + ), + rx.recharts.reference_line( + y = 4343, + stroke = rx.color("green", 10), + label="Average" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data_2, + height = 300, + width = "100%", + ) + +``` + +## Reference Dot + +The `rx.recharts.reference_dot` component in Recharts is used to mark a specific data point on the chart with a customizable dot. It allows you to highlight important values, outliers, or thresholds by providing a visual reference marker at the specified coordinates (x, y) on the chart. + +```python demo graphing + +data_3 = [ + { + "x": 45, + "y": 100, + "z": 150, + }, + { + "x": 100, + "y": 200, + "z": 200, + }, + { + "x": 120, + "y": 100, + "z": 260, + }, + { + "x": 170, + "y": 300, + "z": 400, + }, + { + "x": 140, + "y": 250, + "z": 280, + }, + { + "x": 150, + "y": 400, + "z": 500, + }, + { + "x": 110, + "y": 280, + "z": 200, + }, + { + "x": 80, + "y": 150, + "z": 180, + }, + { + "x": 200, + "y": 350, + "z": 450, + }, + { + "x": 90, + "y": 220, + "z": 240, + }, + { + "x": 130, + "y": 320, + "z": 380, + }, + { + "x": 180, + "y": 120, + "z": 300, + }, +] + +def reference_dot(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data_3, + fill=rx.color("accent", 9), + name="A", + ), + rx.recharts.x_axis( + data_key="x", name="x", type_="number" + ), + rx.recharts.y_axis( + data_key="y", name="y", type_="number" + ), + rx.recharts.reference_dot( + x = 160, + y = 350, + r = 15, + fill = rx.color("accent", 5), + stroke = rx.color("accent", 10), + ), + rx.recharts.reference_dot( + x = 170, + y = 300, + r = 20, + fill = rx.color("accent", 7), + ), + rx.recharts.reference_dot( + x = 90, + y = 220, + r = 18, + fill = rx.color("green", 7), + ), + height = 200, + width = "100%", + ) + +``` diff --git a/docs/library/graphing/general/tooltip.md b/docs/library/graphing/general/tooltip.md new file mode 100644 index 00000000000..c89037bd6f7 --- /dev/null +++ b/docs/library/graphing/general/tooltip.md @@ -0,0 +1,172 @@ +--- +components: + - rx.recharts.GraphingTooltip +--- + +# Tooltip + +```python exec +import reflex as rx +``` + +Tooltips are the little boxes that pop up when you hover over something. Tooltips are always attached to something, like a dot on a scatter chart, or a bar on a bar chart. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def tooltip_simple(): + return rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + data_key="amt", + bar_size=20, + fill="#413ea0" + ), + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke="#ff7300" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.graphing_tooltip(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Custom Styling + +The `rx.recharts.graphing_tooltip` component allows for customization of the tooltip's style, position, and layout. `separator` sets the separator between the data key and value. `view_box` prop defines the dimensions of the chart's viewbox while `allow_escape_view_box` determines whether the tooltip can extend beyond the viewBox horizontally (x) or vertically (y). `wrapper_style` prop allows you to style the outer container or wrapper of the tooltip. `content_style` prop allows you to style the inner content area of the tooltip. `is_animation_active` prop determines if the tooltip animation is active or not. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def tooltip_custom_styling(): + return rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", stroke="#8884d8", fill="#8884d8" + ), + rx.recharts.bar( + data_key="amt", bar_size=20, fill="#413ea0" + ), + rx.recharts.line( + data_key="pv", type_="monotone", stroke="#ff7300" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.graphing_tooltip( + separator = " - ", + view_box = {"width" : 675, " height" : 300 }, + allow_escape_view_box={"x": True, "y": False}, + wrapper_style={ + "backgroundColor": rx.color("accent", 3), + "borderRadius": "8px", + "padding": "10px", + }, + content_style={ + "backgroundColor": rx.color("accent", 4), + "borderRadius": "4px", + "padding": "8px", + }, + position = {"x" : 600, "y" : 0}, + is_animation_active = False, + ), + data=data, + height = 300, + width = "100%", + ) +``` diff --git a/docs/library/graphing/other-charts/plotly.md b/docs/library/graphing/other-charts/plotly.md new file mode 100644 index 00000000000..3d036fac706 --- /dev/null +++ b/docs/library/graphing/other-charts/plotly.md @@ -0,0 +1,144 @@ +--- +components: + - rx.plotly +--- + +# Plotly + +```python exec +import reflex as rx +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +``` + +Plotly is a graphing library that can be used to create interactive graphs. Use the rx.plotly component to wrap Plotly as a component for use in your web page. Checkout [Plotly](https://plotly.com/graphing-libraries/) for more information. + +```md alert info +# When integrating Plotly graphs into your UI code, note that the method for displaying the graph differs from a regular Python script. Instead of using `fig.show()`, use `rx.plotly(data=fig)` within your UI code to ensure the graph is properly rendered and displayed within the user interface +``` + +## Basic Example + +Let's create a line graph of life expectancy in Canada. + +```python demo exec +import plotly.express as px + +df = px.data.gapminder().query("country=='Canada'") +fig = px.line(df, x="year", y="lifeExp", title='Life expectancy in Canada') + +def line_chart(): + return rx.center( + rx.plotly(data=fig), + ) +``` + +## 3D graphing example + +Let's create a 3D surface plot of Mount Bruno. This is a slightly more complicated example, but it wraps in Reflex using the same method. In fact, you can wrap any figure using the same approach. + +```python demo exec +import plotly.graph_objects as go +import pandas as pd + +# Read data from a csv +z_data = pd.read_csv('data/mt_bruno_elevation.csv') + +fig = go.Figure(data=[go.Surface(z=z_data.values)]) +fig.update_traces(contours_z=dict(show=True, usecolormap=True, + highlightcolor="limegreen", project_z=True)) +fig.update_layout( + scene_camera_eye=dict(x=1.87, y=0.88, z=-0.64), + margin=dict(l=65, r=50, b=65, t=90) +) + +def mountain_surface(): + return rx.center( + rx.plotly(data=fig), + ) +``` + +📊 **Dataset source:** [mt_bruno_elevation.csv](https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv) + +## Plot as State Var + +If the figure is set as a state var, it can be updated during run time. + +```python demo exec +import plotly.express as px +import plotly.graph_objects as go +import pandas as pd + +class PlotlyState(rx.State): + df: pd.DataFrame + figure: go.Figure = px.line() + + @rx.event + def create_figure(self): + self.df = px.data.gapminder().query("country=='Canada'") + self.figure = px.line( + self.df, + x="year", + y="lifeExp", + title="Life expectancy in Canada", + ) + + @rx.event + def set_selected_country(self, country): + self.df = px.data.gapminder().query(f"country=='{country}'") + self.figure = px.line( + self.df, + x="year", + y="lifeExp", + title=f"Life expectancy in {country}", + ) + + + +def line_chart_with_state(): + return rx.vstack( + rx.select( + ['China', 'France', 'United Kingdom', 'United States', 'Canada'], + default_value="Canada", + on_change=PlotlyState.set_selected_country, + ), + rx.plotly( + data=PlotlyState.figure, + on_mount=PlotlyState.create_figure, + ), + ) +``` + +## Adding Styles and Layouts + +Use `update_layout()` method to update the layout of your chart. Checkout [Plotly Layouts](https://plotly.com/python/reference/layout/) for all layouts props. + +```md alert info +Note that the width and height props are not recommended to ensure the plot remains size responsive to its container. The size of plot will be determined by it's outer container. +``` + +```python demo exec +df = px.data.gapminder().query("country=='Canada'") +fig_1 = px.line( + df, + x="year", + y="lifeExp", + title="Life expectancy in Canada", +) +fig_1.update_layout( + title_x=0.5, + plot_bgcolor="#c3d7f7", + paper_bgcolor="rgba(128, 128, 128, 0.1)", + showlegend=True, + title_font_family="Open Sans", + title_font_size=25, +) + +def add_styles(): + return rx.center( + rx.plotly(data=fig_1), + width="100%", + height="100%", + ) +``` diff --git a/docs/library/graphing/other-charts/pyplot.md b/docs/library/graphing/other-charts/pyplot.md new file mode 100644 index 00000000000..5e57071bf4d --- /dev/null +++ b/docs/library/graphing/other-charts/pyplot.md @@ -0,0 +1,155 @@ +--- +components: + - pyplot +--- + +```python exec +import reflex as rx +from reflex_pyplot import pyplot +import numpy as np +import random +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from reflex.style import toggle_color_mode +``` + +# Pyplot + +Pyplot (`reflex-pyplot`) is a graphing library that wraps Matplotlib. Use the `pyplot` component to display any Matplotlib plot in your app. Check out [Matplotlib](https://matplotlib.org/) for more information. + +## Installation + +Install the `reflex-pyplot` package using pip. + +```bash +pip install reflex-pyplot +``` + +## Basic Example + +To display a Matplotlib plot in your app, you can use the `pyplot` component. Pass in the figure you created with Matplotlib to the `pyplot` component as a child. + +```python demo exec +import matplotlib.pyplot as plt +import reflex as rx +from reflex_pyplot import pyplot +import numpy as np + +def create_contour_plot(): + X, Y = np.meshgrid(np.linspace(-3, 3, 256), np.linspace(-3, 3, 256)) + Z = (1 - X/2 + X**5 + Y**3) * np.exp(-X**2 - Y**2) + levels = np.linspace(Z.min(), Z.max(), 7) + + fig, ax = plt.subplots() + ax.contourf(X, Y, Z, levels=levels) + plt.close(fig) + return fig + +def pyplot_simple_example(): + return rx.card( + pyplot(create_contour_plot(), width="100%", height="400px"), + bg_color='#ffffff', + width="100%", + ) +``` + +```md alert info +# You must close the figure after creating + +Not closing the figure could cause memory issues. +``` + +## Stateful Example + +Lets create a scatter plot of random data. We'll also allow the user to randomize the data and change the number of points. + +In this example, we'll use a `color_mode_cond` to display the plot in both light and dark mode. We need to do this manually here because the colors are determined by the matplotlib chart and not the theme. + +```python demo exec +import random +from typing import Literal +import matplotlib.pyplot as plt +import reflex as rx +from reflex_pyplot import pyplot +import numpy as np + + +def create_plot(theme: str, plot_data: tuple, scale: list): + bg_color, text_color = ('#1e1e1e', 'white') if theme == 'dark' else ('white', 'black') + grid_color = '#555555' if theme == 'dark' else '#cccccc' + + fig, ax = plt.subplots(facecolor=bg_color) + ax.set_facecolor(bg_color) + + for (x, y), color in zip(plot_data, ["#4e79a7", "#f28e2b"]): + ax.scatter(x, y, c=color, s=scale, label=color, alpha=0.6, edgecolors="none") + + ax.legend(loc="upper right", facecolor=bg_color, edgecolor='none', labelcolor=text_color) + ax.grid(True, color=grid_color) + ax.tick_params(colors=text_color) + for spine in ax.spines.values(): + spine.set_edgecolor(text_color) + + for item in [ax.xaxis.label, ax.yaxis.label, ax.title]: + item.set_color(text_color) + plt.close(fig) + + return fig + +class PyplotState(rx.State): + num_points: int = 25 + plot_data: tuple = tuple(np.random.rand(2, 25) for _ in range(2)) + scale: list = [random.uniform(0, 100) for _ in range(25)] + + @rx.event(temporal=True, throttle=500) + def randomize(self): + self.plot_data = tuple(np.random.rand(2, self.num_points) for _ in range(2)) + self.scale = [random.uniform(0, 100) for _ in range(self.num_points)] + + @rx.event(temporal=True, throttle=500) + def set_num_points(self, num_points: list[int | float]): + self.num_points = int(num_points[0]) + yield PyplotState.randomize() + + @rx.var + def fig_light(self) -> Figure: + fig = create_plot("light", self.plot_data, self.scale) + return fig + + @rx.var + def fig_dark(self) -> Figure: + fig = create_plot("dark", self.plot_data, self.scale) + return fig + +def pyplot_example(): + return rx.vstack( + rx.card( + rx.color_mode_cond( + pyplot(PyplotState.fig_light, width="100%", height="100%"), + pyplot(PyplotState.fig_dark, width="100%", height="100%"), + ), + rx.vstack( + rx.hstack( + rx.button( + "Randomize", + on_click=PyplotState.randomize, + ), + rx.text("Number of Points:"), + rx.slider( + default_value=25, + min_=10, + max=100, + on_value_commit=PyplotState.set_num_points, + ), + width="100%", + ), + width="100%", + ), + width="100%", + ), + justify_content="center", + align_items="center", + height="100%", + width="100%", + ) +``` diff --git a/docs/library/layout/aspect_ratio.md b/docs/library/layout/aspect_ratio.md new file mode 100644 index 00000000000..0b9326e9612 --- /dev/null +++ b/docs/library/layout/aspect_ratio.md @@ -0,0 +1,84 @@ +--- +components: + - rx.aspect_ratio +--- + +```python exec +import reflex as rx +``` + +# Aspect Ratio + +Displays content with a desired ratio. + +## Basic Example + +Setting the `ratio` prop will adjust the width or height +of the content such that the `width` divided by the `height` equals the `ratio`. +For responsive scaling, set the `width` or `height` of the content to `"100%"`. + +```python demo +rx.grid( + rx.aspect_ratio( + rx.box( + "Widescreen 16:9", + background_color="papayawhip", + width="100%", + height="100%", + ), + ratio=16 / 9, + ), + rx.aspect_ratio( + rx.box( + "Letterbox 4:3", + background_color="orange", + width="100%", + height="100%", + ), + ratio=4 / 3, + ), + rx.aspect_ratio( + rx.box( + "Square 1:1", + background_color="green", + width="100%", + height="100%", + ), + ratio=1, + ), + rx.aspect_ratio( + rx.box( + "Portrait 5:7", + background_color="lime", + width="100%", + height="100%", + ), + ratio=5 / 7, + ), + spacing="2", + width="25%", +) +``` + +```md alert warning +# Never set `height` or `width` directly on an `aspect_ratio` component or its contents. + +Instead, wrap the `aspect_ratio` in a `box` that constrains either the width or the height, then set the content width and height to `"100%"`. +``` + +```python demo +rx.flex( + *[ + rx.box( + rx.aspect_ratio( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="100%", height="100%"), + ratio=ratio, + ), + width="20%", + ) + for ratio in [16 / 9, 3 / 2, 2 / 3, 1] + ], + justify="between", + width="100%", +) +``` diff --git a/docs/library/layout/box.md b/docs/library/layout/box.md new file mode 100644 index 00000000000..c170f397066 --- /dev/null +++ b/docs/library/layout/box.md @@ -0,0 +1,46 @@ +--- +components: + - rx.box +--- + +```python exec +import reflex as rx +``` + +# Box + +Box is a generic container component that can be used to group other components. + +By default, the Box component is based on the `div` and rendered as a block element. It's primary use is for applying styles. + +## Basic Example + +```python demo +rx.box( + rx.box("CSS color", background_color="yellow", border_radius="2px", width="20%", margin="4px", padding="4px"), + rx.box("CSS color", background_color="orange", border_radius="5px", width="40%", margin="8px", padding="8px"), + rx.box("Radix Color", background_color="var(--tomato-3)", border_radius="5px", width="60%", margin="12px", padding="12px"), + rx.box("Radix Color", background_color="var(--plum-3)", border_radius="10px", width="80%", margin="16px", padding="16px"), + rx.box("Radix Theme Color", background_color="var(--accent-2)", radius="full", width="100%", margin="24px", padding="25px"), + flex_grow="1", + text_align="center", +) +``` + +## Background + +To set a background [image](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images) or +[gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images/Using_CSS_gradients), +use the [`background` CSS prop](https://developer.mozilla.org/en-US/docs/Web/CSS/background). + +```python demo +rx.flex( + rx.box(background="linear-gradient(45deg, var(--tomato-9), var(--plum-9))", width="20%", height="100%"), + rx.box(background="linear-gradient(red, yellow, blue, orange)", width="20%", height="100%"), + rx.box(background="radial-gradient(at 0% 30%, red 10px, yellow 30%, #1e90ff 50%)", width="20%", height="100%"), + rx.box(background="center/cover url('https://web.reflex-assets.dev/other/reflex_banner.png')", width="20%", height="100%"), + spacing="2", + width="100%", + height="10vh", +) +``` diff --git a/docs/library/layout/card.md b/docs/library/layout/card.md new file mode 100644 index 00000000000..844a542be22 --- /dev/null +++ b/docs/library/layout/card.md @@ -0,0 +1,58 @@ +--- +components: + - rx.card + +Card: | + lambda **props: rx.card("Basic Card ", **props) +--- + +```python exec +import reflex as rx +``` + +# Card + +A Card component is used for grouping related components. It is similar to the Box, except it has a +border, uses the theme colors and border radius, and provides a `size` prop to control spacing +and margin according to the Radix `"1"` - `"5"` scale. + +The Card requires less styling than a Box to achieve consistent visual results when used with +themes. + +## Basic Example + +```python demo +rx.flex( + rx.card("Card 1", size="1"), + rx.card("Card 2", size="2"), + rx.card("Card 3", size="3"), + rx.card("Card 4", size="4"), + rx.card("Card 5", size="5"), + spacing="2", + align_items="flex-start", + flex_wrap="wrap", +) +``` + +## Rendering as a Different Element + +The `as_child` prop may be used to render the Card as a different element. Link and Button are +commonly used to make a Card clickable. + +```python demo +rx.card( + rx.link( + rx.flex( + rx.avatar(src="https://web.reflex-assets.dev/other/reflex_banner.png"), + rx.box( + rx.heading("Quick Start"), + rx.text("Get started with Reflex in 5 minutes."), + ), + spacing="2", + ), + ), + as_child=True, +) +``` + +## Using Inset Content diff --git a/docs/library/layout/center.md b/docs/library/layout/center.md new file mode 100644 index 00000000000..3d422553680 --- /dev/null +++ b/docs/library/layout/center.md @@ -0,0 +1,21 @@ +--- +components: + - rx.center +--- + +```python exec +import reflex as rx +``` + +# Center + +`Center` is a component that centers its children within itself. It is based on the `flex` component and therefore inherits all of its props. + +```python demo +rx.center( + rx.text("Hello World!"), + border_radius="15px", + border_width="thick", + width="50%", +) +``` diff --git a/docs/library/layout/container.md b/docs/library/layout/container.md new file mode 100644 index 00000000000..caeab8c84a1 --- /dev/null +++ b/docs/library/layout/container.md @@ -0,0 +1,40 @@ +--- +components: + - rx.container +--- + +```python exec +import reflex as rx +``` + +# Container + +Constrains the maximum width of page content, while keeping flexible margins +for responsive layouts. + +A Container is generally used to wrap the main content for a page. + +## Basic Example + +```python demo +rx.box( + rx.container( + rx.card("This content is constrained to a max width of 448px.", width="100%"), + size="1", + ), + rx.container( + rx.card("This content is constrained to a max width of 688px.", width="100%"), + size="2", + ), + rx.container( + rx.card("This content is constrained to a max width of 880px.", width="100%"), + size="3", + ), + rx.container( + rx.card("This content is constrained to a max width of 1136px.", width="100%"), + size="4", + ), + background_color="var(--gray-3)", + width="100%", +) +``` diff --git a/docs/library/layout/flex.md b/docs/library/layout/flex.md new file mode 100644 index 00000000000..181cc5317ae --- /dev/null +++ b/docs/library/layout/flex.md @@ -0,0 +1,208 @@ +--- +components: + - rx.flex +--- + +```python exec +import reflex as rx +``` + +# Flex + +The Flex component is used to make [flexbox layouts](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox). +It makes it simple to arrange child components in horizontal or vertical directions, apply wrapping, +justify and align content, and automatically size components based on available space, making it +ideal for building responsive layouts. + +By default, children are arranged horizontally (`direction="row"`) without wrapping. + +## Basic Example + +```python demo +rx.flex( + rx.card("Card 1"), + rx.card("Card 2"), + rx.card("Card 3"), + rx.card("Card 4"), + rx.card("Card 5"), + spacing="2", + width="100%", +) +``` + +## Wrapping + +With `flex_wrap="wrap"`, the children will wrap to the next line instead of being resized. + +```python demo +rx.flex( + rx.foreach( + rx.Var.range(10), + lambda i: rx.card(f"Card {i + 1}", width="16%"), + ), + spacing="2", + flex_wrap="wrap", + width="100%", +) +``` + +## Direction + +With `direction="column"`, the children will be arranged vertically. + +```python demo +rx.flex( + rx.card("Card 1"), + rx.card("Card 2"), + rx.card("Card 3"), + rx.card("Card 4"), + spacing="2", + direction="column", +) +``` + +## Alignment + +Two props control how children are aligned within the Flex component: + +- `align` controls how children are aligned along the cross axis (vertical for `row` and horizontal for `column`). +- `justify` controls how children are aligned along the main axis (horizontal for `row` and vertical for `column`). + +The following example visually demonstrates the effect of these props with different `wrap` and `direction` values. + +```python demo exec +class FlexPlaygroundState(rx.State): + align: str = "stretch" + justify: str = "start" + direction: str = "row" + wrap: str = "nowrap" + + @rx.event + def set_align(self, value: str): + self.align = value + + @rx.event + def set_justify(self, value: str): + self.justify = value + + @rx.event + def set_direction(self, value: str): + self.direction = value + + @rx.event + def set_wrap(self, value: str): + self.wrap = value + + +def select(label, items, value, on_change): + return rx.flex( + rx.text(label), + rx.select.root( + rx.select.trigger(), + rx.select.content( + *[ + rx.select.item(item, value=item) + for item in items + ] + ), + value=value, + on_change=on_change, + ), + align="center", + justify="center", + direction="column", + ) + + +def selectors(): + return rx.flex( + select("Wrap", ["nowrap", "wrap", "wrap-reverse"], FlexPlaygroundState.wrap, FlexPlaygroundState.set_wrap), + select("Direction", ["row", "column", "row-reverse", "column-reverse"], FlexPlaygroundState.direction, FlexPlaygroundState.set_direction), + select("Align", ["start", "center", "end", "baseline", "stretch"], FlexPlaygroundState.align, FlexPlaygroundState.set_align), + select("Justify", ["start", "center", "end", "between"], FlexPlaygroundState.justify, FlexPlaygroundState.set_justify), + width="100%", + spacing="2", + justify="between", + ) + + +def example1(): + return rx.box( + selectors(), + rx.flex( + rx.foreach( + rx.Var.range(10), + lambda i: rx.card(f"Card {i + 1}", width="16%"), + ), + spacing="2", + direction=FlexPlaygroundState.direction, + align=FlexPlaygroundState.align, + justify=FlexPlaygroundState.justify, + wrap=FlexPlaygroundState.wrap, + width="100%", + height="20vh", + margin_top="16px", + ), + width="100%", + ) +``` + +## Size Hinting + +When a child component is included in a flex container, +the `flex_grow` (default `"0"`) and `flex_shrink` (default `"1"`) props control +how the box is sized relative to other components in the same container. + +The resizing always applies to the main axis of the flex container. If the direction is +`row`, then the sizing applies to the `width`. If the direction is `column`, then the sizing +applies to the `height`. To set the optimal size along the main axis, the `flex_basis` prop +is used and may be either a percentage or CSS size units. When unspecified, the +corresponding `width` or `height` value is used if set, otherwise the content size is used. + +When `flex_grow="0"`, the box will not grow beyond the `flex_basis`. + +When `flex_shrink="0"`, the box will not shrink to less than the `flex_basis`. + +These props are used when creating flexible responsive layouts. + +Move the slider below and see how adjusting the width of the flex container +affects the computed sizes of the flex items based on the props that are set. + +```python demo exec +class FlexGrowShrinkState(rx.State): + width_pct: list[int] = [100] + + @rx.event + def set_width_pct(self, value: list[int | float]): + self.width_pct = [int(value[0])] + + +def border_box(*children, **props): + return rx.box( + *children, + border="1px solid var(--gray-10)", + border_radius="2px", + **props, + ) + + +def example2(): + return rx.box( + rx.flex( + border_box("flex_shrink=0", flex_shrink="0", width="100px"), + border_box("flex_shrink=1", flex_shrink="1", width="200px"), + border_box("flex_grow=0", flex_grow="0"), + border_box("flex_grow=1", flex_grow="1"), + width=f"{FlexGrowShrinkState.width_pct}%", + margin_bottom="16px", + spacing="2", + ), + rx.slider( + min=0, + max=100, + value=FlexGrowShrinkState.width_pct, + on_change=FlexGrowShrinkState.set_width_pct, + ), + width="100%", + ) +``` diff --git a/docs/library/layout/fragment.md b/docs/library/layout/fragment.md new file mode 100644 index 00000000000..731e9eb02a4 --- /dev/null +++ b/docs/library/layout/fragment.md @@ -0,0 +1,25 @@ +--- +components: + - rx.fragment +--- + +# Fragment + +```python exec +import reflex as rx +``` + +A Fragment is a Component that allow you to group multiple Components without a wrapper node. + +Refer to the React docs at [React/Fragment](https://react.dev/reference/react/Fragment) for more information on its use-case. + +```python demo +rx.fragment( + rx.text("Component1"), + rx.text("Component2") +) +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=3196&end=3340 +# Video: Fragment +``` diff --git a/docs/library/layout/grid.md b/docs/library/layout/grid.md new file mode 100644 index 00000000000..f09071a2d71 --- /dev/null +++ b/docs/library/layout/grid.md @@ -0,0 +1,40 @@ +--- +components: + - rx.grid +--- + +```python exec +import reflex as rx +``` + +# Grid + +Component for creating grid layouts. Either `rows` or `columns` may be specified. + +## Basic Example + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card(f"Card {i + 1}", height="10vh"), + ), + columns="3", + spacing="4", + width="100%", +) +``` + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card(f"Card {i + 1}", height="10vh"), + ), + rows="3", + flow="column", + justify="between", + spacing="4", + width="100%", +) +``` diff --git a/docs/library/layout/inset.md b/docs/library/layout/inset.md new file mode 100644 index 00000000000..f7f8951ada0 --- /dev/null +++ b/docs/library/layout/inset.md @@ -0,0 +1,89 @@ +--- +components: + - rx.inset + +Inset: | + lambda **props: rx.card( + rx.inset( + rx.image(src="https://web.reflex-assets.dev/other/reflex_banner.png", height="auto"), + **props, + ), + width="500px", + ) +--- + +```python exec +import reflex as rx +``` + +# Inset + +Applies a negative margin to allow content to bleed into the surrounding container. + +## Basic Example + +Nesting an Inset component inside a Card will render the content from edge to edge of the card. + +```python demo +rx.card( + rx.inset( + rx.image(src="https://web.reflex-assets.dev/other/reflex_banner.png", width="100%", height="auto"), + side="top", + pb="current", + ), + rx.text("Reflex is a web framework that allows developers to build their app in pure Python."), + width="25vw", +) +``` + +## Other Directions + +The `side` prop controls which side the negative margin is applied to. When using a specific side, +it is helpful to set the padding for the opposite side to `current` to retain the same padding the +content would have had if it went to the edge of the parent component. + +```python demo +rx.card( + rx.text("The inset below uses a bottom side."), + rx.inset( + rx.image(src="https://web.reflex-assets.dev/other/reflex_banner.png", width="100%", height="auto"), + side="bottom", + pt="current", + ), + width="25vw", +) +``` + +```python demo +rx.card( + rx.flex( + rx.text("This inset uses a right side, which requires a flex with direction row."), + rx.inset( + rx.box(background="center/cover url('https://web.reflex-assets.dev/other/reflex_banner.png')", height="100%"), + width="100%", + side="right", + pl="current", + ), + direction="row", + width="100%", + ), + width="25vw", +) +``` + +```python demo +rx.card( + rx.flex( + rx.inset( + rx.box(background="center/cover url('https://web.reflex-assets.dev/other/reflex_banner.png')", height="100%"), + width="100%", + side="left", + pr="current", + ), + rx.text("This inset uses a left side, which also requires a flex with direction row."), + direction="row", + width="100%", + ), + width="25vw", +) +``` diff --git a/docs/library/layout/section.md b/docs/library/layout/section.md new file mode 100644 index 00000000000..05f818bd0e1 --- /dev/null +++ b/docs/library/layout/section.md @@ -0,0 +1,36 @@ +--- +components: + - rx.section +--- + +```python exec +import reflex as rx +``` + +# Section + +Denotes a section of page content, providing vertical padding by default. + +Primarily this is a semantic component that is used to group related textual content. + +## Basic Example + +```python demo +rx.box( + rx.section( + rx.heading("First"), + rx.text("This is the first content section"), + padding_left="12px", + padding_right="12px", + background_color="var(--gray-2)", + ), + rx.section( + rx.heading("Second"), + rx.text("This is the second content section"), + padding_left="12px", + padding_right="12px", + background_color="var(--gray-2)", + ), + width="100%", +) +``` diff --git a/docs/library/layout/separator.md b/docs/library/layout/separator.md new file mode 100644 index 00000000000..e1d4e443399 --- /dev/null +++ b/docs/library/layout/separator.md @@ -0,0 +1,58 @@ +--- +components: + - rx.separator +Separator: | + lambda **props: rx.separator(**props) +--- + +```python exec +import reflex as rx +``` + +# Separator + +Visually or semantically separates content. + +## Basic Example + +```python demo +rx.flex( + rx.card("Section 1"), + rx.divider(), + rx.card("Section 2"), + spacing="4", + direction="column", + align="center", +) +``` + +## Size + +The `size` prop controls how long the separator is. Using `size="4"` will make +the separator fill the parent container. Setting CSS `width` or `height` prop to `"100%"` +can also achieve this effect, but `size` works the same regardless of the orientation. + +```python demo +rx.flex( + rx.card("Section 1"), + rx.divider(size="4"), + rx.card("Section 2"), + spacing="4", + direction="column", +) +``` + +## Orientation + +Setting the orientation prop to `vertical` will make the separator appear vertically. + +```python demo +rx.flex( + rx.card("Section 1"), + rx.divider(orientation="vertical", size="4"), + rx.card("Section 2"), + spacing="4", + width="100%", + height="10vh", +) +``` diff --git a/docs/library/layout/spacer.md b/docs/library/layout/spacer.md new file mode 100644 index 00000000000..0d9d1b01723 --- /dev/null +++ b/docs/library/layout/spacer.md @@ -0,0 +1,25 @@ +--- +components: + - rx.spacer +--- + +```python exec +import reflex as rx +``` + +# Spacer + +Creates an adjustable, empty space that can be used to tune the spacing between child elements within `flex`. + +```python demo +rx.flex( + rx.center(rx.text("Example"), bg="lightblue"), + rx.spacer(), + rx.center(rx.text("Example"), bg="lightgreen"), + rx.spacer(), + rx.center(rx.text("Example"), bg="salmon"), + width="100%", +) +``` + +As `stack`, `vstack` and `hstack` are all built from `flex`, it is possible to also use `spacer` inside of these components. diff --git a/docs/library/layout/stack.md b/docs/library/layout/stack.md new file mode 100644 index 00000000000..04fdde0b292 --- /dev/null +++ b/docs/library/layout/stack.md @@ -0,0 +1,172 @@ +--- +components: + - rx.stack + - rx.hstack + - rx.vstack +Stack: | + lambda **props: rx.stack( + rx.card("Card 1", size="2"), rx.card("Card 2", size="2"), rx.card("Card 3", size="2"), + width="100%", + height="20vh", + **props, + ) +--- + +```python exec +import reflex as rx +``` + +# Stack + +`Stack` is a layout component used to group elements together and apply a space between them. + +`vstack` is used to stack elements in the vertical direction. + +`hstack` is used to stack elements in the horizontal direction. + +`stack` is used to stack elements in the vertical or horizontal direction. + +These components are based on the `flex` component and therefore inherit all of its props. + +The `stack` component can be used with the `flex_direction` prop to set to either `row` or `column` to set the direction. + +```python demo +rx.flex( + rx.stack( + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="20%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="30%", + ), + rx.box( + "Example", + bg="lightgreen", + border_radius="3px", + width="50%", + ), + flex_direction="row", + width="100%", + ), + rx.stack( + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="20%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="30%", + ), + rx.box( + "Example", + bg="lightgreen", + border_radius="3px", + width="50%", + ), + flex_direction="column", + width="100%", + ), + width="100%", +) +``` + +## Hstack + +```python demo +rx.hstack( + rx.box( + "Example", bg="red", border_radius="3px", width="10%" + ), + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="10%", + ), + rx.box( + "Example", + bg="yellow", + border_radius="3px", + width="10%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="10%", + ), + rx.box( + "Example", + bg="lightgreen", + border_radius="3px", + width="60%", + ), + width="100%", +) +``` + +## Vstack + +```python demo +rx.vstack( + rx.box( + "Example", bg="red", border_radius="3px", width="20%" + ), + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="40%", + ), + rx.box( + "Example", + bg="yellow", + border_radius="3px", + width="60%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="80%", + ), + rx.box( + "Example", + bg="lightgreen", + border_radius="3px", + width="100%", + ), + width="100%", +) +``` + +## Real World Example + +```python demo +rx.hstack( + rx.box( + rx.heading("Saving Money"), + rx.text("Saving money is an art that combines discipline, strategic planning, and the wisdom to foresee future needs and emergencies. It begins with the simple act of setting aside a portion of one's income, creating a buffer that can grow over time through interest or investments.", margin_top="0.5em"), + padding="1em", + border_width="1px", + ), + rx.box( + rx.heading("Spending Money"), + rx.text("Spending money is a balancing act between fulfilling immediate desires and maintaining long-term financial health. It's about making choices, sometimes indulging in the pleasures of the moment, and at other times, prioritizing essential expenses.", margin_top="0.5em"), + padding="1em", + border_width="1px", + ), + gap="2em", +) + +``` diff --git a/docs/library/media/audio.md b/docs/library/media/audio.md new file mode 100644 index 00000000000..cd7b02f12f3 --- /dev/null +++ b/docs/library/media/audio.md @@ -0,0 +1,28 @@ +--- +components: + - rx.audio +--- + +# Audio + +```python exec +import reflex as rx +``` + +The audio component can display an audio given an src path as an argument. This could either be a local path from the assets folder or an external link. + +```python demo +rx.audio( + src="https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3", + width="400px", + height="32px", +) +``` + +If we had a local file in the `assets` folder named `test.mp3` we could set `src="/test.mp3"` to view the audio file. + +```md alert info +# How to let your user upload an audio file + +To let a user upload an audio file to your app check out the [upload docs](/docs/library/forms/upload). +``` diff --git a/docs/library/media/image.md b/docs/library/media/image.md new file mode 100644 index 00000000000..598f365e2db --- /dev/null +++ b/docs/library/media/image.md @@ -0,0 +1,63 @@ +--- +components: + - rx.image +--- + +```python exec +import reflex as rx +``` + +# Image + +The Image component can display an image given a `src` path as an argument. +This could either be a local path from the assets folder or an external link. + +```python demo +rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="100px", height="auto") +``` + +Image composes a box and can be styled similarly. + +```python demo +rx.image( + src="https://web.reflex-assets.dev/other/logo.jpg", + width="100px", + height="auto", + border_radius="15px 50px", + border="5px solid #555", +) +``` + +You can also pass a `PIL` image object as the `src`. + +```python demo box +rx.image(src="https://picsum.photos/id/1/200/300", alt="An Unsplash Image") +``` + +```python +from PIL import Image +import requests + + +class ImageState(rx.State): + url: str = "https://picsum.photos/id/1/200/300" + image: Image.Image = Image.open(requests.get(url, stream=True).raw) + + +def image_pil_example(): + return rx.vstack( + rx.image(src=ImageState.image) + ) +``` + +```md alert info +# rx.image only accepts URLs and Pillow Images + +A cv2 image must be converted to a PIL image to be passed directly to `rx.image` as a State variable, or saved to the `assets` folder and then passed to the `rx.image` component. +``` + +```md alert info +# How to let your user upload an image + +To let a user upload an image to your app check out the [upload docs](/docs/library/forms/upload). +``` diff --git a/docs/library/media/video.md b/docs/library/media/video.md new file mode 100644 index 00000000000..a98ad200a82 --- /dev/null +++ b/docs/library/media/video.md @@ -0,0 +1,28 @@ +--- +components: + - rx.video +--- + +# Video + +```python exec +import reflex as rx +``` + +The video component can display a video given an src path as an argument. This could either be a local path from the assets folder or an external link. + +```python demo +rx.video( + src="https://www.youtube.com/embed/9bZkp7q19f0", + width="400px", + height="auto" +) +``` + +If we had a local file in the `assets` folder named `test.mp4` we could set `url="/test.mp4"` to view the video. + +```md alert info +# How to let your user upload a video + +To let a user upload a video to your app check out the [upload docs](/docs/library/forms/upload). +``` diff --git a/docs/library/other/clipboard.md b/docs/library/other/clipboard.md new file mode 100644 index 00000000000..5fe2076154c --- /dev/null +++ b/docs/library/other/clipboard.md @@ -0,0 +1,78 @@ +--- +components: + - rx.clipboard +--- + +```python exec +import reflex as rx +``` + +# Clipboard + +_New in 0.5.6_ + +The Clipboard component can be used to respond to paste events with complex data. + +If the Clipboard component is included in a page without children, +`rx.clipboard()`, then it will attach to the document's `paste` event handler +and will be triggered when data is pasted anywhere into the page. + +```python demo exec +class ClipboardPasteState(rx.State): + @rx.event + def on_paste(self, data: list[tuple[str, str]]): + for mime_type, item in data: + yield rx.toast(f"Pasted {mime_type} data: {item}") + + +def clipboard_example(): + return rx.fragment( + rx.clipboard(on_paste=ClipboardPasteState.on_paste), + "Paste Content Here", + ) +``` + +The `data` argument passed to the `on_paste` method is a list of tuples, where +each tuple contains the MIME type of the pasted data and the data itself. Binary +data will be base64 encoded as a data URI, and can be decoded using python's +`urlopen` or used directly as the `src` prop of an image. + +## Scoped Paste Events + +If you want to limit the scope of the paste event to a specific element, wrap +the `rx.clipboard` component around the elements that should trigger the paste +event. + +To avoid having outer paste handlers also trigger the event, you can use the +event action `.stop_propagation` to prevent the paste from bubbling up through +the DOM. + +If you need to also prevent the default action of pasting the data into a text +box, you can also attach the `.prevent_default` action. + +```python demo exec +class ClipboardPasteImageState(rx.State): + last_image_uri: str = "" + + def on_paste(self, data: list[tuple[str, str]]): + for mime_type, item in data: + if mime_type.startswith("image/"): + self.last_image_uri = item + break + else: + return rx.toast("Did not find an image in the pasted data") + + +def clipboard_image_example(): + return rx.vstack( + rx.clipboard( + rx.input(placeholder="Paste Image (stop propagation)"), + on_paste=ClipboardPasteImageState.on_paste.stop_propagation + ), + rx.clipboard( + rx.input(placeholder="Paste Image (prevent default)"), + on_paste=ClipboardPasteImageState.on_paste.prevent_default + ), + rx.image(src=ClipboardPasteImageState.last_image_uri), + ) +``` diff --git a/docs/library/other/html.md b/docs/library/other/html.md new file mode 100644 index 00000000000..f74ec858c9d --- /dev/null +++ b/docs/library/other/html.md @@ -0,0 +1,119 @@ +--- +components: + - rx.el.a + - rx.el.abbr + - rx.el.address + - rx.el.area + - rx.el.article + - rx.el.aside + - rx.el.audio + - rx.el.b + - rx.el.bdi + - rx.el.bdo + - rx.el.blockquote + - rx.el.body + - rx.el.br + - rx.el.button + - rx.el.canvas + - rx.el.caption + - rx.el.cite + - rx.el.code + - rx.el.col + - rx.el.colgroup + - rx.el.data + - rx.el.dd + - rx.el.Del + - rx.el.details + - rx.el.dfn + - rx.el.dialog + - rx.el.div + - rx.el.dl + - rx.el.dt + - rx.el.em + - rx.el.embed + - rx.el.fieldset + - rx.el.figcaption + - rx.el.footer + - rx.el.form + - rx.el.h1 + - rx.el.h2 + - rx.el.h3 + - rx.el.h4 + - rx.el.h5 + - rx.el.h6 + - rx.el.head + - rx.el.header + - rx.el.hr + - rx.el.html + - rx.el.i + - rx.el.iframe + - rx.el.img + - rx.el.input + - rx.el.ins + - rx.el.kbd + - rx.el.label + - rx.el.legend + - rx.el.li + - rx.el.link + - rx.el.main + - rx.el.mark + - rx.el.math + - rx.el.meta + - rx.el.meter + - rx.el.nav + - rx.el.noscript + - rx.el.object + - rx.el.ol + - rx.el.optgroup + - rx.el.option + - rx.el.output + - rx.el.p + - rx.el.picture + - rx.el.portal + - rx.el.pre + - rx.el.progress + - rx.el.q + - rx.el.rp + - rx.el.rt + - rx.el.ruby + - rx.el.s + - rx.el.samp + - rx.el.script + - rx.el.section + - rx.el.select + - rx.el.small + - rx.el.source + - rx.el.span + - rx.el.strong + - rx.el.sub + - rx.el.sup + - rx.el.svg.circle + - rx.el.svg.defs + - rx.el.svg.linear_gradient + - rx.el.svg.polygon + - rx.el.svg.path + - rx.el.svg.rect + - rx.el.svg.stop + - rx.el.table + - rx.el.tbody + - rx.el.td + - rx.el.template + - rx.el.textarea + - rx.el.tfoot + - rx.el.th + - rx.el.thead + - rx.el.time + - rx.el.title + - rx.el.tr + - rx.el.track + - rx.el.u + - rx.el.ul + - rx.el.video + - rx.el.wbr +--- + +# HTML + +Reflex also provides a set of HTML elements that can be used to create web pages. These elements are the same as the HTML elements that are used in web development. These elements come unstyled bhy default. You can style them using style props or tailwindcss classes. + +The following is a list of the HTML elements that are available in Reflex: diff --git a/docs/library/other/html_embed.md b/docs/library/other/html_embed.md new file mode 100644 index 00000000000..2e54677fc31 --- /dev/null +++ b/docs/library/other/html_embed.md @@ -0,0 +1,38 @@ +--- +components: + - rx.html +--- + +```python exec +import reflex as rx +``` + +# HTML Embed + +The HTML component can be used to render raw HTML code. + +Before you reach for this component, consider using Reflex's raw HTML element support instead. + +```python demo +rx.vstack( + rx.html("

Hello World

"), + rx.html("

Hello World

"), + rx.html("

Hello World

"), + rx.html("

Hello World

"), + rx.html("
Hello World
"), + rx.html("
Hello World
"), +) +``` + +```md alert +# Missing Styles? + +Reflex uses Radix-UI and tailwind for styling, both of which reset default styles for headings. +If you are using the html component and want pretty default styles, consider setting `class_name='prose'`, adding `@tailwindcss/typography` package to `frontend_packages` and enabling it via `tailwind` config in `rxconfig.py`. See the [Tailwind docs](/docs/styling/overview) for an example of adding this plugin. +``` + +In this example, we render an image. + +```python demo +rx.html("") +``` diff --git a/docs/library/other/memo.md b/docs/library/other/memo.md new file mode 100644 index 00000000000..2e75238c3ca --- /dev/null +++ b/docs/library/other/memo.md @@ -0,0 +1,166 @@ +```python exec +import reflex as rx +``` + +# Memo + +The `memo` decorator is used to optimize component rendering by memoizing components that don't need to be re-rendered. This is particularly useful for expensive components that depend on specific props and don't need to be re-rendered when other state changes in your application. + +## Requirements + +When using `rx.memo`, you must follow these requirements: + +1. **Type all arguments**: All arguments to a memoized component must have type annotations. +2. **Use keyword arguments**: When calling a memoized component, you must use keyword arguments (not positional arguments). + +## Basic Usage + +When you wrap a component function with `@rx.memo`, the component will only re-render when its props change. This helps improve performance by preventing unnecessary re-renders. + +```python +# Define a state class to track count +class DemoState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + +# Define a memoized component +@rx.memo +def expensive_component(label: str) -> rx.Component: + return rx.vstack( + rx.heading(label), + rx.text("This component only re-renders when props change!"), + rx.divider(), + ) + +# Use the memoized component in your app +def index(): + return rx.vstack( + rx.heading("Memo Example"), + rx.text("Count: 0"), # This will update with state.count + rx.button("Increment", on_click=DemoState.increment), + rx.divider(), + expensive_component(label="Memoized Component"), # Must use keyword arguments + spacing="4", + padding="4", + border_radius="md", + border="1px solid #eaeaea", + ) +``` + +In this example, the `expensive_component` will only re-render when the `label` prop changes, not when the `count` state changes. + +## With Event Handlers + +You can also use `rx.memo` with components that have event handlers: + +```python +# Define a state class to track clicks +class ButtonState(rx.State): + clicks: int = 0 + + @rx.event + def increment(self): + self.clicks += 1 + +# Define a memoized button component +@rx.memo +def my_button(text: str, on_click: rx.EventHandler) -> rx.Component: + return rx.button(text, on_click=on_click) + +# Use the memoized button in your app +def index(): + return rx.vstack( + rx.text("Clicks: 0"), # This will update with state.clicks + my_button( + text="Click me", + on_click=ButtonState.increment + ), + spacing="4", + ) +``` + +## With State Variables + +When used with state variables, memoized components will only re-render when the specific state variables they depend on change: + +```python +# Define a state class with multiple variables +class AppState(rx.State): + name: str = "World" + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def set_name(self, name: str): + self.name = name + +# Define a memoized greeting component +@rx.memo +def greeting(name: str) -> rx.Component: + return rx.heading("Hello, " + name) # Will display the name prop + +# Use the memoized component with state variables +def index(): + return rx.vstack( + greeting(name=AppState.name), # Must use keyword arguments + rx.text("Count: 0"), # Will display the count + rx.button("Increment Count", on_click=AppState.increment), + rx.input( + placeholder="Enter your name", + on_change=AppState.set_name, + value="World", # Will be bound to AppState.name + ), + spacing="4", + ) +``` + +## Advanced Event Handler Example + +You can also pass arguments to event handlers in memoized components: + +```python +# Define a state class to track messages +class MessageState(rx.State): + message: str = "" + + @rx.event + def set_message(self, text: str): + self.message = text + +# Define a memoized component with event handlers that pass arguments +@rx.memo +def action_buttons(on_action: rx.EventHandler[rx.event.passthrough_event_spec(str)]) -> rx.Component: + return rx.hstack( + rx.button("Save", on_click=on_action("Saved!")), + rx.button("Delete", on_click=on_action("Deleted!")), + rx.button("Cancel", on_click=on_action("Cancelled!")), + spacing="2", + ) + +# Use the memoized component with event handlers +def index(): + return rx.vstack( + rx.text("Status: "), # Will display the message + action_buttons(on_action=MessageState.set_message), + spacing="4", + ) +``` + +## Performance Considerations + +Use `rx.memo` for: + +- Components with expensive rendering logic +- Components that render the same result given the same props +- Components that re-render too often due to parent component updates + +Avoid using `rx.memo` for: + +- Simple components where the memoization overhead might exceed the performance gain +- Components that almost always receive different props on re-render diff --git a/docs/library/other/script.md b/docs/library/other/script.md new file mode 100644 index 00000000000..8065ad34c67 --- /dev/null +++ b/docs/library/other/script.md @@ -0,0 +1,42 @@ +--- +components: + - rx.script +--- + +```python exec +import reflex as rx +``` + +# Script + +The Script component can be used to include inline javascript or javascript files by URL. + +It uses the [`next/script` component](https://nextjs.org/docs/app/api-reference/components/script) to inject the script and can be safely used with conditional rendering to allow script side effects to be controlled by the state. + +```python +rx.script("console.log('inline javascript')") +``` + +Complex inline scripting should be avoided. +If the code to be included is more than a couple lines, it is more maintainable to implement it in a separate javascript file in the `assets` directory and include it via the `src` prop. + +```python +rx.script(src="/my-custom.js") +``` + +This component is particularly helpful for including tracking and social scripts. +Any additional attrs needed for the script tag can be supplied via `custom_attrs` prop. + +```python +rx.script(src="//gc.zgo.at/count.js", custom_attrs=\{"data-goatcounter": "https://reflextoys.goatcounter.com/count"}) +``` + +This code renders to something like the following to enable stat counting with a third party service. + +```jsx + +``` diff --git a/docs/library/other/skeleton.md b/docs/library/other/skeleton.md new file mode 100644 index 00000000000..1f6c1927f0f --- /dev/null +++ b/docs/library/other/skeleton.md @@ -0,0 +1,27 @@ +--- +description: Skeleton, a loading placeholder component for content that is not yet available. +components: + - rx.skeleton +--- + +```python exec +import reflex as rx +``` + +# Skeleton (loading placeholder) + +`Skeleton` is a loading placeholder component that serves as a visual placeholder while content is loading. +It is useful for maintaining the layout's structure and providing users with a sense of progression while awaiting the final content. + +```python demo +rx.vstack( + rx.skeleton(rx.button("button-small"), height="10px"), + rx.skeleton(rx.button("button-big"), height="20px"), + rx.skeleton(rx.text("Text is loaded."), loading=True,), + rx.skeleton(rx.text("Text is already loaded."), loading=False,), +), +``` + +When using `Skeleton` with text, wrap the text itself instead of the parent element to have a placeholder of the same size. + +Use the loading prop to control whether the skeleton or its children are displayed. Skeleton preserves the dimensions of children when they are hidden and disables interactive elements. diff --git a/docs/library/other/theme.md b/docs/library/other/theme.md new file mode 100644 index 00000000000..3683b2cc061 --- /dev/null +++ b/docs/library/other/theme.md @@ -0,0 +1,31 @@ +--- +components: + - rx.theme + - rx.theme_panel +--- + +# Theme + +The `Theme` component is used to change the theme of the application. The `Theme` can be set directly in the rx.App. + +```python +app = rx.App( + theme=rx.theme( + appearance="light", has_background=True, radius="large", accent_color="teal" + ) +) +``` + +# Theme Panel + +The `ThemePanel` component is a container for the `Theme` component. It provides a way to change the theme of the application. + +```python +rx.theme_panel() +``` + +The theme panel is closed by default. You can set it open `default_open=True`. + +```python +rx.theme_panel(default_open=True) +``` diff --git a/docs/library/overlay/alert_dialog.md b/docs/library/overlay/alert_dialog.md new file mode 100644 index 00000000000..7b4abb8b6fd --- /dev/null +++ b/docs/library/overlay/alert_dialog.md @@ -0,0 +1,366 @@ +--- +components: + - rx.alert_dialog.root + - rx.alert_dialog.content + - rx.alert_dialog.trigger + - rx.alert_dialog.title + - rx.alert_dialog.description + - rx.alert_dialog.action + - rx.alert_dialog.cancel + +only_low_level: + - True + +AlertDialogRoot: | + lambda **props: rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel"), + ), + rx.alert_dialog.action( + rx.button("Revoke access"), + ), + spacing="3", + ), + ), + **props + ) + +AlertDialogContent: | + lambda **props: rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel"), + ), + rx.alert_dialog.action( + rx.button("Revoke access"), + ), + spacing="3", + ), + **props + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Alert Dialog + +An alert dialog is a modal confirmation dialog that interrupts the user and expects a response. + +The `alert_dialog.root` contains all the parts of the dialog. + +The `alert_dialog.trigger` wraps the control that will open the dialog. + +The `alert_dialog.content` contains the content of the dialog. + +The `alert_dialog.title` is the title that is announced when the dialog is opened. + +The `alert_dialog.description` is an optional description that is announced when the dialog is opened. + +The `alert_dialog.action` wraps the control that will close the dialog. This should be distinguished visually from the `alert_dialog.cancel` control. + +The `alert_dialog.cancel` wraps the control that will close the dialog. This should be distinguished visually from the `alert_dialog.action` control. + +## Basic Example + +```python demo +rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel"), + ), + rx.alert_dialog.action( + rx.button("Revoke access"), + ), + spacing="3", + ), + ), +) +``` + +This example has a different color scheme and the `cancel` and `action` buttons are right aligned. + +```python demo +rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access", color_scheme="red"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + size="2", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel", variant="soft", color_scheme="gray"), + ), + rx.alert_dialog.action( + rx.button("Revoke access", color_scheme="red", variant="solid"), + ), + spacing="3", + margin_top="16px", + justify="end", + ), + style={"max_width": 450}, + ), +) +``` + +Use the `inset` component to align content flush with the sides of the dialog. + +```python demo +rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Delete Users", color_scheme="red"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Delete Users"), + rx.alert_dialog.description( + "Are you sure you want to delete these users? This action is permanent and cannot be undone.", + size="2", + ), + rx.inset( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + ), + side="x", + margin_top="24px", + margin_bottom="24px", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel", variant="soft", color_scheme="gray"), + ), + rx.alert_dialog.action( + rx.button("Delete users", color_scheme="red"), + ), + spacing="3", + justify="end", + ), + style={"max_width": 500}, + ), +) +``` + +## Events when the Alert Dialog opens or closes + +The `on_open_change` event is called when the `open` state of the dialog changes. It is used in conjunction with the `open` prop. + +```python demo exec +class AlertDialogState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def alert_dialog(): + return rx.flex( + rx.heading(f"Number of times alert dialog opened or closed: {AlertDialogState.num_opens}"), + rx.heading(f"Alert Dialog open: {AlertDialogState.opened}"), + rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access", color_scheme="red"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + size="2", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel", variant="soft", color_scheme="gray"), + ), + rx.alert_dialog.action( + rx.button("Revoke access", color_scheme="red", variant="solid"), + ), + spacing="3", + margin_top="16px", + justify="end", + ), + style={"max_width": 450}, + ), + on_open_change=AlertDialogState.count_opens, + ), + direction="column", + spacing="3", + ) +``` + +## Controlling Alert Dialog with State + +This example shows how to control whether the dialog is open or not with state. This is an easy way to show the dialog without needing to use the `rx.alert_dialog.trigger`. + +`rx.alert_dialog.root` has a prop `open` that can be set to a boolean value to control whether the dialog is open or not. + +We toggle this `open` prop with a button outside of the dialog and the `rx.alert_dialog.cancel` and `rx.alert_dialog.action` buttons inside the dialog. + +```python demo exec +class AlertDialogState2(rx.State): + opened: bool = False + + @rx.event + def dialog_open(self): + self.opened = ~self.opened + + +def alert_dialog2(): + return rx.box( + rx.alert_dialog.root( + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel", on_click=AlertDialogState2.dialog_open), + ), + rx.alert_dialog.action( + rx.button("Revoke access", on_click=AlertDialogState2.dialog_open), + ), + spacing="3", + ), + ), + open=AlertDialogState2.opened, + ), + rx.button("Button to Open the Dialog", on_click=AlertDialogState2.dialog_open), +) +``` + +## Form Submission to a Database from an Alert Dialog + +This example adds new users to a database from an alert dialog using a form. + +1. It defines a User1 model with name and email fields. +2. The `add_user_to_db` method adds a new user to the database, checking for existing emails. +3. On form submission, it calls the `add_user_to_db` method. +4. The UI component has: + +- A button to open an alert dialog +- An alert dialog containing a form to add a new user +- Input fields for name and email +- Submit and Cancel buttons + +```python demo exec +class User1(rx.Model, table=True): + """The user model.""" + name: str + email: str + +class State(rx.State): + + current_user: User1 = User1() + + @rx.event + def add_user_to_db(self, form_data: dict): + self.current_user = form_data + ### Uncomment the code below to add your data to a database ### + # with rx.session() as session: + # if session.exec( + # select(User1).where(user.email == self.current_user["email"]) + # ).first(): + # return rx.window_alert("User with this email already exists") + # session.add(User1(**self.current_user)) + # session.commit() + + return rx.toast.info(f"User {self.current_user['name']} has been added.", position="bottom-right") + + +def index() -> rx.Component: + return rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.alert_dialog.content( + rx.alert_dialog.title( + "Add New User", + ), + rx.alert_dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name" + ), + rx.input( + placeholder="user@reflex.dev", name="email" + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.alert_dialog.action( + rx.button("Submit", type="submit"), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user_to_db, + reset_on_submit=False, + ), + max_width="450px", + ), + ) +``` diff --git a/docs/library/overlay/context_menu.md b/docs/library/overlay/context_menu.md new file mode 100644 index 00000000000..b284a82986c --- /dev/null +++ b/docs/library/overlay/context_menu.md @@ -0,0 +1,343 @@ +--- +components: + - rx.context_menu.root + - rx.context_menu.item + - rx.context_menu.separator + - rx.context_menu.trigger + - rx.context_menu.content + - rx.context_menu.sub + - rx.context_menu.sub_trigger + - rx.context_menu.sub_content + +only_low_level: + - True + +ContextMenuRoot: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + ), + ), + **props + ) + +ContextMenuTrigger: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)"), + **props + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + ), + ), + ) + +ContextMenuContent: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + ), + **props + ), + ) + +ContextMenuSub: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + **props + ), + ), + ) + +ContextMenuSubTrigger: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More", **props), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + ), + ), + ) + +ContextMenuSubContent: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + **props + ), + ), + ), + ) + +ContextMenuItem: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C", **props), + rx.context_menu.item("Share", **props), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red", **props), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate", **props), + rx.context_menu.item("Duplicate", **props), + rx.context_menu.item("Archive", **props), + ), + ), + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Context Menu + +A Context Menu is a popup menu that appears upon user interaction, such as a right-click or a hover. + +## Basic Usage + +A Context Menu is composed of a `context_menu.root`, a `context_menu.trigger` and a `context_menu.content`. The `context_menu_root` contains all the parts of a context menu. The `context_menu.trigger` is the element that the user interacts with to open the menu. It wraps the element that will open the context menu. The `context_menu.content` is the component that pops out when the context menu is open. + +The `context_menu.item` contains the actual context menu items and sits under the `context_menu.content`. + +The `context_menu.sub` contains all the parts of a submenu. There is a `context_menu.sub_trigger`, which is an item that opens a submenu. It must be rendered inside a `context_menu.sub` component. The `context_menu.sub_content` is the component that pops out when a submenu is open. It must also be rendered inside a `context_menu.sub` component. + +The `context_menu.separator` is used to visually separate items in a context menu. + +```python demo +rx.context_menu.root( + rx.context_menu.trigger( + rx.button("Right click me"), + ), + rx.context_menu.content( + rx.context_menu.item("Edit", shortcut="⌘ E"), + rx.context_menu.item("Duplicate", shortcut="⌘ D"), + rx.context_menu.separator(), + rx.context_menu.item("Archive", shortcut="⌘ N"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Move to project…"), + rx.context_menu.item("Move to folder…"), + rx.context_menu.separator(), + rx.context_menu.item("Advanced options…"), + ), + ), + rx.context_menu.separator(), + rx.context_menu.item("Share"), + rx.context_menu.item("Add to favorites"), + rx.context_menu.separator(), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + ), +) +``` + +````md alert warning +# `rx.context_menu.item` must be a DIRECT child of `rx.context_menu.content` + +The code below for example is not allowed: + +```python +rx.context_menu.root( + rx.context_menu.trigger( + rx.button("Right click me"), + ), + rx.context_menu.content( + rx.cond( + State.count % 2 == 0, + rx.vstack( + rx.context_menu.item("Even Option 1", on_click=State.set_selected_option("Even Option 1")), + rx.context_menu.item("Even Option 2", on_click=State.set_selected_option("Even Option 2")), + rx.context_menu.item("Even Option 3", on_click=State.set_selected_option("Even Option 3")), + ), + rx.vstack( + rx.context_menu.item("Odd Option A", on_click=State.set_selected_option("Odd Option A")), + rx.context_menu.item("Odd Option B", on_click=State.set_selected_option("Odd Option B")), + rx.context_menu.item("Odd Option C", on_click=State.set_selected_option("Odd Option C")), + ) + ) + ), +) +``` +```` + +## Opening a Dialog from Context Menu using State + +Accessing an overlay component from within another overlay component is a common use case but does not always work exactly as expected. + +The code below will not work as expected as because the dialog is within the menu and the dialog will only be open when the menu is open, rendering the dialog unusable. + +```python +rx.context_menu.root( + rx.context_menu.trigger(rx.icon("ellipsis-vertical")), + rx.context_menu.content( + rx.context_menu.item( + rx.dialog.root( + rx.dialog.trigger(rx.text("Edit")), + rx.dialog.content(....), + ..... + ), + ), + ), +) +``` + +In this example, we will show how to open a dialog box from a context menu, where the menu will close and the dialog will open and be functional. + +```python demo exec +class ContextMenuState(rx.State): + which_dialog_open: str = "" + + @rx.event + def set_which_dialog_open(self, value: str): + self.which_dialog_open = value + + @rx.event + def delete(self): + yield rx.toast("Deleted item") + + @rx.event + def save_settings(self): + yield rx.toast("Saved settings") + + +def delete_dialog(): + return rx.alert_dialog.root( + rx.alert_dialog.content( + rx.alert_dialog.title("Are you Sure?"), + rx.alert_dialog.description( + rx.text( + "This action cannot be undone. Are you sure you want to delete this item?", + ), + margin_bottom="20px", + ), + rx.hstack( + rx.alert_dialog.action( + rx.button( + "Delete", + color_scheme="red", + on_click=ContextMenuState.delete, + ), + ), + rx.spacer(), + rx.alert_dialog.cancel(rx.button("Cancel")), + ), + ), + open=ContextMenuState.which_dialog_open == "delete", + on_open_change=ContextMenuState.set_which_dialog_open(""), + ) + + +def settings_dialog(): + return rx.dialog.root( + rx.dialog.content( + rx.dialog.title("Settings"), + rx.dialog.description( + rx.text("Set your settings in this settings dialog."), + margin_bottom="20px", + ), + rx.dialog.close( + rx.button("Close", on_click=ContextMenuState.save_settings), + ), + ), + open=ContextMenuState.which_dialog_open == "settings", + on_open_change=ContextMenuState.set_which_dialog_open(""), + ) + + +def context_menu_call_dialog() -> rx.Component: + return rx.vstack( + rx.context_menu.root( + rx.context_menu.trigger(rx.icon("ellipsis-vertical")), + rx.context_menu.content( + rx.context_menu.item( + "Delete", + on_click=ContextMenuState.set_which_dialog_open("delete"), + ), + rx.context_menu.item( + "Settings", + on_click=ContextMenuState.set_which_dialog_open("settings"), + ), + ), + ), + rx.cond( + ContextMenuState.which_dialog_open, + rx.heading(f"{ContextMenuState.which_dialog_open} dialog is open"), + ), + delete_dialog(), + settings_dialog(), + align="center", + ) +``` diff --git a/docs/library/overlay/dialog.md b/docs/library/overlay/dialog.md new file mode 100644 index 00000000000..a2df0150ba4 --- /dev/null +++ b/docs/library/overlay/dialog.md @@ -0,0 +1,282 @@ +--- +components: + - rx.dialog.root + - rx.dialog.trigger + - rx.dialog.title + - rx.dialog.content + - rx.dialog.description + - rx.dialog.close + +only_low_level: + - True + +DialogRoot: | + lambda **props: rx.dialog.root( + rx.dialog.trigger(rx.button("Open Dialog")), + rx.dialog.content( + rx.dialog.title("Welcome to Reflex!"), + rx.dialog.description( + "This is a dialog component. You can render anything you want in here.", + ), + rx.dialog.close( + rx.button("Close Dialog"), + ), + ), + **props, + ) + +DialogContent: | + lambda **props: rx.dialog.root( + rx.dialog.trigger(rx.button("Open Dialog")), + rx.dialog.content( + rx.dialog.title("Welcome to Reflex!"), + rx.dialog.description( + "This is a dialog component. You can render anything you want in here.", + ), + rx.dialog.close( + rx.button("Close Dialog"), + ), + **props, + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Dialog + +The `dialog.root` contains all the parts of a dialog. + +The `dialog.trigger` wraps the control that will open the dialog. + +The `dialog.content` contains the content of the dialog. + +The `dialog.title` is a title that is announced when the dialog is opened. + +The `dialog.description` is a description that is announced when the dialog is opened. + +The `dialog.close` wraps the control that will close the dialog. + +```python demo +rx.dialog.root( + rx.dialog.trigger(rx.button("Open Dialog")), + rx.dialog.content( + rx.dialog.title("Welcome to Reflex!"), + rx.dialog.description( + "This is a dialog component. You can render anything you want in here.", + ), + rx.dialog.close( + rx.button("Close Dialog", size="3"), + ), + ), +) +``` + +## In context examples + +```python demo +rx.dialog.root( + rx.dialog.trigger( + rx.button("Edit Profile", size="4") + ), + rx.dialog.content( + rx.dialog.title("Edit Profile"), + rx.dialog.description( + "Change your profile details and preferences.", + size="2", + margin_bottom="16px", + ), + rx.flex( + rx.text("Name", as_="div", size="2", margin_bottom="4px", weight="bold"), + rx.input(default_value="Freja Johnson", placeholder="Enter your name"), + rx.text("Email", as_="div", size="2", margin_bottom="4px", weight="bold"), + rx.input(default_value="freja@example.com", placeholder="Enter your email"), + direction="column", + spacing="3", + ), + rx.flex( + rx.dialog.close( + rx.button("Cancel", color_scheme="gray", variant="soft"), + ), + rx.dialog.close( + rx.button("Save"), + ), + spacing="3", + margin_top="16px", + justify="end", + ), + ), +) +``` + +```python demo +rx.dialog.root( + rx.dialog.trigger(rx.button("View users", size="4")), + rx.dialog.content( + rx.dialog.title("Users"), + rx.dialog.description("The following users have access to this project."), + + rx.inset( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + ), + side="x", + margin_top="24px", + margin_bottom="24px", + ), + rx.flex( + rx.dialog.close( + rx.button("Close", variant="soft", color_scheme="gray"), + ), + spacing="3", + justify="end", + ), + ), +) +``` + +## Events when the Dialog opens or closes + +The `on_open_change` event is called when the `open` state of the dialog changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class DialogState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def dialog_example(): + return rx.flex( + rx.heading(f"Number of times dialog opened or closed: {DialogState.num_opens}"), + rx.heading(f"Dialog open: {DialogState.opened}"), + rx.dialog.root( + rx.dialog.trigger(rx.button("Open Dialog")), + rx.dialog.content( + rx.dialog.title("Welcome to Reflex!"), + rx.dialog.description( + "This is a dialog component. You can render anything you want in here.", + ), + rx.dialog.close( + rx.button("Close Dialog", size="3"), + ), + ), + on_open_change=DialogState.count_opens, + ), + direction="column", + spacing="3", + ) +``` + +Check out the [menu docs](/docs/library/overlay/dropdown_menu) for an example of opening a dialog from within a dropdown menu. + +## Form Submission to a Database from a Dialog + +This example adds new users to a database from a dialog using a form. + +1. It defines a User model with name and email fields. +2. The `add_user_to_db` method adds a new user to the database, checking for existing emails. +3. On form submission, it calls the `add_user_to_db` method. +4. The UI component has: + +- A button to open a dialog +- A dialog containing a form to add a new user +- Input fields for name and email +- Submit and Cancel buttons + +```python demo exec +class User(rx.Model, table=True): + """The user model.""" + name: str + email: str + +class State(rx.State): + + current_user: User = User() + + @rx.event + def add_user_to_db(self, form_data: dict): + self.current_user = form_data + ### Uncomment the code below to add your data to a database ### + # with rx.session() as session: + # if session.exec( + # select(User).where(user.email == self.current_user["email"]) + # ).first(): + # return rx.window_alert("User with this email already exists") + # session.add(User(**self.current_user)) + # session.commit() + + return rx.toast.info(f"User {self.current_user['name']} has been added.", position="bottom-right") + + +def index() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name" + ), + rx.input( + placeholder="user@reflex.dev", name="email" + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button("Submit", type="submit"), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user_to_db, + reset_on_submit=False, + ), + max_width="450px", + ), + ) +``` diff --git a/docs/library/overlay/drawer.md b/docs/library/overlay/drawer.md new file mode 100644 index 00000000000..a8beb954514 --- /dev/null +++ b/docs/library/overlay/drawer.md @@ -0,0 +1,119 @@ +--- +components: + - rx.drawer.root + - rx.drawer.trigger + - rx.drawer.overlay + - rx.drawer.portal + - rx.drawer.content + - rx.drawer.close + +only_low_level: + - True + +DrawerRoot: | + lambda **props: rx.drawer.root( + rx.drawer.trigger(rx.button("Open Drawer")), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.flex( + rx.drawer.close(rx.button("Close")), + ), + height="100%", + width="20em", + background_color="#FFF" + ), + ), + **props, + ) +--- + +```python exec +import reflex as rx +``` + +# Drawer + +```python demo +rx.drawer.root( + rx.drawer.trigger( + rx.button("Open Drawer") + ), + rx.drawer.overlay( + z_index="5" + ), + rx.drawer.portal( + rx.drawer.content( + rx.flex( + rx.drawer.close(rx.box(rx.button("Close"))), + align_items="start", + direction="column", + ), + top="auto", + right="auto", + height="100%", + width="20em", + padding="2em", + background_color="#FFF" + #background_color=rx.color("green", 3) + ) + ), + direction="left", +) +``` + +## Sidebar Menu with a Drawer and State + +This example shows how to create a sidebar menu with a drawer. The drawer is opened by clicking a button. The drawer contains links to different sections of the page. When a link is clicked the drawer closes and the page scrolls to the section. + +The `rx.drawer.root` component has an `open` prop that is set by the state variable `is_open`. Setting the `modal` prop to `False` allows the user to interact with the rest of the page while the drawer is open and allows the page to be scrolled when a user clicks one of the links. + +```python demo exec +class DrawerState(rx.State): + is_open: bool = False + + @rx.event + def toggle_drawer(self): + self.is_open = not self.is_open + +def drawer_content(): + return rx.drawer.content( + rx.flex( + rx.drawer.close(rx.button("Close", on_click=DrawerState.toggle_drawer)), + rx.link("Link 1", href="#test1", on_click=DrawerState.toggle_drawer), + rx.link("Link 2", href="#test2", on_click=DrawerState.toggle_drawer), + align_items="start", + direction="column", + ), + height="100%", + width="20%", + padding="2em", + background_color=rx.color("grass", 7), + ) + + +def lateral_menu(): + return rx.drawer.root( + rx.drawer.trigger(rx.button("Open Drawer", on_click=DrawerState.toggle_drawer)), + rx.drawer.overlay(), + rx.drawer.portal(drawer_content()), + open=DrawerState.is_open, + direction="left", + modal=False, + ) + +def drawer_sidebar(): + return rx.vstack( + lateral_menu(), + rx.section( + rx.heading("Test1", size="8"), + id='test1', + height="400px", + ), + rx.section( + rx.heading("Test2", size="8"), + id='test2', + height="400px", + ) + ) +``` diff --git a/docs/library/overlay/dropdown_menu.md b/docs/library/overlay/dropdown_menu.md new file mode 100644 index 00000000000..1db023e87cf --- /dev/null +++ b/docs/library/overlay/dropdown_menu.md @@ -0,0 +1,315 @@ +--- +components: + - rx.dropdown_menu.root + - rx.dropdown_menu.content + - rx.dropdown_menu.trigger + - rx.dropdown_menu.item + - rx.dropdown_menu.separator + - rx.dropdown_menu.sub_content + +only_low_level: + - True + +DropdownMenuRoot: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + ), + ), + ), + **props + ) + +DropdownMenuContent: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + ), + ), + **props, + ), + ) + +DropdownMenuItem: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E", **props), + rx.menu.item("Share", **props), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red", **props), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate", **props), + rx.menu.item("Duplicate", **props), + rx.menu.item("Archive", **props), + ), + ), + ), + ) + +DropdownMenuSub: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + ), + **props, + ), + ), + ) + +DropdownMenuSubTrigger: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More", **props), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + ), + ), + ), + ) + +DropdownMenuSubContent: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + **props, + ), + ), + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Dropdown Menu + +A Dropdown Menu is a menu that offers a list of options that a user can select from. They are typically positioned near a button that will control their appearance and disappearance. + +A Dropdown Menu is composed of a `menu.root`, a `menu.trigger` and a `menu.content`. The `menu.trigger` is the element that the user interacts with to open the menu. It wraps the element that will open the dropdown menu. The `menu.content` is the component that pops out when the dropdown menu is open. + +The `menu.item` contains the actual dropdown menu items and sits under the `menu.content`. The `shortcut` prop is an optional shortcut command displayed next to the item text. + +The `menu.sub` contains all the parts of a submenu. There is a `menu.sub_trigger`, which is an item that opens a submenu. It must be rendered inside a `menu.sub` component. The `menu.sub_component` is the component that pops out when a submenu is open. It must also be rendered inside a `menu.sub` component. + +The `menu.separator` is used to visually separate items in a dropdown menu. + +```python demo +rx.menu.root( + rx.menu.trigger( + rx.button("Options", variant="soft"), + ), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Duplicate", shortcut="⌘ D"), + rx.menu.separator(), + rx.menu.item("Archive", shortcut="⌘ N"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Move to project…"), + rx.menu.item("Move to folder…"), + rx.menu.separator(), + rx.menu.item("Advanced options…"), + ), + ), + rx.menu.separator(), + rx.menu.item("Share"), + rx.menu.item("Add to favorites"), + rx.menu.separator(), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + ), +) +``` + +## Events when the Dropdown Menu opens or closes + +The `on_open_change` event, from the `menu.root`, is called when the `open` state of the dropdown menu changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class DropdownMenuState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def dropdown_menu_example(): + return rx.flex( + rx.heading(f"Number of times Dropdown Menu opened or closed: {DropdownMenuState.num_opens}"), + rx.heading(f"Dropdown Menu open: {DropdownMenuState.opened}"), + rx.menu.root( + rx.menu.trigger( + rx.button("Options", variant="soft", size="2"), + ), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Duplicate", shortcut="⌘ D"), + rx.menu.separator(), + rx.menu.item("Archive", shortcut="⌘ N"), + rx.menu.separator(), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + ), + on_open_change=DropdownMenuState.count_opens, + ), + direction="column", + spacing="3", + ) +``` + +## Opening a Dialog from Menu using State + +Accessing an overlay component from within another overlay component is a common use case but does not always work exactly as expected. + +The code below will not work as expected as because the dialog is within the menu and the dialog will only be open when the menu is open, rendering the dialog unusable. + +```python +rx.menu.root( + rx.menu.trigger(rx.icon("ellipsis-vertical")), + rx.menu.content( + rx.menu.item( + rx.dialog.root( + rx.dialog.trigger(rx.text("Edit")), + rx.dialog.content(....), + ..... + ), + ), + ), +) +``` + +In this example, we will show how to open a dialog box from a dropdown menu, where the menu will close and the dialog will open and be functional. + +```python demo exec +class DropdownMenuState2(rx.State): + which_dialog_open: str = "" + + @rx.event + def set_which_dialog_open(self, value: str): + self.which_dialog_open = value + + @rx.event + def delete(self): + yield rx.toast("Deleted item") + + @rx.event + def save_settings(self): + yield rx.toast("Saved settings") + + +def delete_dialog(): + return rx.alert_dialog.root( + rx.alert_dialog.content( + rx.alert_dialog.title("Are you Sure?"), + rx.alert_dialog.description( + rx.text( + "This action cannot be undone. Are you sure you want to delete this item?", + ), + margin_bottom="20px", + ), + rx.hstack( + rx.alert_dialog.action( + rx.button( + "Delete", + color_scheme="red", + on_click=DropdownMenuState2.delete, + ), + ), + rx.spacer(), + rx.alert_dialog.cancel(rx.button("Cancel")), + ), + ), + open=DropdownMenuState2.which_dialog_open == "delete", + on_open_change=DropdownMenuState2.set_which_dialog_open(""), + ) + + +def settings_dialog(): + return rx.dialog.root( + rx.dialog.content( + rx.dialog.title("Settings"), + rx.dialog.description( + rx.text("Set your settings in this settings dialog."), + margin_bottom="20px", + ), + rx.dialog.close( + rx.button("Close", on_click=DropdownMenuState2.save_settings), + ), + ), + open=DropdownMenuState2.which_dialog_open == "settings", + on_open_change=DropdownMenuState2.set_which_dialog_open(""), + ) + + +def menu_call_dialog() -> rx.Component: + return rx.vstack( + rx.menu.root( + rx.menu.trigger(rx.icon("menu")), + rx.menu.content( + rx.menu.item( + "Delete", + on_click=DropdownMenuState2.set_which_dialog_open("delete"), + ), + rx.menu.item( + "Settings", + on_click=DropdownMenuState2.set_which_dialog_open("settings"), + ), + ), + ), + rx.cond( + DropdownMenuState2.which_dialog_open, + rx.heading(f"{DropdownMenuState2.which_dialog_open} dialog is open"), + ), + delete_dialog(), + settings_dialog(), + align="center", + ) +``` diff --git a/docs/library/overlay/hover_card.md b/docs/library/overlay/hover_card.md new file mode 100644 index 00000000000..6a23341ca74 --- /dev/null +++ b/docs/library/overlay/hover_card.md @@ -0,0 +1,126 @@ +--- +components: + - rx.hover_card.root + - rx.hover_card.content + - rx.hover_card.trigger + +only_low_level: + - True + +HoverCardRoot: | + lambda **props: rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me"), + ), + rx.hover_card.content( + rx.text("This is the tooltip content."), + ), + **props + ) + +HoverCardContent: | + lambda **props: rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me"), + ), + rx.hover_card.content( + rx.text("This is the tooltip content."), + **props + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Hovercard + +The `hover_card.root` contains all the parts of a hover card. + +The `hover_card.trigger` wraps the link that will open the hover card. + +The `hover_card.content` contains the content of the open hover card. + +```python demo +rx.text( + "Hover over the text to see the tooltip. ", + rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me", color_scheme="blue", underline="always"), + ), + rx.hover_card.content( + rx.text("This is the hovercard content."), + ), + ), +) +``` + +```python demo +rx.text( + "Hover over the text to see the tooltip. ", + rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me", color_scheme="blue", underline="always"), + ), + rx.hover_card.content( + rx.grid( + rx.inset( + side="left", + pr="current", + background="url('https://images.unsplash.com/5/unsplash-kitsune-4.jpg') center/cover", + height="full", + ), + rx.box( + rx.text_area(placeholder="Write a comment…", style={"height": 80}), + rx.flex( + rx.checkbox("Send to group"), + spacing="3", + margin_top="12px", + justify="between", + ), + padding_left="12px", + ), + columns="120px 1fr", + ), + style={"width": 360}, + ), + ), +) +``` + +## Events when the Hovercard opens or closes + +The `on_open_change` event is called when the `open` state of the hovercard changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class HovercardState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def hovercard_example(): + return rx.flex( + rx.heading(f"Number of times hovercard opened or closed: {HovercardState.num_opens}"), + rx.heading(f"Hovercard open: {HovercardState.opened}"), + rx.text( + "Hover over the text to see the hover card. ", + rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me", color_scheme="blue", underline="always"), + ), + rx.hover_card.content( + rx.text("This is the tooltip content."), + ), + on_open_change=HovercardState.count_opens, + ), + ), + direction="column", + spacing="3", + ) +``` diff --git a/docs/library/overlay/popover.md b/docs/library/overlay/popover.md new file mode 100644 index 00000000000..2d6b81f4728 --- /dev/null +++ b/docs/library/overlay/popover.md @@ -0,0 +1,224 @@ +--- +components: + - rx.popover.root + - rx.popover.content + - rx.popover.trigger + - rx.popover.close + +only_low_level: + - True + +PopoverRoot: | + lambda **props: rx.popover.root( + rx.popover.trigger( + rx.button("Popover"), + ), + rx.popover.content( + rx.flex( + rx.text("Simple Example"), + rx.popover.close( + rx.button("Close"), + ), + direction="column", + spacing="3", + ), + ), + **props + ) + +PopoverContent: | + lambda **props: rx.popover.root( + rx.popover.trigger( + rx.button("Popover"), + ), + rx.popover.content( + rx.flex( + rx.text("Simple Example"), + rx.popover.close( + rx.button("Close"), + ), + direction="column", + spacing="3", + ), + **props + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Popover + +A popover displays content, triggered by a button. + +The `popover.root` contains all the parts of a popover. + +The `popover.trigger` contains the button that toggles the popover. + +The `popover.content` is the component that pops out when the popover is open. + +The `popover.close` is the button that closes an open popover. + +## Basic Example + +```python demo +rx.popover.root( + rx.popover.trigger( + rx.button("Popover"), + ), + rx.popover.content( + rx.flex( + rx.text("Simple Example"), + rx.popover.close( + rx.button("Close"), + ), + direction="column", + spacing="3", + ), + ), +) +``` + +## Examples in Context + +```python demo + +rx.popover.root( + rx.popover.trigger( + rx.button("Comment", variant="soft"), + ), + rx.popover.content( + rx.flex( + rx.avatar( + "2", + fallback="RX", + radius="full" + ), + rx.box( + rx.text_area(placeholder="Write a comment…", style={"height": 80}), + rx.flex( + rx.checkbox("Send to group"), + rx.popover.close( + rx.button("Comment", size="1") + ), + spacing="3", + margin_top="12px", + justify="between", + ), + flex_grow="1", + ), + spacing="3" + ), + style={"width": 360}, + ) +) +``` + +```python demo +rx.popover.root( + rx.popover.trigger( + rx.button("Feedback", variant="classic"), + ), + rx.popover.content( + rx.inset( + side="top", + background="url('https://images.unsplash.com/5/unsplash-kitsune-4.jpg') center/cover", + height="100px", + ), + rx.box( + rx.text_area(placeholder="Write a comment…", style={"height": 80}), + rx.flex( + rx.checkbox("Send to group"), + rx.popover.close( + rx.button("Comment", size="1") + ), + spacing="3", + margin_top="12px", + justify="between", + ), + padding_top="12px", + ), + style={"width": 360}, + ) +) +``` + +## Popover with dynamic title + +Code like below will not work as expected and it is necessary to place the dynamic title (`Index2State.language`) inside of an `rx.text` component. + +```python +class Index2State(rx.State): + language: str = "EN" + +def index() -> rx.Component: + return rx.popover.root( + rx.popover.trigger( + rx.button(Index2State.language), + ), + rx.popover.content( + rx.text('Success') + ) + ) +``` + +This code will work: + +```python demo exec +class Index2State(rx.State): + language: str = "EN" + +def index() -> rx.Component: + return rx.popover.root( + rx.popover.trigger( + rx.button( + rx.text(Index2State.language) + ), + ), + rx.popover.content( + rx.text('Success') + ) + ) +``` + +## Events when the Popover opens or closes + +The `on_open_change` event is called when the `open` state of the popover changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class PopoverState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def popover_example(): + return rx.flex( + rx.heading(f"Number of times popover opened or closed: {PopoverState.num_opens}"), + rx.heading(f"Popover open: {PopoverState.opened}"), + rx.popover.root( + rx.popover.trigger( + rx.button("Popover"), + ), + rx.popover.content( + rx.flex( + rx.text("Simple Example"), + rx.popover.close( + rx.button("Close"), + ), + direction="column", + spacing="3", + ), + ), + on_open_change=PopoverState.count_opens, + ), + direction="column", + spacing="3", + ) +``` diff --git a/docs/library/overlay/toast.md b/docs/library/overlay/toast.md new file mode 100644 index 00000000000..7c5c666060c --- /dev/null +++ b/docs/library/overlay/toast.md @@ -0,0 +1,105 @@ +--- +components: + - rx.toast.provider +--- + +```python exec +import reflex as rx +``` + +# Toast + +A `rx.toast` is a non-blocking notification that disappears after a certain amount of time. It is often used to show a message to the user without interrupting their workflow. + +## Usage + +You can use `rx.toast` as an event handler for any component that triggers an action. + +```python demo +rx.button("Show Toast", on_click=rx.toast("Hello, World!")) +``` + +### Usage in State + +You can also use `rx.toast` in a state to show a toast when a specific action is triggered, using `yield`. + +```python demo exec +import asyncio +class ToastState(rx.State): + + @rx.event + async def fetch_data(self): + # Simulate fetching data for a 2-second delay + await asyncio.sleep(2) + # Shows a toast when the data is fetched + yield rx.toast("Data fetched!") + + +def render(): + return rx.button("Get Data", on_click=ToastState.fetch_data) +``` + +## Interaction + +If you want to interact with a toast, a few props are available to customize the behavior. + +By passing a `ToastAction` to the `action` or `cancel` prop, you can trigger an action when the toast is clicked or when it is closed. + +```python demo +rx.button("Show Toast", on_click=rx.toast("Hello, World!", duration=5000, close_button=True)) +``` + +### Presets + +`rx.toast` has some presets that you can use to show different types of toasts. + +```python demo +rx.hstack( + rx.button("Success", on_click=rx.toast.success("Success!"), color_scheme="green"), + rx.button("Error", on_click=rx.toast.error("Error!"), color_scheme="red"), + rx.button("Warning", on_click=rx.toast.warning("Warning!"), color_scheme="orange"), + rx.button("Info", on_click=rx.toast.info("Info!"), color_scheme="blue"), +) +``` + +### Customization + +If the presets don't fit your needs, you can customize the toasts by passing to `rx.toast` or to `rx.toast.options` some kwargs. + +```python demo +rx.button( + "Custom", + on_click=rx.toast( + "Custom Toast!", + position="top-right", + style={"background-color": "green", "color": "white", "border": "1px solid green", "border-radius": "0.53m"} + ) +) +``` + +The following props are available for customization: + +- `description`: `str | Var`: Toast's description, renders underneath the title. +- `close_button`: `bool`: Whether to show the close button. +- `invert`: `bool`: Dark toast in light mode and vice versa. +- `important`: `bool`: Control the sensitivity of the toast for screen readers. +- `duration`: `int`: Time in milliseconds that should elapse before automatically closing the toast. +- `position`: `LiteralPosition`: Position of the toast. +- `dismissible`: `bool`: If false, it'll prevent the user from dismissing the toast. +- `action`: `ToastAction`: Renders a primary button, clicking it will close the toast. +- `cancel`: `ToastAction`: Renders a secondary button, clicking it will close the toast. +- `id`: `str | Var`: Custom id for the toast. +- `unstyled`: `bool`: Removes the default styling, which allows for easier customization. +- `style`: `Style`: Custom style for the toast. +- `on_dismiss`: `Any`: The function gets called when either the close button is clicked, or the toast is swiped. +- `on_auto_close`: `Any`: Function that gets called when the toast disappears automatically after it's timeout (`duration` prop). + +## Toast Provider + +Using the `rx.toast` function require to have a toast provider in your app. + +`rx.toast.provider` is a component that provides a context for displaying toasts. It should be placed at the root of your app. + +```md alert warning +# In most case you will not need to include this component directly, as it is already included in `rx.app` as the `overlay_component` for displaying connections errors. +``` diff --git a/docs/library/overlay/tooltip.md b/docs/library/overlay/tooltip.md new file mode 100644 index 00000000000..32b9993e6cd --- /dev/null +++ b/docs/library/overlay/tooltip.md @@ -0,0 +1,60 @@ +--- +components: + - rx.tooltip + +Tooltip: | + lambda **props: rx.tooltip( + rx.button("Hover over me"), + content="This is the tooltip content.", + **props, + ) +--- + +```python exec +import reflex as rx +``` + +# Tooltip + +A `tooltip` displays informative information when users hover over or focus on an element. + +It takes a `content` prop, which is the content associated with the tooltip. + +```python demo +rx.tooltip( + rx.button("Hover over me"), + content="This is the tooltip content.", +) +``` + +## Events when the Tooltip opens or closes + +The `on_open_change` event is called when the `open` state of the tooltip changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class TooltipState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def index(): + return rx.flex( + rx.heading(f"Number of times tooltip opened or closed: {TooltipState.num_opens}"), + rx.heading(f"Tooltip open: {TooltipState.opened}"), + rx.text( + "Hover over the button to see the tooltip.", + rx.tooltip( + rx.button("Hover over me"), + content="This is the tooltip content.", + on_open_change=TooltipState.count_opens, + ), + ), + direction="column", + spacing="3", + ) +``` diff --git a/docs/library/tables-and-data-grids/data_editor.md b/docs/library/tables-and-data-grids/data_editor.md new file mode 100644 index 00000000000..37448be9a91 --- /dev/null +++ b/docs/library/tables-and-data-grids/data_editor.md @@ -0,0 +1,355 @@ +--- +components: + - rx.data_editor +--- + +# Data Editor + +A datagrid editor based on [Glide Data Grid](https://grid.glideapps.com/) + +```python exec +import reflex as rx +from typing import Any + +columns: list[dict[str, str]] = [ + { + "title":"Code", + "type": "str", + }, + { + "title":"Value", + "type": "int", + }, + { + "title":"Activated", + "type": "bool", + }, +] +data: list[list[Any]] = [ + ["A", 1, True], + ["B", 2, False], + ["C", 3, False], + ["D", 4, True], + ["E", 5, True], + ["F", 6, False], +] + +``` + +This component is introduced as an alternative to the [datatable](/docs/library/tables-and-data-grids/data_table) to support editing the displayed data. + +## Columns + +The columns definition should be a `list` of `dict`, each `dict` describing the associated columns. +Property of a column dict: + +- `title`: The text to display in the header of the column. +- `id`: An id for the column, if not defined, will default to a lower case of `title` +- `width`: The width of the column. +- `type`: The type of the columns, default to `"str"`. + +## Data + +The `data` props of `rx.data_editor` accept a `list` of `list`, where each `list` represent a row of data to display in the table. + +## Simple Example + +Here is a basic example of using the data_editor representing data with no interaction and no styling. Below we define the `columns` and the `data` which are taken in by the `rx.data_editor` component. When we define the `columns` we must define a `title` and a `type` for each column we create. The columns in the `data` must then match the defined `type` or errors will be thrown. + +```python demo box +rx.data_editor( + columns=columns, + data=data, +) +``` + +```python +columns: list[dict[str, str]] = [ + { + "title":"Code", + "type": "str", + }, + { + "title":"Value", + "type": "int", + }, + { + "title":"Activated", + "type": "bool", + }, +] +data: list[list[Any]] = [ + ["A", 1, True], + ["B", 2, False], + ["C", 3, False], + ["D", 4, True], + ["E", 5, True], + ["F", 6, False], +] +``` + +```python +rx.data_editor( + columns=columns, + data=data, +) +``` + +## Interactive Example + +```python exec +class DataEditorState_HP(rx.State): + + clicked_data: str = "Cell clicked: " + cols: list[Any] = [ + {"title": "Title", "type": "str"}, + { + "title": "Name", + "type": "str", + "group": "Data", + "width": 300, + }, + { + "title": "Birth", + "type": "str", + "id": "date", + "group": "Data", + "width": 150, + }, + { + "title": "Human", + "type": "bool", + "group": "Data", + "width": 80, + }, + { + "title": "House", + "type": "str", + "id": "date", + "group": "Data", + }, + { + "title": "Wand", + "type": "str", + "id": "date", + "group": "Data", + "width": 250, + }, + { + "title": "Patronus", + "type": "str", + "id": "date", + "group": "Data", + }, + { + "title": "Blood status", + "type": "str", + "id": "date", + "group": "Data", + "width": 200, + } + ] + + data = [ + ["1", "Harry James Potter", "31 July 1980", True, "Gryffindor", "11' Holly phoenix feather", "Stag", "Half-blood"], + ["2", "Ronald Bilius Weasley", "1 March 1980", True,"Gryffindor", "12' Ash unicorn tail hair", "Jack Russell terrier", "Pure-blood"], + ["3", "Hermione Jean Granger", "19 September, 1979", True, "Gryffindor", "10¾' vine wood dragon heartstring", "Otter", "Muggle-born"], + ["4", "Albus Percival Wulfric Brian Dumbledore", "Late August 1881", True, "Gryffindor", "15' Elder Thestral tail hair core", "Phoenix", "Half-blood"], + ["5", "Rubeus Hagrid", "6 December 1928", False, "Gryffindor", "16' Oak unknown core", "None", "Part-Human (Half-giant)"], + ["6", "Fred Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"], + ] + + def click_cell(self, pos): + col, row = pos + yield self.get_clicked_data(pos) + + + def get_clicked_data(self, pos) -> str: + self.clicked_data = f"Cell clicked: {pos}" + +``` + +Here we define a State, as shown below, that allows us to print the location of the cell as a heading when we click on it, using the `on_cell_clicked` `event trigger`. Check out all the other `event triggers` that you can use with datatable at the bottom of this page. We also define a `group` with a label `Data`. This groups all the columns with this `group` label under a larger group `Data` as seen in the table below. + +```python demo box +rx.heading(DataEditorState_HP.clicked_data) +``` + +```python demo box +rx.data_editor( + columns=DataEditorState_HP.cols, + data=DataEditorState_HP.data, + on_cell_clicked=DataEditorState_HP.click_cell, +) +``` + +```python +class DataEditorState_HP(rx.State): + + clicked_data: str = "Cell clicked: " + + cols: list[Any] = [ + { + "title": "Title", + "type": "str" + }, + { + "title": "Name", + "type": "str", + "group": "Data", + "width": 300, + }, + { + "title": "Birth", + "type": "str", + "group": "Data", + "width": 150, + }, + { + "title": "Human", + "type": "bool", + "group": "Data", + "width": 80, + }, + { + "title": "House", + "type": "str", + "group": "Data", + }, + { + "title": "Wand", + "type": "str", + "group": "Data", + "width": 250, + }, + { + "title": "Patronus", + "type": "str", + "group": "Data", + }, + { + "title": "Blood status", + "type": "str", + "group": "Data", + "width": 200, + } + ] + + data = [ + ["1", "Harry James Potter", "31 July 1980", True, "Gryffindor", "11' Holly phoenix feather", "Stag", "Half-blood"], + ["2", "Ronald Bilius Weasley", "1 March 1980", True,"Gryffindor", "12' Ash unicorn tail hair", "Jack Russell terrier", "Pure-blood"], + ["3", "Hermione Jean Granger", "19 September, 1979", True, "Gryffindor", "10¾' vine wood dragon heartstring", "Otter", "Muggle-born"], + ["4", "Albus Percival Wulfric Brian Dumbledore", "Late August 1881", True, "Gryffindor", "15' Elder Thestral tail hair core", "Phoenix", "Half-blood"], + ["5", "Rubeus Hagrid", "6 December 1928", False, "Gryffindor", "16' Oak unknown core", "None", "Part-Human (Half-giant)"], + ["6", "Fred Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"], + ] + + + def click_cell(self, pos): + col, row = pos + yield self.get_clicked_data(pos) + + + def get_clicked_data(self, pos) -> str: + self.clicked_data = f"Cell clicked: \{pos}" +``` + +```python +rx.data_editor( + columns=DataEditorState_HP.cols, + data=DataEditorState_HP.data, + on_cell_clicked=DataEditorState_HP.click_cell, +) +``` + +## Styling Example + +Now let's style our datatable to make it look more aesthetic and easier to use. We must first import `DataEditorTheme` and then we can start setting our style props as seen below in `dark_theme`. + +We then set these themes using `theme=DataEditorTheme(**dark_theme)`. On top of the styling we can also set some `props` to make some other aesthetic changes to our datatable. We have set the `row_height` to equal `50` so that the content is easier to read. We have also made the `smooth_scroll_x` and `smooth_scroll_y` equal `True` so that we can smoothly scroll along the columns and rows. Finally, we added `column_select=single`, where column select can take any of the following values `none`, `single` or `multiple`. + +```python exec +from reflex.components.datadisplay.dataeditor import DataEditorTheme +dark_theme = { + "accentColor": "#8c96ff", + "accentLight": "rgba(202, 206, 255, 0.253)", + "textDark": "#ffffff", + "textMedium": "#b8b8b8", + "textLight": "#a0a0a0", + "textBubble": "#ffffff", + "bgIconHeader": "#b8b8b8", + "fgIconHeader": "#000000", + "textHeader": "#a1a1a1", + "textHeaderSelected": "#000000", + "bgCell": "#16161b", + "bgCellMedium": "#202027", + "bgHeader": "#212121", + "bgHeaderHasFocus": "#474747", + "bgHeaderHovered": "#404040", + "bgBubble": "#212121", + "bgBubbleSelected": "#000000", + "bgSearchResult": "#423c24", + "borderColor": "rgba(225,225,225,0.2)", + "drilldownBorder": "rgba(225,225,225,0.4)", + "linkColor": "#4F5DFF", + "headerFontStyle": "bold 14px", + "baseFontStyle": "13px", + "fontFamily": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif", +} +``` + +```python demo box +rx.data_editor( + columns=DataEditorState_HP.cols, + data=DataEditorState_HP.data, + row_height=80, + smooth_scroll_x=True, + smooth_scroll_y=True, + column_select="single", + theme=DataEditorTheme(**dark_theme), + height="30vh", +) +``` + +```python +from reflex.components.datadisplay.dataeditor import DataEditorTheme +dark_theme_snake_case = { + "accent_color": "#8c96ff", + "accent_light": "rgba(202, 206, 255, 0.253)", + "text_dark": "#ffffff", + "text_medium": "#b8b8b8", + "text_light": "#a0a0a0", + "text_bubble": "#ffffff", + "bg_icon_header": "#b8b8b8", + "fg_icon_header": "#000000", + "text_header": "#a1a1a1", + "text_header_selected": "#000000", + "bg_cell": "#16161b", + "bg_cell_medium": "#202027", + "bg_header": "#212121", + "bg_header_has_focus": "#474747", + "bg_header_hovered": "#404040", + "bg_bubble": "#212121", + "bg_bubble_selected": "#000000", + "bg_search_result": "#423c24", + "border_color": "rgba(225,225,225,0.2)", + "drilldown_border": "rgba(225,225,225,0.4)", + "link_color": "#4F5DFF", + "header_font_style": "bold 14px", + "base_font_style": "13px", + "font_family": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif", +} +``` + +```python +rx.data_editor( + columns=DataEditorState_HP.cols, + data=DataEditorState_HP.data, + row_height=80, + smooth_scroll_x=True, + smooth_scroll_y=True, + column_select="single", + theme=DataEditorTheme(**dark_theme), + height="30vh", +) +``` diff --git a/docs/library/tables-and-data-grids/data_table.md b/docs/library/tables-and-data-grids/data_table.md new file mode 100644 index 00000000000..be56fc98e1e --- /dev/null +++ b/docs/library/tables-and-data-grids/data_table.md @@ -0,0 +1,71 @@ +--- +components: + - rx.data_table +--- + +```python exec +import reflex as rx +``` + +# Data Table + +The data table component is a great way to display static data in a table format. +You can pass in a pandas dataframe to the data prop to create the table. + +In this example we will read data from a csv file, convert it to a pandas dataframe and display it in a data_table. + +We will also add a search, pagination, sorting to the data_table to make it more accessible. + +If you want to [add, edit or remove data](/docs/library/tables-and-data-grids/table) in your app or deal with anything but static data then the [`rx.table`](/docs/library/tables-and-data-grids/table) might be a better fit for your use case. + +```python demo box +rx.data_table( + data=[ + ["Avery Bradley", "6-2", 25.0], + ["Jae Crowder", "6-6", 25.0], + ["John Holland", "6-5", 27.0], + ["R.J. Hunter", "6-5", 22.0], + ["Jonas Jerebko", "6-10", 29.0], + ["Amir Johnson", "6-9", 29.0], + ["Jordan Mickey", "6-8", 21.0], + ["Kelly Olynyk", "7-0", 25.0], + ["Terry Rozier", "6-2", 22.0], + ["Marcus Smart", "6-4", 22.0], + ], + columns=["Name", "Height", "Age"], + pagination=True, + search=True, + sort=True, +) +``` + +```python +import pandas as pd +nba_data = pd.read_csv("data/nba.csv") +... +rx.data_table( + data = nba_data[["Name", "Height", "Age"]], + pagination= True, + search= True, + sort= True, +) +``` + +📊 **Dataset source:** [nba.csv](https://media.geeksforgeeks.org/wp-content/uploads/nba.csv) + +The example below shows how to create a data table from from a list. + +```python +class State(rx.State): + data: List = [ + ["Lionel", "Messi", "PSG"], + ["Christiano", "Ronaldo", "Al-Nasir"] + ] + columns: List[str] = ["First Name", "Last Name"] + +def index(): + return rx.data_table( + data=State.data, + columns=State.columns, + ) +``` diff --git a/docs/library/tables-and-data-grids/table.md b/docs/library/tables-and-data-grids/table.md new file mode 100644 index 00000000000..61a90ab4f5d --- /dev/null +++ b/docs/library/tables-and-data-grids/table.md @@ -0,0 +1,1210 @@ +--- +components: + - rx.table.root + - rx.table.header + - rx.table.row + - rx.table.column_header_cell + - rx.table.body + - rx.table.cell + - rx.table.row_header_cell + +only_low_level: + - True + +TableRoot: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + width="80%", + **props, + ) + +TableRow: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + **props, + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell(rx.text("danilo@example.com", as_="p"), rx.text("danilo@yahoo.com", as_="p"), rx.text("danilo@gmail.com", as_="p"),), + rx.table.cell("Developer"), + **props, + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + **props, + ), + ), + width="80%", + ) + +TableColumnHeaderCell: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name", **props,), + rx.table.column_header_cell("Email", **props,), + rx.table.column_header_cell("Group", **props,), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + width="80%", + ) + +TableCell: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com", **props,), + rx.table.cell("Developer", **props,), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com", **props,), + rx.table.cell("Admin", **props,), + ), + ), + width="80%", + ) + +TableRowHeaderCell: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa", **props,), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa", **props,), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + width="80%", + ) +--- + +```python exec +import reflex as rx +class Customer(rx.Model, table=True): + name: str + email: str + phone: str + address: str +``` + +# Table + +A semantic table for presenting tabular data. + +If you just want to [represent static data](/docs/library/tables-and-data-grids/data_table) then the [`rx.data_table`](/docs/library/tables-and-data-grids/data_table) might be a better fit for your use case as it comes with in-built pagination, search and sorting. + +## Basic Example + +```python demo +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ),rx.table.row( + rx.table.row_header_cell("Jasper Eriks"), + rx.table.cell("jasper@example.com"), + rx.table.cell("Developer"), + ), + ), + width="100%", +) +``` + +```md alert info +# Set the table `width` to fit within its container and prevent it from overflowing. +``` + +## Showing State data (using foreach) + +Many times there is a need for the data we represent in our table to be dynamic. Dynamic data must be in `State`. Later we will show an example of how to access data from a database and how to load data from a source file. + +In this example there is a `people` data structure in `State` that is [iterated through using `rx.foreach`](/docs/components/rendering_iterables). + +```python demo exec +class TableForEachState(rx.State): + people: list[list] = [ + ["Danilo Sousa", "danilo@example.com", "Developer"], + ["Zahra Ambessa", "zahra@example.com", "Admin"], + ["Jasper Eriks", "jasper@example.com", "Developer"], + ] + +def show_person(person: list): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(person[0]), + rx.table.cell(person[1]), + rx.table.cell(person[2]), + ) + +def foreach_table_example(): + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body(rx.foreach(TableForEachState.people, show_person)), + width="100%", + ) +``` + +It is also possible to define a `class` such as `Person` below and then iterate through this data structure, as a `list[Person]`. + +```python +import dataclasses + +@dataclasses.dataclass +class Person: + full_name: str + email: str + group: str +``` + +## Sorting and Filtering (Searching) + +In this example we show two approaches to sort and filter data: + +1. Using SQL-like operations for database-backed models (simulated) +2. Using Python operations for in-memory data + +Both approaches use the same UI components: `rx.select` for sorting and `rx.input` for filtering. + +### Approach 1: Database Filtering and Sorting + +For database-backed models, we typically use SQL queries with `select`, `where`, and `order_by`. In this example, we'll simulate this behavior with mock data. + +```python demo exec +# Simulating database operations with mock data +class DatabaseTableState(rx.State): + # Mock data to simulate database records + users: list = [ + {"name": "John Doe", "email": "john@example.com", "phone": "555-1234", "address": "123 Main St"}, + {"name": "Jane Smith", "email": "jane@example.com", "phone": "555-5678", "address": "456 Oak Ave"}, + {"name": "Bob Johnson", "email": "bob@example.com", "phone": "555-9012", "address": "789 Pine Rd"}, + {"name": "Alice Brown", "email": "alice@example.com", "phone": "555-3456", "address": "321 Maple Dr"}, + ] + filtered_users: list[dict] = [] + sort_value = "" + search_value = "" + + + @rx.event + def load_entries(self): + """Simulate querying the database with filter and sort.""" + # Start with all users + result = self.users.copy() + + # Apply filtering if search value exists + if self.search_value != "": + search_term = self.search_value.lower() + result = [ + user for user in result + if any(search_term in str(value).lower() for value in user.values()) + ] + + # Apply sorting if sort column is selected + if self.sort_value != "": + result = sorted(result, key=lambda x: x[self.sort_value]) + + self.filtered_users = result + yield + + @rx.event + def sort_values(self, sort_value): + """Update sort value and reload data.""" + self.sort_value = sort_value + yield DatabaseTableState.load_entries() + + @rx.event + def filter_values(self, search_value): + """Update search value and reload data.""" + self.search_value = search_value + yield DatabaseTableState.load_entries() + + +def show_customer(user): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user["name"]), + rx.table.cell(user["email"]), + rx.table.cell(user["phone"]), + rx.table.cell(user["address"]), + ) + + +def database_table_example(): + return rx.vstack( + rx.select( + ["name", "email", "phone", "address"], + placeholder="Sort By: Name", + on_change=lambda value: DatabaseTableState.sort_values(value), + ), + rx.input( + placeholder="Search here...", + on_change=lambda value: DatabaseTableState.filter_values(value), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState.filtered_users, show_customer)), + on_mount=DatabaseTableState.load_entries, + width="100%", + ), + width="100%", + ) +``` + +### Approach 2: In-Memory Filtering and Sorting + +For in-memory data, we use Python operations like `sorted()` and list comprehensions. + +The state variable `_people` is set to be a backend-only variable. This is done in case the variable is very large in order to reduce network traffic and improve performance. + +When a `select` item is selected, the `on_change` event trigger is hooked up to the `set_sort_value` event handler. Every base var has a built-in event handler to set its value for convenience, called `set_VARNAME`. + +`current_people` is an `rx.var(cache=True)`. It is a var that is only recomputed when the other state vars it depends on change. This ensures that the `People` shown in the table are always up to date whenever they are searched or sorted. + +```python demo exec +import dataclasses + +@dataclasses.dataclass +class Person: + full_name: str + email: str + group: str + + +class InMemoryTableState(rx.State): + + _people: list[Person] = [ + Person(full_name="Danilo Sousa", email="danilo@example.com", group="Developer"), + Person(full_name="Zahra Ambessa", email="zahra@example.com", group="Admin"), + Person(full_name="Jasper Eriks", email="zjasper@example.com", group="B-Developer"), + ] + + sort_value = "" + search_value = "" + + @rx.event + def set_sort_value(self, value: str): + self.sort_value = value + + @rx.event + def set_search_value(self, value: str): + self.search_value = value + + @rx.var(cache=True) + def current_people(self) -> list[Person]: + people = self._people + + if self.sort_value != "": + people = sorted( + people, key=lambda user: getattr(user, self.sort_value).lower() + ) + + if self.search_value != "": + people = [ + person for person in people + if any( + self.search_value.lower() in getattr(person, attr).lower() + for attr in ['full_name', 'email', 'group'] + ) + ] + return people + + +def show_person(person: Person): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(person.full_name), + rx.table.cell(person.email), + rx.table.cell(person.group), + ) + +def in_memory_table_example(): + return rx.vstack( + rx.select( + ["full_name", "email", "group"], + placeholder="Sort By: full_name", + on_change=InMemoryTableState.set_sort_value, + ), + rx.input( + placeholder="Search here...", + on_change=InMemoryTableState.set_search_value, + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body(rx.foreach(InMemoryTableState.current_people, show_person)), + width="100%", + ), + width="100%", + ) +``` + +### When to Use Each Approach + +- **Database Approach**: Best for large datasets or when the data already exists in a database +- **In-Memory Approach**: Best for smaller datasets, prototyping, or when the data is static or loaded from an API + +Both approaches provide the same user experience with filtering and sorting functionality. + +# Database + +The more common use case for building an `rx.table` is to use data from a database. + +The code below shows how to load data from a database and place it in an `rx.table`. + +## Loading data into table + +A `Customer` [model](/docs/database/tables) is defined that inherits from `rx.Model`. + +The `load_entries` event handler executes a [query](/docs/database/queries) that is used to request information from a database table. This `load_entries` event handler is called on the `on_mount` event trigger of the `rx.table.root`. + +If you want to load the data when the page in the app loads you can set `on_load` in `app.add_page()` to equal this event handler, like `app.add_page(page_name, on_load=State.load_entries)`. + +```python +class Customer(rx.Model, table=True): + """The customer model.""" + name: str + email: str + phone: str + address: str +``` + +```python exec +class DatabaseTableState(rx.State): + + users: list[dict] = [] + + @rx.event + def load_entries(self): + """Get all users from the database.""" + customers_json = [ + { + "name": "John Doe", + "email": "john@example.com", + "phone": "555-1234", + "address": "123 Main St" + }, + { + "name": "Jane Smith", + "email": "jane@example.com", + "phone": "555-5678", + "address": "456 Oak Ave" + }, + { + "name": "Bob Johnson", + "email": "bob@example.com", + "phone": "555-9012", + "address": "789 Pine Blvd" + }, + { + "name": "Alice Williams", + "email": "alice@example.com", + "phone": "555-3456", + "address": "321 Maple Dr" + } + ] + self.users = customers_json + +def show_customer(user: dict): + return rx.table.row( + rx.table.cell(user["name"]), + rx.table.cell(user["email"]), + rx.table.cell(user["phone"]), + rx.table.cell(user["address"]), + ) + +def loading_data_table_example(): + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState.users, show_customer)), + on_mount=DatabaseTableState.load_entries, + width="100%", + margin_bottom="1em", +) +``` + +```python eval +loading_data_table_example() +``` + +```python +from sqlmodel import select + +class DatabaseTableState(rx.State): + + users: list[Customer] = [] + + @rx.event + def load_entries(self): + """Get all users from the database.""" + with rx.session() as session: + self.users = session.exec(select(Customer)).all() + + +def show_customer(user: Customer): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + +def loading_data_table_example(): + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState.users, show_customer)), + on_mount=DatabaseTableState.load_entries, + width="100%", + ) + +``` + +## Filtering (Searching) and Sorting + +In this example we sort and filter the data. + +For sorting the `rx.select` component is used. The data is sorted based on the attributes of the `Customer` class. When a select item is selected, as the `on_change` event trigger is hooked up to the `sort_values` event handler, the data is sorted based on the state variable `sort_value` attribute selected. + +The sorting query gets the `sort_column` based on the state variable `sort_value`, it gets the order using the `asc` function from sql and finally uses the `order_by` function. + +For filtering the `rx.input` component is used. The data is filtered based on the search query entered into the `rx.input` component. When a search query is entered, as the `on_change` event trigger is hooked up to the `filter_values` event handler, the data is filtered based on if the state variable `search_value` is present in any of the data in that specific `Customer`. + +The `%` character before and after `search_value` makes it a wildcard pattern that matches any sequence of characters before or after the `search_value`. `query.where(...)` modifies the existing query to include a filtering condition. The `or_` operator is a logical OR operator that combines multiple conditions. The query will return results that match any of these conditions. `Customer.name.ilike(search_value)` checks if the `name` column of the `Customer` table matches the `search_value` pattern in a case-insensitive manner (`ilike` stands for "case-insensitive like"). + +```python +class Customer(rx.Model, table=True): + """The customer model.""" + + name: str + email: str + phone: str + address: str +``` + +```python exec +class DatabaseTableState2(rx.State): + + # Mock data to simulate database records + _users: list[dict] = [ + {"name": "John Doe", "email": "john@example.com", "phone": "555-1234", "address": "123 Main St"}, + {"name": "Jane Smith", "email": "jane@example.com", "phone": "555-5678", "address": "456 Oak Ave"}, + {"name": "Bob Johnson", "email": "bob@example.com", "phone": "555-9012", "address": "789 Pine Rd"}, + {"name": "Alice Brown", "email": "alice@example.com", "phone": "555-3456", "address": "321 Maple Dr"}, + {"name": "Charlie Wilson", "email": "charlie@example.com", "phone": "555-7890", "address": "654 Elm St"}, + {"name": "Emily Davis", "email": "emily@example.com", "phone": "555-2345", "address": "987 Cedar Ln"}, + ] + + users: list[dict] = [] + sort_value = "" + search_value = "" + + @rx.event + def load_entries(self): + # Start with all users + result = self._users.copy() + + # Apply filtering if search value exists + if self.search_value != "": + search_term = self.search_value.lower() + result = [ + user for user in result + if any(search_term in str(value).lower() for value in user.values()) + ] + + # Apply sorting if sort column is selected + if self.sort_value != "": + result = sorted(result, key=lambda x: x[self.sort_value]) + + self.users = result + + @rx.event + def sort_values(self, sort_value): + self.sort_value = sort_value + yield DatabaseTableState2.load_entries() + + @rx.event + def filter_values(self, search_value): + self.search_value = search_value + yield DatabaseTableState2.load_entries() + + +def show_customer_2(user: dict): + return rx.table.row( + rx.table.cell(user["name"]), + rx.table.cell(user["email"]), + rx.table.cell(user["phone"]), + rx.table.cell(user["address"]), + ) + +def loading_data_table_example_2(): + return rx.vstack( + rx.select( + ["name", "email", "phone", "address"], + placeholder="Sort By: Name", + on_change= lambda value: DatabaseTableState2.sort_values(value), + ), + rx.input( + placeholder="Search here...", + on_change= lambda value: DatabaseTableState2.filter_values(value), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState2.users, show_customer_2)), + on_mount=DatabaseTableState2.load_entries, + width="100%", + ), + width="100%", + margin_bottom="1em", +) +``` + +```python eval +loading_data_table_example_2() +``` + +```python +from sqlmodel import select, asc, or_ + + +class DatabaseTableState2(rx.State): + + users: list[Customer] = [] + + sort_value = "" + search_value = "" + + @rx.event + def load_entries(self): + """Get all users from the database.""" + with rx.session() as session: + query = select(Customer) + + if self.search_value != "": + search_value = self.search_value.lower() + query = query.where( + or_( + Customer.name.ilike(search_value), + Customer.email.ilike(search_value), + Customer.phone.ilike(search_value), + Customer.address.ilike(search_value), + ) + ) + + if self.sort_value != "": + sort_column = getattr(Customer, self.sort_value) + order = asc(sort_column) + query = query.order_by(order) + + self.users = session.exec(query).all() + + @rx.event + def sort_values(self, sort_value): + print(sort_value) + self.sort_value = sort_value + self.load_entries() + + @rx.event + def filter_values(self, search_value): + print(search_value) + self.search_value = search_value + self.load_entries() + + +def show_customer(user: Customer): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + + +def loading_data_table_example2(): + return rx.vstack( + rx.select( + ["name", "email", "phone", "address"], + placeholder="Sort By: Name", + on_change= lambda value: DatabaseTableState2.sort_values(value), + ), + rx.input( + placeholder="Search here...", + on_change= lambda value: DatabaseTableState2.filter_values(value), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState2.users, show_customer)), + on_mount=DatabaseTableState2.load_entries, + width="100%", + ), + width="100%", + ) + +``` + +## Pagination + +Pagination is an important part of database management, especially when working with large datasets. It helps in enabling efficient data retrieval by breaking down results into manageable loads. + +The purpose of this code is to retrieve a specific subset of rows from the `Customer` table based on the specified pagination parameters `offset` and `limit`. + +`query.offset(self.offset)` modifies the query to skip a certain number of rows before returning the results. The number of rows to skip is specified by `self.offset`. + +`query.limit(self.limit)` modifies the query to limit the number of rows returned. The maximum number of rows to return is specified by `self.limit`. + +```python exec +class DatabaseTableState3(rx.State): + + _mock_data: list[Customer] = [ + Customer(name="John Doe", email="john@example.com", phone="555-1234", address="123 Main St"), + Customer(name="Jane Smith", email="jane@example.com", phone="555-5678", address="456 Oak Ave"), + Customer(name="Bob Johnson", email="bob@example.com", phone="555-9012", address="789 Pine Rd"), + Customer(name="Alice Brown", email="alice@example.com", phone="555-3456", address="321 Maple Dr"), + Customer(name="Charlie Wilson", email="charlie@example.com", phone="555-7890", address="654 Elm St"), + Customer(name="Emily Davis", email="emily@example.com", phone="555-2345", address="987 Cedar Ln"), + ] + users: list[Customer] = [] + + total_items: int + offset: int = 0 + limit: int = 3 + + @rx.var(cache=True) + def page_number(self) -> int: + return ( + (self.offset // self.limit) + + 1 + + (1 if self.offset % self.limit else 0) + ) + + @rx.var(cache=True) + def total_pages(self) -> int: + return self.total_items // self.limit + ( + 1 if self.total_items % self.limit else 0 + ) + + @rx.event + def prev_page(self): + self.offset = max(self.offset - self.limit, 0) + self.load_entries() + + @rx.event + def next_page(self): + if self.offset + self.limit < self.total_items: + self.offset += self.limit + self.load_entries() + + def _get_total_items(self, session): + self.total_items = session.exec(select(func.count(Customer.id))).one() + + @rx.event + def load_entries(self): + self.users = self._mock_data[self.offset:self.offset + self.limit] + self.total_items = len(self._mock_data) + + +def show_customer(user: Customer): + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + + +def loading_data_table_example3(): + return rx.vstack( + rx.hstack( + rx.button( + "Prev", + on_click=DatabaseTableState3.prev_page, + ), + rx.text( + f"Page {DatabaseTableState3.page_number} / {DatabaseTableState3.total_pages}" + ), + rx.button( + "Next", + on_click=DatabaseTableState3.next_page, + ), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState3.users, show_customer)), + on_mount=DatabaseTableState3.load_entries, + width="100%", + ), + width="100%", + margin_bottom="1em", + ) +``` + +```python eval +loading_data_table_example3() +``` + +```python +from sqlmodel import select, func + + +class DatabaseTableState3(rx.State): + + users: list[Customer] = [] + + total_items: int + offset: int = 0 + limit: int = 3 + + @rx.var(cache=True) + def page_number(self) -> int: + return ( + (self.offset // self.limit) + + 1 + + (1 if self.offset % self.limit else 0) + ) + + @rx.var(cache=True) + def total_pages(self) -> int: + return self.total_items // self.limit + ( + 1 if self.total_items % self.limit else 0 + ) + + @rx.event + def prev_page(self): + self.offset = max(self.offset - self.limit, 0) + self.load_entries() + + @rx.event + def next_page(self): + if self.offset + self.limit < self.total_items: + self.offset += self.limit + self.load_entries() + + def _get_total_items(self, session): + """Return the total number of items in the Customer table.""" + self.total_items = session.exec(select(func.count(Customer.id))).one() + + @rx.event + def load_entries(self): + """Get all users from the database.""" + with rx.session() as session: + query = select(Customer) + + # Apply pagination + query = query.offset(self.offset).limit(self.limit) + + self.users = session.exec(query).all() + self._get_total_items(session) + + +def show_customer(user: Customer): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + + +def loading_data_table_example3(): + return rx.vstack( + rx.hstack( + rx.button( + "Prev", + on_click=DatabaseTableState3.prev_page, + ), + rx.text( + f"Page {DatabaseTableState3.page_number} / {DatabaseTableState3.total_pages}" + ), + rx.button( + "Next", + on_click=DatabaseTableState3.next_page, + ), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState3.users, show_customer)), + on_mount=DatabaseTableState3.load_entries, + width="100%", + ), + width="100%", + ) + +``` + +## More advanced examples + +The real power of the `rx.table` comes where you are able to visualise, add and edit data live in your app. Check out these apps and code to see how this is done: app: https://customer-data-app.reflex.run code: https://github.com/reflex-dev/templates/tree/main/customer_data_app and code: https://github.com/reflex-dev/templates/tree/main/sales. + +# Download + +Most users will want to download their data after they have got the subset that they would like in their table. + +In this example there are buttons to download the data as a `json` and as a `csv`. + +For the `json` download the `rx.download` is in the frontend code attached to the `on_click` event trigger for the button. This works because if the `Var` is not already a string, it will be converted to a string using `JSON.stringify`. + +For the `csv` download the `rx.download` is in the backend code as an event_handler `download_csv_data`. There is also a helper function `_convert_to_csv` that converts the data in `self.users` to `csv` format. + +```python exec +import io +import csv +import json + +class TableDownloadState(rx.State): + _mock_data: list[Customer] = [ + Customer(name="John Doe", email="john@example.com", phone="555-1234", address="123 Main St"), + Customer(name="Jane Smith", email="jane@example.com", phone="555-5678", address="456 Oak Ave"), + Customer(name="Bob Johnson", email="bob@example.com", phone="555-9012", address="789 Pine Rd"), + ] + users: list[Customer] = [] + + @rx.event + def load_entries(self): + self.users = self._mock_data + + def _convert_to_csv(self) -> str: + if not self.users: + self.load_entries() + + fieldnames = ["id", "name", "email", "phone", "address"] + output = io.StringIO() + writer = csv.DictWriter(output, fieldnames=fieldnames) + writer.writeheader() + for user in self.users: + writer.writerow(user.dict()) + + csv_data = output.getvalue() + output.close() + return csv_data + + + def _convert_to_json(self) -> str: + return json.dumps([u.dict() for u in self._mock_data], indent=2) + + @rx.event + def download_csv_data(self): + csv_data = self._convert_to_csv() + return rx.download( + data=csv_data, + filename="data.csv", + ) + + @rx.event + def download_json_data(self): + json_data = self._convert_to_json() + return rx.download( + data=json_data, + filename="data.json", + ) + +def show_customer(user: Customer): + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + +def download_data_table_example(): + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(TableDownloadState.users, show_customer)), + width="100%", + on_mount=TableDownloadState.load_entries, + ), + rx.hstack( + rx.button( + "Download as JSON", + on_click=TableDownloadState.download_json_data, + ), + rx.button( + "Download as CSV", + on_click=TableDownloadState.download_csv_data, + ), + spacing="7", + ), + width="100%", + spacing="5", + margin_bottom="1em", + ) + +``` + +```python eval +download_data_table_example() +``` + +```python +import io +import csv +from sqlmodel import select + +class TableDownloadState(rx.State): + + users: list[Customer] = [] + + @rx.event + def load_entries(self): + """Get all users from the database.""" + with rx.session() as session: + self.users = session.exec(select(Customer)).all() + + + def _convert_to_csv(self) -> str: + """Convert the users data to CSV format.""" + + # Make sure to load the entries first + if not self.users: + self.load_entries() + + # Define the CSV file header based on the Customer model's attributes + fieldnames = list(Customer.__fields__) + + # Create a string buffer to hold the CSV data + output = io.StringIO() + writer = csv.DictWriter(output, fieldnames=fieldnames) + writer.writeheader() + for user in self.users: + writer.writerow(user.dict()) + + # Get the CSV data as a string + csv_data = output.getvalue() + output.close() + return csv_data + + @rx.event + def download_csv_data(self): + csv_data = self._convert_to_csv() + return rx.download( + data=csv_data, + filename="data.csv", + ) + + +def show_customer(user: Customer): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + +def download_data_table_example(): + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(TableDownloadState.users, show_customer)), + width="100%", + on_mount=TableDownloadState.load_entries, + ), + rx.hstack( + rx.button( + "Download as JSON", + on_click=rx.download( + data=TableDownloadState.users, + filename="data.json", + ), + ), + rx.button( + "Download as CSV", + on_click=TableDownloadState.download_csv_data, + ), + spacing="7", + ), + width="100%", + spacing="5", + ) + +``` + +# Real World Example UI + +```python demo +rx.flex( + rx.heading("Your Team"), + rx.text("Invite and manage your team members"), + rx.flex( + rx.input(placeholder="Email Address"), + rx.button("Invite"), + justify="center", + spacing="2", + ), + rx.table.root( + rx.table.body( + rx.table.row( + rx.table.cell(rx.avatar(fallback="DS")), + rx.table.row_header_cell(rx.link("Danilo Sousa")), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + align="center", + ), + rx.table.row( + rx.table.cell(rx.avatar(fallback="ZA")), + rx.table.row_header_cell(rx.link("Zahra Ambessa")), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + align="center", + ), + rx.table.row( + rx.table.cell(rx.avatar(fallback="JE")), + rx.table.row_header_cell(rx.link("Jasper Eriksson")), + rx.table.cell("jasper@example.com"), + rx.table.cell("Developer"), + align="center", + ), + ), + width="100%", + ), + width="100%", + direction="column", + spacing="2", +) +``` diff --git a/docs/library/typography/blockquote.md b/docs/library/typography/blockquote.md new file mode 100644 index 00000000000..33b79ac34cc --- /dev/null +++ b/docs/library/typography/blockquote.md @@ -0,0 +1,77 @@ +--- +components: + - rx.blockquote +--- + +```python exec +import reflex as rx +``` + +# Blockquote + +```python demo +rx.blockquote("Perfect typography is certainly the most elusive of all arts.") +``` + +## Size + +Use the `size` prop to control the size of the blockquote. The prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="1"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="2"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="3"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="4"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="5"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="6"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="7"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="8"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="9"), + direction="column", + spacing="3", +) +``` + +## Weight + +Use the `weight` prop to set the blockquote weight. + +```python demo +rx.flex( + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", weight="light"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", weight="regular"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", weight="medium"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", weight="bold"), + direction="column", + spacing="3", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", color_scheme="indigo"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", color_scheme="cyan"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", color_scheme="crimson"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", color_scheme="orange"), + direction="column", + spacing="3", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.blockquote("Perfect typography is certainly the most elusive of all arts."), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", high_contrast=True), + direction="column", + spacing="3", +) +``` diff --git a/docs/library/typography/code.md b/docs/library/typography/code.md new file mode 100644 index 00000000000..8db6cf613b2 --- /dev/null +++ b/docs/library/typography/code.md @@ -0,0 +1,110 @@ +--- +components: + - rx.code +--- + +```python exec +import reflex as rx +``` + +# Code + +```python demo +rx.code("console.log()") +``` + +## Size + +Use the `size` prop to control text size. This prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.code("console.log()", size="1"), + rx.code("console.log()", size="2"), + rx.code("console.log()", size="3"), + rx.code("console.log()", size="4"), + rx.code("console.log()", size="5"), + rx.code("console.log()", size="6"), + rx.code("console.log()", size="7"), + rx.code("console.log()", size="8"), + rx.code("console.log()", size="9"), + direction="column", + spacing="3", + align="start", +) +``` + +## Weight + +Use the `weight` prop to set the text weight. + +```python demo +rx.flex( + rx.code("console.log()", weight="light"), + rx.code("console.log()", weight="regular"), + rx.code("console.log()", weight="medium"), + rx.code("console.log()", weight="bold"), + direction="column", + spacing="3", +) +``` + +## Variant + +Use the `variant` prop to control the visual style. + +```python demo +rx.flex( + rx.code("console.log()", variant="solid"), + rx.code("console.log()", variant="soft"), + rx.code("console.log()", variant="outline"), + rx.code("console.log()", variant="ghost"), + direction="column", + spacing="2", + align="start", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.code("console.log()", color_scheme="indigo"), + rx.code("console.log()", color_scheme="crimson"), + rx.code("console.log()", color_scheme="orange"), + rx.code("console.log()", color_scheme="cyan"), + direction="column", + spacing="2", + align="start", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.flex( + rx.code("console.log()", variant="solid"), + rx.code("console.log()", variant="soft"), + rx.code("console.log()", variant="outline"), + rx.code("console.log()", variant="ghost"), + direction="column", + align="start", + spacing="2", + ), + rx.flex( + rx.code("console.log()", variant="solid", high_contrast=True), + rx.code("console.log()", variant="soft", high_contrast=True), + rx.code("console.log()", variant="outline", high_contrast=True), + rx.code("console.log()", variant="ghost", high_contrast=True), + direction="column", + align="start", + spacing="2", + ), + spacing="3", +) +``` diff --git a/docs/library/typography/em.md b/docs/library/typography/em.md new file mode 100644 index 00000000000..a977335333c --- /dev/null +++ b/docs/library/typography/em.md @@ -0,0 +1,16 @@ +--- +components: + - rx.text.em +--- + +```python exec +import reflex as rx +``` + +# Em (Emphasis) + +Marks text to stress emphasis. + +```python demo +rx.text("We ", rx.text.em("had"), " to do something about it.") +``` diff --git a/docs/library/typography/heading.md b/docs/library/typography/heading.md new file mode 100644 index 00000000000..7d0a3737328 --- /dev/null +++ b/docs/library/typography/heading.md @@ -0,0 +1,152 @@ +--- +components: + - rx.heading +--- + +```python exec +import reflex as rx +``` + +# Heading + +```python demo +rx.heading("The quick brown fox jumps over the lazy dog.") +``` + +## As another element + +Use the `as_` prop to change the heading level. This prop is purely semantic and does not change the visual appearance. + +```python demo +rx.flex( + rx.heading("Level 1", as_="h1"), + rx.heading("Level 2", as_="h2"), + rx.heading("Level 3", as_="h3"), + direction="column", + spacing="3", +) +``` + +## Size + +Use the `size` prop to control the size of the heading. The prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease + +```python demo +rx.flex( + rx.heading("The quick brown fox jumps over the lazy dog.", size="1"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="2"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="3"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="4"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="5"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="6"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="7"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="8"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="9"), + direction="column", + spacing="3", +) +``` + +## Weight + +Use the `weight` prop to set the text weight. + +```python demo +rx.flex( + rx.heading("The quick brown fox jumps over the lazy dog.", weight="light"), + rx.heading("The quick brown fox jumps over the lazy dog.", weight="regular"), + rx.heading("The quick brown fox jumps over the lazy dog.", weight="medium"), + rx.heading("The quick brown fox jumps over the lazy dog.", weight="bold"), + direction="column", + spacing="3", +) +``` + +## Align + +Use the `align` prop to set text alignment. + +```python demo +rx.flex( + rx.heading("Left-aligned", align="left"), + rx.heading("Center-aligned", align="center"), + rx.heading("Right-aligned", align="right"), + direction="column", + spacing="3", + width="100%", +) +``` + +## Trim + +Use the `trim` prop to trim the leading space at the start, end, or both sides of the text. + +```python demo +rx.flex( + rx.heading("Without Trim", + trim="normal", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + rx.heading("With Trim", + trim="both", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + direction="column", + spacing="3", +) +``` + +Trimming the leading is useful when dialing in vertical spacing in cards or other “boxy” components. Otherwise, padding looks larger on top and bottom than on the sides. + +```python demo +rx.flex( + rx.box( + rx.heading("Without trim", margin_bottom="4px", size="3",), + rx.text("The goal of typography is to relate font size, line height, and line width in a proportional way that maximizes beauty and makes reading easier and more pleasant."), + style={"background": "var(--gray-a2)", + "border": "1px dashed var(--gray-a7)",}, + padding="16px", + ), + rx.box( + rx.heading("With trim", margin_bottom="4px", size="3", trim="start"), + rx.text("The goal of typography is to relate font size, line height, and line width in a proportional way that maximizes beauty and makes reading easier and more pleasant."), + style={"background": "var(--gray-a2)", + "border": "1px dashed var(--gray-a7)",}, + padding="16px", + ), + direction="column", + spacing="3", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="indigo"), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="cyan"), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="crimson"), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="orange"), + direction="column", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="indigo", high_contrast=True), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="cyan", high_contrast=True), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="crimson", high_contrast=True), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="orange", high_contrast=True), + direction="column", +) +``` diff --git a/docs/library/typography/kbd.md b/docs/library/typography/kbd.md new file mode 100644 index 00000000000..e20078b75dd --- /dev/null +++ b/docs/library/typography/kbd.md @@ -0,0 +1,36 @@ +--- +components: + - rx.text.kbd +--- + +```python exec +import reflex as rx +``` + +# rx.text.kbd (Keyboard) + +Represents keyboard input or a hotkey. + +```python demo +rx.text.kbd("Shift + Tab") +``` + +## Size + +Use the `size` prop to control text size. This prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.text.kbd("Shift + Tab", size="1"), + rx.text.kbd("Shift + Tab", size="2"), + rx.text.kbd("Shift + Tab", size="3"), + rx.text.kbd("Shift + Tab", size="4"), + rx.text.kbd("Shift + Tab", size="5"), + rx.text.kbd("Shift + Tab", size="6"), + rx.text.kbd("Shift + Tab", size="7"), + rx.text.kbd("Shift + Tab", size="8"), + rx.text.kbd("Shift + Tab", size="9"), + direction="column", + spacing="3", +) +``` diff --git a/docs/library/typography/link.md b/docs/library/typography/link.md new file mode 100644 index 00000000000..fd70d985ef8 --- /dev/null +++ b/docs/library/typography/link.md @@ -0,0 +1,146 @@ +--- +components: + - rx.link +--- + +```python exec +import reflex as rx +``` + +# Link + +Links are accessible elements used primarily for navigation. Use the `href` prop to specify the location for the link to navigate to. + +```python demo +rx.link("Reflex Home Page.", href="https://reflex.dev/") +``` + +You can also provide local links to other pages in your project without writing the full url. + +```python demo +rx.link("Example", href="/docs/library",) +``` + +The `link` component can be used to wrap other components to make them link to other pages. + +```python demo +rx.link(rx.button("Example"), href="https://reflex.dev/") +``` + +You can also create anchors to link to specific parts of a page using the `id` prop. + +```python demo +rx.box("Example", id="example") +``` + +To reference an anchor, you can use the `href` prop of the `link` component. The `href` should be in the format of the page you want to link to followed by a # and the id of the anchor. + +```python demo +rx.link("Example", href="/docs/library/typography/link#example") +``` + +```md alert info +# Redirecting the user using State + +It is also possible to redirect the user to a new path within the application, using `rx.redirect()`. Check out the docs [here](/docs/api-reference/special_events). +``` + +# Style + +## Size + +Use the `size` prop to control the size of the link. The prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog.", size="1"), + rx.link("The quick brown fox jumps over the lazy dog.", size="2"), + rx.link("The quick brown fox jumps over the lazy dog.", size="3"), + rx.link("The quick brown fox jumps over the lazy dog.", size="4"), + rx.link("The quick brown fox jumps over the lazy dog.", size="5"), + rx.link("The quick brown fox jumps over the lazy dog.", size="6"), + rx.link("The quick brown fox jumps over the lazy dog.", size="7"), + rx.link("The quick brown fox jumps over the lazy dog.", size="8"), + rx.link("The quick brown fox jumps over the lazy dog.", size="9"), + direction="column", + spacing="3", +) +``` + +## Weight + +Use the `weight` prop to set the text weight. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog.", weight="light"), + rx.link("The quick brown fox jumps over the lazy dog.", weight="regular"), + rx.link("The quick brown fox jumps over the lazy dog.", weight="medium"), + rx.link("The quick brown fox jumps over the lazy dog.", weight="bold"), + direction="column", + spacing="3", +) +``` + +## Trim + +Use the `trim` prop to trim the leading space at the start, end, or both sides of the rendered text. + +```python demo +rx.flex( + rx.link("Without Trim", + trim="normal", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + rx.link("With Trim", + trim="both", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + direction="column", + spacing="3", +) +``` + +## Underline + +Use the `underline` prop to manage the visibility of the underline affordance. It defaults to `auto`. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog.", underline="auto"), + rx.link("The quick brown fox jumps over the lazy dog.", underline="hover"), + rx.link("The quick brown fox jumps over the lazy dog.", underline="always"), + direction="column", + spacing="3", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog.", color_scheme="indigo"), + rx.link("The quick brown fox jumps over the lazy dog.", color_scheme="cyan"), + rx.link("The quick brown fox jumps over the lazy dog.", color_scheme="crimson"), + rx.link("The quick brown fox jumps over the lazy dog.", color_scheme="orange"), + direction="column", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog."), + rx.link("The quick brown fox jumps over the lazy dog.", high_contrast=True), + direction="column", +) +``` diff --git a/docs/library/typography/markdown.md b/docs/library/typography/markdown.md new file mode 100644 index 00000000000..19f19a07725 --- /dev/null +++ b/docs/library/typography/markdown.md @@ -0,0 +1,196 @@ +--- +components: + - rx.markdown +--- + +```python exec +import reflex as rx +``` + +# Markdown + +The `rx.markdown` component can be used to render markdown text. +It is based on [Github Flavored Markdown](https://github.github.com/gfm/). + +```python demo +rx.vstack( + rx.markdown("# Hello World!"), + rx.markdown("## Hello World!"), + rx.markdown("### Hello World!"), + rx.markdown("Support us on [Github](https://github.com/reflex-dev/reflex)."), + rx.markdown("Use `reflex deploy` to deploy your app with **a single command**."), +) +``` + +## Math Equations + +You can render math equations using LaTeX. +For inline equations, surround the equation with `$`: + +```python demo +rx.markdown("Pythagorean theorem: $a^2 + b^2 = c^2$.") +``` + +## Syntax Highlighting + +You can render code blocks with syntax highlighting using the \`\`\`\{language} syntax: + +````python demo +rx.markdown( +r""" +```python +import reflex as rx +from .pages import index + +app = rx.App() +app.add_page(index) +``` +""" +) +```` + +## Tables + +You can render tables using the `|` syntax: + +```python demo +rx.markdown( + """ +| Syntax | Description | +| ----------- | ----------- | +| Header | Title | +| Paragraph | Text | +""" +) +``` + +## Plugins + +Plugins can be used to extend the functionality of the markdown renderer. + +By default Reflex uses the following plugins: + +- `remark-gfm` for Github Flavored Markdown support (`use_gfm`). +- `remark-math` and `rehype-katex` for math equation support (`use_math`, `use_katex`). +- `rehype-unwrap-images` to remove paragraph tags around images (`use_unwrap_images`). +- `rehype-raw` to render raw HTML in markdown (`use_raw`). NOTE: in a future release this will be disabled by default for security reasons. + +These default plugins can be disabled by passing `use_[plugin_name]=False` to the `rx.markdown` component. For example, to disable raw HTML rendering, use `rx.markdown(..., use_raw=False)`. + +## Arbitrary Plugins + +You can also add arbitrary remark or rehype plugins using the `remark_plugins` +and `rehype_plugins` props in conjunction with the `rx.markdown.plugin` helper. + +`rx.markdown.plugin` takes two arguments: + +1. The npm package name and version of the plugin (e.g. `remark-emoji@5.0.2`). +2. The named export to use from the plugin (e.g. `remarkEmoji`). + +### Remark Plugin Example + +For example, to add support for emojis using the `remark-emoji` plugin: + +```python demo +rx.markdown( + "Hello :smile:! :rocket: :tada:", + remark_plugins=[ + rx.markdown.plugin("remark-emoji@5.0.2", "remarkEmoji"), + ], +) +``` + +### Rehype Plugin Example + +To make `rehype-raw` safer for untrusted HTML input we can use `rehype-sanitize`, which defaults to a safe schema similar to that used by Github. + +```python demo +rx.markdown( + """Here is some **bold** text and a .""", + use_raw=True, + rehype_plugins=[ + rx.markdown.plugin("rehype-sanitize@5.0.1", "rehypeSanitize"), + ], +) +``` + +### Plugin Options + +Both `remark_plugins` and `rehype_plugins` accept a heterogeneous list of `plugin` +or tuple of `(plugin, options)` in case the plugin requires some kind of special +configuration. + +For example, `rehype-slug` is a simple plugin that adds ID attributes to the +headings, but the `rehype-autolink-headings` plugin accepts options to specify +how to render the links to those anchors. + +```python demo +rx.markdown( + """ +# Heading 1 +## Heading 2 +""", + rehype_plugins=[ + rx.markdown.plugin("rehype-slug@6.0.0", "rehypeSlug"), + ( + rx.markdown.plugin("rehype-autolink-headings@7.1.0", "rehypeAutolinkHeadings"), + { + "behavior": "wrap", + "properties": { + "className": ["heading-link"], + }, + }, + ), + ], +) +``` + +## Component Map + +You can specify which components to use for rendering markdown elements using the +`component_map` prop. + +Each key in the `component_map` prop is a markdown element, and the value is +a function that takes the text of the element as input and returns a Reflex component. + +```md alert +The `pre` and `a` tags are special cases. In addition to the `text`, they also receive a `props` argument containing additional props for the component. +``` + +````python demo exec +component_map = { + "h1": lambda text: rx.heading(text, size="5", margin_y="1em"), + "h2": lambda text: rx.heading(text, size="3", margin_y="1em"), + "h3": lambda text: rx.heading(text, size="1", margin_y="1em"), + "p": lambda text: rx.text(text, color="green", margin_y="1em"), + "code": lambda text: rx.code(text, color="purple"), + "pre": lambda text, **props: rx.code_block(text, **props, theme=rx.code_block.themes.dark, margin_y="1em"), + "a": lambda text, **props: rx.link(text, **props, color="blue", _hover={"color": "red"}), +} + +def index(): + return rx.box( + rx.markdown( +r""" +# Hello World! + +## This is a Subheader + +### And Another Subheader + +Here is some `code`: + +```python +import reflex as rx + +component = rx.text("Hello World!") +``` + +And then some more text here, +followed by a link to +[Reflex](https://reflex.dev/). +""", + component_map=component_map, +) + ) +```` diff --git a/docs/library/typography/quote.md b/docs/library/typography/quote.md new file mode 100644 index 00000000000..aefedc8abc5 --- /dev/null +++ b/docs/library/typography/quote.md @@ -0,0 +1,19 @@ +--- +components: + - rx.text.quote +--- + +```python exec +import reflex as rx +``` + +# Quote + +A short inline quotation. + +```python demo +rx.text("His famous quote, ", + rx.text.quote("Styles come and go. Good design is a language, not a style"), + ", elegantly sums up Massimo’s philosophy of design." + ) +``` diff --git a/docs/library/typography/strong.md b/docs/library/typography/strong.md new file mode 100644 index 00000000000..7ebfc9132df --- /dev/null +++ b/docs/library/typography/strong.md @@ -0,0 +1,16 @@ +--- +components: + - rx.text.strong +--- + +```python exec +import reflex as rx +``` + +# Strong + +Marks text to signify strong importance. + +```python demo +rx.text("The most important thing to remember is, ", rx.text.strong("stay positive"), ".") +``` diff --git a/docs/library/typography/text.md b/docs/library/typography/text.md new file mode 100644 index 00000000000..73f9e42df98 --- /dev/null +++ b/docs/library/typography/text.md @@ -0,0 +1,204 @@ +--- +components: + - rx.text + - rx.text.em +--- + +```python exec +import reflex as rx +``` + +# Text + +```python demo +rx.text("The quick brown fox jumps over the lazy dog.") +``` + +## As another element + +Use the `as_` prop to render text as a `p`, `label`, `div` or `span`. This prop is purely semantic and does not alter visual appearance. + +```python demo +rx.flex( + rx.text("This is a ", rx.text.strong("paragraph"), " element.", as_="p"), + rx.text("This is a ", rx.text.strong("label"), " element.", as_="label"), + rx.text("This is a ", rx.text.strong("div"), " element.", as_="div"), + rx.text("This is a ", rx.text.strong("span"), " element.", as_="span"), + direction="column", + spacing="3", +) +``` + +## Size + +Use the `size` prop to control text size. This prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.text("The quick brown fox jumps over the lazy dog.", size="1"), + rx.text("The quick brown fox jumps over the lazy dog.", size="2"), + rx.text("The quick brown fox jumps over the lazy dog.", size="3"), + rx.text("The quick brown fox jumps over the lazy dog.", size="4"), + rx.text("The quick brown fox jumps over the lazy dog.", size="5"), + rx.text("The quick brown fox jumps over the lazy dog.", size="6"), + rx.text("The quick brown fox jumps over the lazy dog.", size="7"), + rx.text("The quick brown fox jumps over the lazy dog.", size="8"), + rx.text("The quick brown fox jumps over the lazy dog.", size="9"), + direction="column", + spacing="3", +) +``` + +Sizes 2–4 are designed to work well for long-form content. Sizes 1–3 are designed to work well for UI labels. + +## Weight + +Use the `weight` prop to set the text weight. + +```python demo +rx.flex( + rx.text("The quick brown fox jumps over the lazy dog.", weight="light", as_="div"), + rx.text("The quick brown fox jumps over the lazy dog.", weight="regular", as_="div"), + rx.text("The quick brown fox jumps over the lazy dog.", weight="medium", as_="div"), + rx.text("The quick brown fox jumps over the lazy dog.", weight="bold", as_="div"), + direction="column", + spacing="3", +) +``` + +## Align + +Use the `align` prop to set text alignment. + +```python demo +rx.flex( + rx.text("Left-aligned", align="left", as_="div"), + rx.text("Center-aligned", align="center", as_="div"), + rx.text("Right-aligned", align="right", as_="div"), + direction="column", + spacing="3", + width="100%", +) +``` + +## Trim + +Use the `trim` prop to trim the leading space at the start, end, or both sides of the text box. + +```python demo +rx.flex( + rx.text("Without Trim", + trim="normal", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + rx.text("With Trim", + trim="both", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + direction="column", + spacing="3", +) +``` + +Trimming the leading is useful when dialing in vertical spacing in cards or other “boxy” components. Otherwise, padding looks larger on top and bottom than on the sides. + +```python demo +rx.flex( + rx.box( + rx.heading("Without trim", margin_bottom="4px", size="3",), + rx.text("The goal of typography is to relate font size, line height, and line width in a proportional way that maximizes beauty and makes reading easier and more pleasant."), + style={"background": "var(--gray-a2)", + "border": "1px dashed var(--gray-a7)",}, + padding="16px", + ), + rx.box( + rx.heading("With trim", margin_bottom="4px", size="3", trim="start"), + rx.text("The goal of typography is to relate font size, line height, and line width in a proportional way that maximizes beauty and makes reading easier and more pleasant."), + style={"background": "var(--gray-a2)", + "border": "1px dashed var(--gray-a7)",}, + padding="16px", + ), + direction="column", + spacing="3", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="indigo"), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="cyan"), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="crimson"), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="orange"), + direction="column", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="indigo", high_contrast=True), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="cyan", high_contrast=True), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="crimson", high_contrast=True), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="orange", high_contrast=True), + direction="column", +) +``` + +## With formatting + +Compose `Text` with formatting components to add emphasis and structure to content. + +```python demo +rx.text( + "Look, such a helpful ", + rx.link("link", href="#"), + ", an ", + rx.text.em("italic emphasis"), + " a piece of computer ", + rx.code("code"), + ", and even a hotkey combination ", + rx.text.kbd("⇧⌘A"), + " within the text.", + size="5", +) +``` + +## Preformmatting + +By Default, the browser renders multiple white spaces into one. To preserve whitespace, use the `white_space = "pre"` css prop. + +```python demo +rx.hstack( + rx.text("This is not pre formatted"), + rx.text("This is pre formatted", white_space="pre"), +) +``` + +## With form controls + +Composing `text` with a form control like `checkbox`, `radiogroup`, or `switch` automatically centers the control with the first line of text, even when the text is multi-line. + +```python demo +rx.box( + rx.text( + rx.flex( + rx.checkbox(default_checked=True), + "I understand that these documents are confidential and cannot be shared with a third party.", + ), + as_="label", + size="3", + ), + style={"max_width": 300}, +) +``` diff --git a/docs/pages/dynamic_routing.md b/docs/pages/dynamic_routing.md new file mode 100644 index 00000000000..14a4be85bec --- /dev/null +++ b/docs/pages/dynamic_routing.md @@ -0,0 +1,109 @@ +```python exec +import reflex as rx +``` + +# Dynamic Routes + +Dynamic routes in Reflex allow you to handle varying URL structures, enabling you to create flexible +and adaptable web applications. This section covers regular dynamic routes, catch-all routes, +and optional catch-all routes, each with detailed examples. + +## Regular Dynamic Routes + +Regular dynamic routes in Reflex allow you to match specific segments in a URL dynamically. A regular dynamic route is defined by square brackets in a route string / url pattern. For example `/users/[id]` or `/products/[category]`. These dynamic route arguments can be accessed through a state var. For the examples above they would be `rx.State.id` and `rx.State.category` respectively. + +```md alert info +# Why is the state var accessed as `rx.State.id`? + +The dynamic route arguments are accessible as `rx.State.id` and `rx.State.category` here as the var is added to the root state, so that it is accessible from any state. +``` + +Example: + +```python +@rx.page(route='/post/[pid]') +def post(): + '''A page that updates based on the route.''' + # Displays the dynamic part of the URL, the post ID + return rx.heading(rx.State.pid) + +app = rx.App() +``` + +The [pid] part in the route is a dynamic segment, meaning it can match any value provided in the URL. For instance, `/post/5`, `/post/10`, or `/post/abc` would all match this route. + +If a user navigates to `/post/5`, `State.post_id` will return `5`, and the page will display `5` as the heading. If the URL is `/post/xyz`, it will display `xyz`. If the URL is `/post/` without any additional parameter, it will display `""`. + +### Adding Dynamic Routes + +Adding dynamic routes uses the `add_page` method like any other page. The only difference is that the route string contains dynamic segments enclosed in square brackets. + +If you are using the `app.add_page` method to define pages, it is necessary to add the dynamic routes first, especially if they use the same function as a non dynamic route. + +For example the code snippet below will: + +```python +app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load) +app.add_page(index, route="/static/x", on_load=DynamicState.on_load) +app.add_page(index) +``` + +But if we switch the order of adding the pages, like in the example below, it will not work: + +```python +app.add_page(index, route="/static/x", on_load=DynamicState.on_load) +app.add_page(index) +app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load) +``` + +## Catch-All Routes + +Catch-all routes in Reflex allow you to match any number of segments in a URL dynamically. + +Example: + +```python +class State(rx.State): + @rx.var + def user_post(self) -> str: + args = self.router.page.params + usernames = args.get('splat', []) + return f"Posts by \{', '.join(usernames)}" + +@rx.page(route='/users/[id]/posts/[[...splat]]') +def post(): + return rx.center( + rx.text(State.user_post) + ) + + +app = rx.App() +``` + +In this case, the `...splat` catch-all pattern captures any number of segments after +`/users/`, allowing URLs like `/users/2/posts/john/` and `/users/1/posts/john/doe/` to match the route. + +```md alert +# Catch-all routes must be named `splat` and be placed at the end of the URL pattern to ensure proper route matching. +``` + +### Routes Validation Table + +| Route Pattern | Example URl | valid | +| :----------------------------------------------- | :---------------------------------------------- | ------: | +| `/users/posts` | `/users/posts` | valid | +| `/products/[category]` | `/products/electronics` | valid | +| `/users/[username]/posts/[id]` | `/users/john/posts/5` | valid | +| `/users/[[...splat]]/posts` | `/users/john/posts` | invalid | +| | `/users/john/doe/posts` | invalid | +| `/users/[[...splat]]` | `/users/john/` | valid | +| | `/users/john/doe` | valid | +| `/products/[category]/[[...splat]]` | `/products/electronics/laptops` | valid | +| | `/products/electronics/laptops/lenovo` | valid | +| `/products/[category]/[[...splat]]` | `/products/electronics` | valid | +| | `/products/electronics/laptops` | valid | +| | `/products/electronics/laptops/lenovo` | valid | +| | `/products/electronics/laptops/lenovo/thinkpad` | valid | +| `/products/[category]/[[...splat]]/[[...splat]]` | `/products/electronics/laptops` | invalid | +| | `/products/electronics/laptops/lenovo` | invalid | +| | `/products/electronics/laptops/lenovo/thinkpad` | invalid | diff --git a/docs/pages/overview.md b/docs/pages/overview.md new file mode 100644 index 00000000000..9d1e67d3463 --- /dev/null +++ b/docs/pages/overview.md @@ -0,0 +1,236 @@ +```python exec +import reflex as rx +``` + +# Pages + +Pages map components to different URLs in your app. This section covers creating pages, handling URL arguments, accessing query parameters, managing page metadata, and handling page load events. + +## Adding a Page + +You can create a page by defining a function that returns a component. +By default, the function name will be used as the route, but you can also specify a route. + +```python +def index(): + return rx.text('Root Page') + +def about(): + return rx.text('About Page') + + +def custom(): + return rx.text('Custom Route') + +app = rx.App() + +app.add_page(index) +app.add_page(about) +app.add_page(custom, route="/custom-route") +``` + +In this example we create three pages: + +- `index` - The root route, available at `/` +- `about` - available at `/about` +- `custom` - available at `/custom-route` + +```md alert +# Index is a special exception where it is available at both `/` and `/index`. All other pages are only available at their specified route. +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=3853&end=4083 +# Video: Pages and URL Routes +``` + +## Page Decorator + +You can also use the `@rx.page` decorator to add a page. + +```python +@rx.page(route='/', title='My Beautiful App') +def index(): + return rx.text('A Beautiful App') +``` + +This is equivalent to calling `app.add_page` with the same arguments. + +```md alert warning +# Remember to import the modules defining your decorated pages. + +This is necessary for the pages to be registered with the app. + +You can directly import the module or import another module that imports the decorated pages. +``` + +## Navigating Between Pages + +### Links + +[Links](/docs/library/typography/link) are accessible elements used primarily for navigation. Use the `href` prop to specify the location for the link to navigate to. + +```python demo +rx.link("Reflex Home Page.", href="https://reflex.dev/") +``` + +You can also provide local links to other pages in your project without writing the full url. + +```python demo +rx.link("Example", href="/docs/library") +``` + +To open the link in a new tab, set the `is_external` prop to `True`. + +```python demo +rx.link("Open in new tab", href="https://reflex.dev/", is_external=True) +``` + +Check out the [link docs](/docs/library/typography/link) to learn more. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=4083&end=4423 +# Video: Link-based Navigation +``` + +### Redirect + +Redirect the user to a new path within the application using `rx.redirect()`. + +- `path`: The destination path or URL to which the user should be redirected. +- `external`: If set to True, the redirection will open in a new tab. Defaults to `False`. + +```python demo +rx.vstack( + rx.button("open in tab", on_click=rx.redirect("/docs/api-reference/special_events")), + rx.button("open in new tab", on_click=rx.redirect('https://github.com/reflex-dev/reflex/', is_external=True)) +) +``` + +Redirect can also be run from an event handler in State, meaning logic can be added behind it. It is necessary to `return` the `rx.redirect()`. + +```python demo exec +class Redirect2ExampleState(rx.State): + redirect_to_org: bool = False + + @rx.event + def change_redirect(self): + self.redirect_to_org = not self.redirect_to_org + + @rx.var + def url(self) -> str: + return 'https://github.com/reflex-dev/' if self.redirect_to_org else 'https://github.com/reflex-dev/reflex/' + + @rx.event + def change_page(self): + return rx.redirect(self.url, is_external=True) + +def redirect_example(): + return rx.vstack( + rx.text(f"{Redirect2ExampleState.url}"), + rx.button("Change redirect location", on_click=Redirect2ExampleState.change_redirect), + rx.button("Redirect to new page in State", on_click=Redirect2ExampleState.change_page), + + ) +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=4423&end=4903 +# Video: Redirecting to a New Page +``` + +## Nested Routes + +Pages can also have nested routes. + +```python +def nested_page(): + return rx.text('Nested Page') + +app = rx.App() +app.add_page(nested_page, route='/nested/page') +``` + +This component will be available at `/nested/page`. + +## Page Metadata + +```python exec +import reflex as rx + +meta_data = ( +""" +@rx.page( + title='My Beautiful App', + description='A beautiful app built with Reflex', + image='https://web.reflex-assets.dev/other/logo.jpg', + meta=meta, +) +def index(): + return rx.text('A Beautiful App') + +@rx.page(title='About Page') +def about(): + return rx.text('About Page') + + +meta = [ + {'name': 'theme_color', 'content': '#FFFFFF'}, + {'char_set': 'UTF-8'}, + {'property': 'og:url', 'content': 'url'}, +] + +app = rx.App() +""" + +) + +``` + +You can add page metadata such as: + +- The title to be shown in the browser tab +- The description as shown in search results +- The preview image to be shown when the page is shared on social media +- Any additional metadata + +```python +{meta_data} +``` + +## Getting the Current Page + +You can access the current page from the `router` attribute in any state. See the [router docs](/docs/utility_methods/router_attributes) for all available attributes. + +```python +class State(rx.State): + def some_method(self): + current_page_route = self.router.page.path + current_page_url = self.router.page.raw_path + # ... Your logic here ... +``` + +The `router.page.path` attribute allows you to obtain the path of the current page from the router data, +for [dynamic pages](/docs/pages/dynamic_routing) this will contain the slug rather than the actual value used to load the page. + +To get the actual URL displayed in the browser, use `router.page.raw_path`. This +will contain all query parameters and dynamic path segments. + +In the above example, `current_page_route` will contain the route pattern (e.g., `/posts/[id]`), while `current_page_url` +will contain the actual URL (e.g., `/posts/123`). + +To get the full URL, access the same attributes with `full_` prefix. + +Example: + +```python +class State(rx.State): + @rx.var + def current_url(self) -> str: + return self.router.page.full_raw_path + +def index(): + return rx.text(State.current_url) + +app = rx.App() +app.add_page(index, route='/posts/[id]') +``` + +In this example, running on `localhost` should display `http://localhost:3000/posts/123/` diff --git a/docs/pe/README.md b/docs/pe/README.md deleted file mode 100644 index efe1231ab68..00000000000 --- a/docs/pe/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -### **✨ برنامه های تحت وب قابل تنظیم، کارآمد تماما پایتونی که در چند ثانیه مستقر(دپلوی) می‎شود. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/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.10+): - -```bash -pip install reflex -``` - -## 🥳 اولین برنامه خود را ایجاد کنید - -نصب `reflex` همچنین `reflex` در خط فرمان را نصب میکند. - -با ایجاد یک پروژه جدید موفقیت آمیز بودن نصب را تست کنید. (`my_app_name` را با اسم پروژه خودتان جایگزین کنید): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -این دستور یک برنامه الگو(تمپلیت) را در فهرست(دایرکتوری) جدید شما مقداردهی اولیه می کند - -می توانید این برنامه را در حالت توسعه(development) اجرا کنید: - -```bash -reflex run -``` - -باید برنامه خود را در حال اجرا ببینید در http://localhost:3000. - -اکنون می‌توانید کد منبع را در «my_app_name/my_app_name.py» تغییر دهید. Reflex دارای تازه‌سازی‌های سریعی است، بنابراین می‌توانید تغییرات خود را بلافاصله پس از ذخیره کد خود مشاهده کنید. - -## 🫧 Example App - برنامه نمونه - -بیایید یک مثال بزنیم: ایجاد یک رابط کاربری برای تولید تصویر [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). برای سادگی، ما فراخوانی میکنیم [OpenAI API](https://platform.openai.com/docs/api-reference/authentication), اما می توانید آن را با یک مدل ML که به صورت محلی اجرا می شود جایگزین کنید. - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -در اینجا کد کامل برای ایجاد این پروژه آمده است. همه اینها در یک فایل پایتون انجام می شود! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## بیاید سادش کنیم - -
-Explaining the differences between backend and frontend parts of the DALL-E app. -
- -### **Reflex UI - رابط کاربری رفلکس** - -بیایید با رابط کاربری شروع کنیم. - -```python -def index(): - return rx.center( - ... - ) -``` - -تابع `index` قسمت فرانت اند برنامه را تعریف می کند. - -ما از اجزای مختلفی مثل `center`, `vstack`, `input` و `button` استفاده میکنیم تا فرانت اند را بسازیم. اجزاء را می توان درون یکدیگر قرار داد -برای ایجاد طرح بندی های پیچیده می توانید از args کلمات کلیدی برای استایل دادن به آنها از CSS استفاده کنید. - -رفلکس دارای [بیش از 60 جزء](https://reflex.dev/docs/library) برای کمک به شما برای شروع. ما به طور فعال اجزای بیشتری را اضافه می کنیم, و این خیلی آسان است که [اجزا خود را بسازید](https://reflex.dev/docs/wrapping-react/overview/). - -### **State - حالت** - -رفلکس رابط کاربری شما را به عنوان تابعی از وضعیت شما نشان می دهد. - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -حالت تمام متغیرها(variables) (به نام vars) را در یک برنامه که می توانند تغییر دهند و توابعی که آنها را تغییر می دهند تعریف می کند.. - -در اینجا حالت از یک `prompt` و `image_url` تشکیل شده است. همچنین دو بولی `processing` و `complete` برای نشان دادن زمان غیرفعال کردن دکمه (در طول تولید تصویر) و زمان نمایش تصویر نتیجه وجود دارد. - -### **Event Handlers - کنترل کنندگان رویداد** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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 -``` - -در داخل حالت، توابعی به نام کنترل کننده رویداد تعریف می کنیم که متغیرهای حالت را تغییر می دهند. کنترل کننده های رویداد راهی هستند که می توانیم وضعیت را در Reflex تغییر دهیم. آنها را می توان در پاسخ به اقدامات کاربر، مانند کلیک کردن روی یک دکمه یا تایپ کردن در یک متن، فراخوانی کرد. به این اعمال وقایع می گویند. - -برنامه DALL·E ما دارای یک کنترل کننده رویداد، `get_image` است که این تصویر را از OpenAI API دریافت می کند. استفاده از `yield` در وسط کنترل‌کننده رویداد باعث به‌روزرسانی رابط کاربری می‌شود. در غیر این صورت رابط کاربری در پایان کنترل کننده رویداد به روز می شود. - -### **Routing - مسیریابی** - -بالاخره اپلیکیشن خود را تعریف می کنیم. - -```python -app = rx.App() -``` - -ما یک صفحه از root برنامه را به جزء index اضافه می کنیم. ما همچنین عنوانی را اضافه می کنیم که در برگه پیش نمایش/مرورگر صفحه نمایش داده می شود. - -```python -app.add_page(index, title="DALL-E") -``` - -با افزودن صفحات بیشتر می توانید یک برنامه چند صفحه ای ایجاد کنید. - -## 📑 Resources - منابع - -
- -📑 [اسناد](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [وبلاگ](https://reflex.dev/blog)   |   📱 [کتابخانه جزء](https://reflex.dev/docs/library)   |   🖼️ [قالب ها](https://reflex.dev/templates/)   |   🛸 [استقرار](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Status - وضعیت - -رفلکس(reflex) در دسامبر 2022 با نام Pynecone راه اندازی شد. - -از سال 2025، [Reflex Cloud](https://cloud.reflex.dev) برای فراهم کردن بهترین تجربه میزبانی برای برنامه های Reflex راه‌اندازی شده است. ما به توسعه آن ادامه خواهیم داد و ویژگی‌های بیشتری را پیاده‌سازی خواهیم کرد. - -رفلکس(reflex) هر هفته نسخه ها و ویژگی های جدیدی دارد! مطمئن شوید که :star: ستاره و :eyes: این مخزن را تماشا کنید تا به روز بمانید. - -## Contributing - مشارکت کردن - -ما از مشارکت در هر اندازه استقبال می کنیم! در زیر چند راه خوب برای شروع در انجمن رفلکس آورده شده است. - -- **به Discord ما بپیوندید**: [Discord](https://discord.gg/T5WSbC2YtQ) ما بهترین مکان برای دریافت کمک در مورد پروژه Reflex و بحث در مورد اینکه چگونه می توانید کمک کنید است. -- **بحث های GitHub**: راهی عالی برای صحبت در مورد ویژگی هایی که می خواهید اضافه کنید یا چیزهایی که گیج کننده هستند/نیاز به توضیح دارند. -- **قسمت مشکلات GitHub**: [قسمت مشکلات](https://github.com/reflex-dev/reflex/issues) یک راه عالی برای گزارش اشکال هستند. علاوه بر این، می توانید یک مشکل موجود را حل کنید و یک PR(pull request) ارسال کنید. - -ما فعالانه به دنبال مشارکت کنندگان هستیم، فارغ از سطح مهارت یا تجربه شما. برای مشارکت [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) را بررسی کنید. - -## All Thanks To Our Contributors - با تشکر از همکاران ما: - - - - - -## License - مجوز - -رفلکس متن باز و تحت مجوز [Apache License 2.0](/LICENSE) است. diff --git a/docs/pt/pt_br/README.md b/docs/pt/pt_br/README.md deleted file mode 100644 index 1c970df9884..00000000000 --- a/docs/pt/pt_br/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -### **✨ Web apps customizáveis, performáticos, em Python puro. Faça deploy em segundos. ✨** - -[![Versão PyPI](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versões](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentação](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex é uma biblioteca para construir aplicações web full-stack em Python puro. - -Principais características: - -- **Python Puro** - Escreva o frontend e o backend da sua aplicação inteiramente em Python, sem necessidade de aprender Javascript. -- **Flexibilidade Total** - O Reflex é fácil de começar a usar, mas também pode escalar para aplicações complexas. -- **Deploy Instantâneo** - Após a construção, faça o deploy da sua aplicação com um [único comando](https://reflex.dev/docs/hosting/deploy-quick-start/) ou hospede-a em seu próprio servidor. - -Veja nossa [página de arquitetura](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) para aprender como o Reflex funciona internamente. - -## ⚙️ Instalação - -Abra um terminal e execute (Requer Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 Crie o seu primeiro app - -Instalar `reflex` também instala a ferramenta de linha de comando `reflex`. - -Crie um novo projeto para verificar se a instalação foi bem sucedida. (Mude `nome_do_meu_app` com o nome do seu projeto): - -```bash -mkdir nome_do_meu_app -cd nome_do_meu_app -reflex init -``` - -Este comando inicializa um app base no seu novo diretório. - -Você pode executar este app em modo desenvolvimento: - -```bash -reflex run -``` - -Você deve conseguir verificar seu app sendo executado em http://localhost:3000. - -Agora, você pode modificar o código fonte em `nome_do_meu_app/nome_do_meu_app.py`. O Reflex apresenta recarregamento rápido para que você possa ver suas mudanças instantaneamente quando você salva o seu código. - -## 🫧 Exemplo de App - -Veja o seguinte exemplo: criar uma interface de criação de imagens por meio do [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Para fins de simplicidade, vamos apenas chamar a [API da OpenAI](https://platform.openai.com/docs/api-reference/authentication), mas você pode substituir esta solução por um modelo de ML executado localmente. - -  - -
-Um encapsulador frontend para o DALL-E, mostrado no processo de criação de uma imagem. -
- -  - -Aqui está o código completo para criar este projeto. Isso tudo foi feito apenas em um arquivo Python! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """Estado da aplicação.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Obtenção da imagem a partir do prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Adição do estado e da página no app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Vamos por partes. - -
-Explicando as diferenças entre as partes de backend e frontend do app DALL-E. -
- -### **Reflex UI** - -Vamos começar com a UI (Interface de Usuário) - -```python -def index(): - return rx.center( - ... - ) -``` - -Esta função `index` define o frontend do app. - -Usamos diferentes componentes, como `center`, `vstack`, `input` e `button`, para construir o frontend. Componentes podem ser aninhados um no do outro -para criar layouts mais complexos. E você pode usar argumentos de chave-valor para estilizá-los com todo o poder do CSS. - -O Reflex vem com [60+ componentes nativos](https://reflex.dev/docs/library) para te ajudar a começar. Estamos adicionando ativamente mais componentes, e é fácil [criar seus próprios componentes](https://reflex.dev/docs/wrapping-react/overview/). - -### **Estado** - -O Reflex representa a sua UI como uma função do seu estado. - -```python -class State(rx.State): - """Estado da aplicação.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -O estado define todas as variáveis (chamadas de vars) em um app que podem mudar e as funções que as alteram. - -Aqui, o estado é composto por um `prompt` e uma `image_url`. Há também os booleanos `processing` e `complete` para indicar quando desabilitar o botão (durante a geração da imagem) e quando mostrar a imagem resultante. - -### **Handlers de Eventos** - -```python -def get_image(self): - """Obtenção da imagem a partir do prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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 do estado, são definidas funções chamadas de Handlers de Eventos, que podem mudar as variáveis do estado. Handlers de Eventos são a forma com a qual podemos modificar o estado dentro do Reflex. Eles podem ser chamados como resposta a uma ação do usuário, como o clique de um botão ou a escrita em uma caixa de texto. Estas ações são chamadas de eventos. - -Nosso app DALL-E possui um Handler de Evento chamado `get_image`, que obtêm a imagem da API da OpenAI. Usar `yield` no meio de um Handler de Evento causa a atualização da UI do seu app. Caso contrário, a UI seria atualizada no fim da execução de um Handler de Evento. - -### **Rotas** - -Finalmente, definimos nosso app. - -```python -app = rx.App() -``` - -Adicionamos uma página na raíz do app, apontando para o componente index. Também adicionamos um título que irá aparecer na visualização da página/aba do navegador. - -```python -app.add_page(index, title="DALL-E") -``` - -Você pode criar um app com múltiplas páginas adicionando mais páginas. - -## 📑 Recursos - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Biblioteca de Componentes](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Status - -O Reflex foi lançado em Dezembro de 2022 com o nome Pynecone. - -A partir de 2025, o [Reflex Cloud](https://cloud.reflex.dev) foi lançado para fornecer a melhor experiência de hospedagem para apps Reflex. Continuaremos a desenvolvê-lo e implementar mais recursos. - -O Reflex tem novas versões e recursos chegando toda semana! Certifique-se de marcar com :star: estrela e :eyes: observar este repositório para se manter atualizado. - -## Contribuições - -Nós somos abertos a contribuições de qualquer tamanho! Abaixo, seguem algumas boas formas de começar a contribuir para a comunidade do Reflex. - -- **Entre no nosso Discord**: Nosso [Discord](https://discord.gg/T5WSbC2YtQ) é o melhor lugar para conseguir ajuda no seu projeto Reflex e para discutir como você pode contribuir. -- **Discussões no GitHub**: Uma boa forma de conversar sobre funcionalidades que você gostaria de ver ou coisas que ainda estão confusas/exigem ajuda. -- **Issues no GitHub**: [Issues](https://github.com/reflex-dev/reflex/issues) são uma excelente forma de reportar bugs. Além disso, você pode tentar resolver alguma issue existente e enviar um PR. - -Estamos ativamente buscando novos contribuidores, não importa o seu nível de habilidade ou experiência. Para contribuir, confira [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md). - -## Todo Agradecimento aos Nossos Contribuidores: - - - - - -## Licença - -O Reflex é de código aberto e licenciado sob a [Apache License 2.0](/LICENSE). diff --git a/docs/recipes/auth/login_form.md b/docs/recipes/auth/login_form.md new file mode 100644 index 00000000000..ef410f43081 --- /dev/null +++ b/docs/recipes/auth/login_form.md @@ -0,0 +1,242 @@ +```python exec +import reflex as rx +``` + +# Login Form + +The login form is a common component in web applications. It allows users to authenticate themselves and access their accounts. This recipe provides examples of login forms with different elements, such as third-party authentication providers. + +## Default + +```python demo exec toggle +def login_default() -> rx.Component: + return rx.card( + rx.vstack( + rx.center( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Sign in to your account", size="6", as_="h2", text_align="center", width="100%"), + direction="column", + spacing="5", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.hstack( + rx.text("Password", size="3", weight="medium"), + rx.link("Forgot password?", href="#", size="3"), + justify="between", + width="100%" + ), + rx.input(placeholder="Enter your password", type="password", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.button("Sign in", size="3", width="100%"), + rx.center( + rx.text("New here?", size="3"), + rx.link("Sign up", href="#", size="3"), + opacity="0.8", + spacing="2", + direction="row" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` + +## Icons + +```python demo exec toggle +def login_default_icons() -> rx.Component: + return rx.card( + rx.vstack( + rx.center( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Sign in to your account", size="6", as_="h2", text_align="center", width="100%"), + direction="column", + spacing="5", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.vstack( + rx.hstack( + rx.text("Password", size="3", weight="medium"), + rx.link("Forgot password?", href="#", size="3"), + justify="between", + width="100%" + ), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.button("Sign in", size="3", width="100%"), + rx.center( + rx.text("New here?", size="3"), + rx.link("Sign up", href="#", size="3"), + opacity="0.8", + spacing="2", + direction="row", + width="100%" + ), + spacing="6", + width="100%" + ), + max_width="28em", + size="4", + width="100%" + ) +``` + +## Third-party auth + +```python demo exec toggle +def login_single_thirdparty() -> rx.Component: + return rx.card( + rx.vstack( + rx.flex( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Sign in to your account", size="6", as_="h2", text_align="left", width="100%"), + rx.hstack( + rx.text("New here?", size="3", text_align="left"), + rx.link("Sign up", href="#", size="3"), + spacing="2", + opacity="0.8", + width="100%" + ), + direction="column", + justify="start", + spacing="4", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.hstack( + rx.text("Password", size="3", weight="medium"), + rx.link("Forgot password?", href="#", size="3"), + justify="between", + width="100%" + ), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.button("Sign in", size="3", width="100%"), + rx.hstack( + rx.divider(margin="0"), + rx.text("Or continue with", white_space="nowrap", weight="medium"), + rx.divider(margin="0"), + align="center", + width="100%" + ), + rx.button( + rx.icon(tag="github"), + "Sign in with Github", + variant="outline", + size="3", + width="100%" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` + +## Multiple third-party auth + +```python demo exec toggle +def login_multiple_thirdparty() -> rx.Component: + return rx.card( + rx.vstack( + rx.flex( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Sign in to your account", size="6", as_="h2", width="100%"), + rx.hstack( + rx.text("New here?", size="3", text_align="left"), + rx.link("Sign up", href="#", size="3"), + spacing="2", + opacity="0.8", + width="100%" + ), + justify="start", + direction="column", + spacing="4", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + spacing="2", + justify="start", + width="100%" + ), + rx.vstack( + rx.hstack( + rx.text("Password", size="3", weight="medium"), + rx.link("Forgot password?", href="#", size="3"), + justify="between", + width="100%" + ), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.button("Sign in", size="3", width="100%"), + rx.hstack( + rx.divider(margin="0"), + rx.text("Or continue with", white_space="nowrap", weight="medium"), + rx.divider(margin="0"), + align="center", + width="100%" + ), + rx.center( + rx.icon_button( + rx.icon(tag="github"), + variant="soft", + size="3" + ), + rx.icon_button( + rx.icon(tag="facebook"), + variant="soft", + size="3" + ), + rx.icon_button( + rx.icon(tag="twitter"), + variant="soft", + size="3" + ), + spacing="4", + direction="row", + width="100%" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` diff --git a/docs/recipes/auth/signup_form.md b/docs/recipes/auth/signup_form.md new file mode 100644 index 00000000000..24ba4719988 --- /dev/null +++ b/docs/recipes/auth/signup_form.md @@ -0,0 +1,259 @@ +```python exec +import reflex as rx +``` + +# Sign up Form + +The sign up form is a common component in web applications. It allows users to create an account and access the application's features. This page provides a few examples of sign up forms that you can use in your application. + +## Default + +```python demo exec toggle +def signup_default() -> rx.Component: + return rx.card( + rx.vstack( + rx.center( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Create an account", size="6", as_="h2", text_align="center", width="100%"), + direction="column", + spacing="5", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.text("Password", size="3", weight="medium", text_align="left", width="100%"), + rx.input(placeholder="Enter your password", type="password", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.box( + rx.checkbox( + "Agree to Terms and Conditions", + default_checked=True, + spacing="2" + ), + width="100%" + ), + rx.button("Register", size="3", width="100%"), + rx.center( + rx.text("Already registered?", size="3"), + rx.link("Sign in", href="#", size="3"), + opacity="0.8", + spacing="2", + direction="row" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` + +## Icons + +```python demo exec toggle +def signup_default_icons() -> rx.Component: + return rx.card( + rx.vstack( + rx.center( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Create an account", size="6", as_="h2", text_align="center", width="100%"), + direction="column", + spacing="5", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.text("Password", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.box( + rx.checkbox( + "Agree to Terms and Conditions", + default_checked=True, + spacing="2" + ), + width="100%" + ), + rx.button("Register", size="3", width="100%"), + rx.center( + rx.text("Already registered?", size="3"), + rx.link("Sign in", href="#", size="3"), + opacity="0.8", + spacing="2", + direction="row", + width="100%" + ), + spacing="6", + width="100%" + ), + max_width="28em", + size="4", + width="100%" + ) +``` + +## Third-party auth + +```python demo exec toggle +def signup_single_thirdparty() -> rx.Component: + return rx.card( + rx.vstack( + rx.flex( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Create an account", size="6", as_="h2", text_align="left", width="100%"), + rx.hstack( + rx.text("Already registered?", size="3", text_align="left"), + rx.link("Sign in", href="#", size="3"), + spacing="2", + opacity="0.8", + width="100%" + ), + direction="column", + justify="start", + spacing="4", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.text("Password", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.box( + rx.checkbox( + "Agree to Terms and Conditions", + default_checked=True, + spacing="2" + ), + width="100%" + ), + rx.button("Register", size="3", width="100%"), + rx.hstack( + rx.divider(margin="0"), + rx.text("Or continue with", white_space="nowrap", weight="medium"), + rx.divider(margin="0"), + align="center", + width="100%" + ), + rx.button( + rx.icon(tag="github"), + "Sign in with Github", + variant="outline", + size="3", + width="100%" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` + +## Multiple third-party auth + +```python demo exec toggle +def signup_multiple_thirdparty() -> rx.Component: + return rx.card( + rx.vstack( + rx.flex( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Create an account", size="6", as_="h2", width="100%"), + rx.hstack( + rx.text("Already registered?", size="3", text_align="left"), + rx.link("Sign in", href="#", size="3"), + spacing="2", + opacity="0.8", + width="100%" + ), + justify="start", + direction="column", + spacing="4", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.text("Password", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.box( + rx.checkbox( + "Agree to Terms and Conditions", + default_checked=True, + spacing="2" + ), + width="100%" + ), + rx.button("Register", size="3", width="100%"), + rx.hstack( + rx.divider(margin="0"), + rx.text("Or continue with", white_space="nowrap", weight="medium"), + rx.divider(margin="0"), + align="center", + width="100%" + ), + rx.center( + rx.icon_button( + rx.icon(tag="github"), + variant="soft", + size="3" + ), + rx.icon_button( + rx.icon(tag="facebook"), + variant="soft", + size="3" + ), + rx.icon_button( + rx.icon(tag="twitter"), + variant="soft", + size="3" + ), + spacing="4", + direction="row", + width="100%" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` diff --git a/docs/recipes/content/forms.md b/docs/recipes/content/forms.md new file mode 100644 index 00000000000..e1c1b9292df --- /dev/null +++ b/docs/recipes/content/forms.md @@ -0,0 +1,172 @@ +```python exec +import reflex as rx +``` + +## Forms + +Forms are a common way to gather information from users. Below are some examples. + +For more details, see the [form docs page](/docs/library/forms/form). + +## Event creation + +```python demo exec toggle +def form_field(label: str, placeholder: str, type: str, name: str) -> rx.Component: + return rx.form.field( + rx.flex( + rx.form.label(label), + rx.form.control( + rx.input( + placeholder=placeholder, + type=type + ), + as_child=True, + ), + direction="column", + spacing="1", + ), + name=name, + width="100%" + ) + +def event_form() -> rx.Component: + return rx.card( + rx.flex( + rx.hstack( + rx.badge( + rx.icon(tag="calendar-plus", size=32), + color_scheme="mint", + radius="full", + padding="0.65rem" + ), + rx.vstack( + rx.heading("Create an event", size="4", weight="bold"), + rx.text("Fill the form to create a custom event", size="2"), + spacing="1", + height="100%", + align_items="start" + ), + height="100%", + spacing="4", + align_items="center", + width="100%", + ), + rx.form.root( + rx.flex( + form_field("Event Name", "Event Name", + "text", "event_name"), + rx.flex( + form_field("Date", "", "date", "event_date"), + form_field("Time", "", "time", "event_time"), + spacing="3", + flex_direction="row", + ), + form_field("Description", "Optional", "text", "description"), + direction="column", + spacing="2", + ), + rx.form.submit( + rx.button("Create"), + as_child=True, + width="100%", + ), + on_submit=lambda form_data: rx.window_alert(form_data.to_string()), + reset_on_submit=False, + ), + width="100%", + direction="column", + spacing="4", + ), + size="3", + ) +``` + +## Contact + +```python demo exec toggle +def form_field(label: str, placeholder: str, type: str, name: str) -> rx.Component: + return rx.form.field( + rx.flex( + rx.form.label(label), + rx.form.control( + rx.input( + placeholder=placeholder, + type=type + ), + as_child=True, + ), + direction="column", + spacing="1", + ), + name=name, + width="100%" + ) + +def contact_form() -> rx.Component: + return rx.card( + rx.flex( + rx.hstack( + rx.badge( + rx.icon(tag="mail-plus", size=32), + color_scheme="blue", + radius="full", + padding="0.65rem" + ), + rx.vstack( + rx.heading("Send us a message", size="4", weight="bold"), + rx.text("Fill the form to contact us", size="2"), + spacing="1", + height="100%", + ), + height="100%", + spacing="4", + align_items="center", + width="100%", + ), + rx.form.root( + rx.flex( + rx.flex( + form_field("First Name", "First Name", + "text", "first_name"), + form_field("Last Name", "Last Name", + "text", "last_name"), + spacing="3", + flex_direction=["column", "row", "row"], + ), + rx.flex( + form_field("Email", "user@reflex.dev", + "email", "email"), + form_field("Phone", "Phone", "tel", "phone"), + spacing="3", + flex_direction=["column", "row", "row"], + ), + rx.flex( + rx.text("Message", style={ + "font-size": "15px", "font-weight": "500", "line-height": "35px"}), + rx.text_area( + placeholder="Message", + name="message", + resize="vertical", + ), + direction="column", + spacing="1", + ), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + width="100%", + ), + on_submit=lambda form_data: rx.window_alert( + form_data.to_string()), + reset_on_submit=False, + ), + width="100%", + direction="column", + spacing="4", + ), + size="3", + ) +``` diff --git a/docs/recipes/content/grid.md b/docs/recipes/content/grid.md new file mode 100644 index 00000000000..5495098d79f --- /dev/null +++ b/docs/recipes/content/grid.md @@ -0,0 +1,50 @@ +```python exec +import reflex as rx +``` + +# Grid + +A simple responsive grid layout. We specify the number of columns to the `grid_template_columns` property as a list. The grid will automatically adjust the number of columns based on the screen size. + +For details, see the [responsive docs page](/docs/styling/responsive). + +## Cards + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card(f"Card {i + 1}", height="10vh"), + ), + gap="1rem", + grid_template_columns=["1fr", "repeat(2, 1fr)", "repeat(2, 1fr)", "repeat(3, 1fr)", "repeat(4, 1fr)"], + width="100%" +) +``` + +## Inset cards + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card( + rx.inset( + rx.image( + src="https://web.reflex-assets.dev/other/reflex_banner.png", + width="100%", + height="auto", + ), + side="top", + pb="current" + ), + rx.text( + f"Card {i + 1}", + ), + ), + ), + gap="1rem", + grid_template_columns=["1fr", "repeat(2, 1fr)", "repeat(2, 1fr)", "repeat(3, 1fr)", "repeat(4, 1fr)"], + width="100%" +) +``` diff --git a/docs/recipes/content/multi_column_row.md b/docs/recipes/content/multi_column_row.md new file mode 100644 index 00000000000..97db67cf5cc --- /dev/null +++ b/docs/recipes/content/multi_column_row.md @@ -0,0 +1,63 @@ +```python exec +import reflex as rx +``` + +# Multi-column and row layout + +A simple responsive multi-column and row layout. We specify the number of columns/rows to the `flex_direction` property as a list. The layout will automatically adjust the number of columns/rows based on the screen size. + +For details, see the [responsive docs page](/docs/styling/responsive). + +## Column + +```python demo +rx.flex( + rx.box(bg=rx.color("accent", 3), width="100%", height="100%"), + rx.box(bg=rx.color("accent", 5), width="100%", height="100%"), + rx.box(bg=rx.color("accent", 7), width="100%", height="100%"), + bg=rx.color("accent", 10), + spacing="4", + padding="1em", + flex_direction=["column", "column", "row"], + height="600px", + width="100%", +) +``` + +```python demo +rx.flex( + rx.box(bg=rx.color("accent", 3), width="100%", height="100%"), + rx.box(bg=rx.color("accent", 5), width=["100%", "100%", "50%"], height=["50%", "50%", "100%"]), + rx.box(bg=rx.color("accent", 7), width="100%", height="100%"), + rx.box(bg=rx.color("accent", 9), width=["100%", "100%", "50%"], height=["50%", "50%", "100%"]), + bg=rx.color("accent", 10), + spacing="4", + padding="1em", + flex_direction=["column", "column", "row"], + height="600px", + width="100%", +) +``` + +## Row + +```python demo +rx.flex( + rx.flex( + rx.box(bg=rx.color("accent", 3), width=["100%", "100%", "50%"], height="100%"), + rx.box(bg=rx.color("accent", 5), width=["100%", "100%", "50%"], height="100%"), + width="100%", + height="100%", + spacing="4", + flex_direction=["column", "column", "row"], + ), + rx.box(bg=rx.color("accent", 7), width="100%", height="50%"), + rx.box(bg=rx.color("accent", 9), width="100%", height="50%"), + bg=rx.color("accent", 10), + spacing="4", + padding="1em", + flex_direction="column", + height="600px", + width="100%", +) +``` diff --git a/docs/recipes/content/stats.md b/docs/recipes/content/stats.md new file mode 100644 index 00000000000..b4d038835fa --- /dev/null +++ b/docs/recipes/content/stats.md @@ -0,0 +1,107 @@ +```python exec +import reflex as rx +``` + +# Stats + +Stats cards are used to display key metrics or data points. They are typically used in dashboards or admin panels. + +## Variant 1 + +```python demo exec toggle +from reflex.components.radix.themes.base import LiteralAccentColor + +def stats(stat_name: str = "Users", value: int = 4200, prev_value: int = 3000, icon: str = "users", badge_color: LiteralAccentColor = "blue") -> rx.Component: + percentage_change = round(((value - prev_value) / prev_value) * 100, 2) if prev_value != 0 else 0 if value == 0 else float('inf') + change = "increase" if value > prev_value else "decrease" + arrow_icon = "trending-up" if value > prev_value else "trending-down" + arrow_color = "grass" if value > prev_value else "tomato" + return rx.card( + rx.vstack( + rx.hstack( + rx.badge( + rx.icon(tag=icon, size=34), + color_scheme=badge_color, + radius="full", + padding="0.7rem" + ), + rx.vstack( + rx.heading(f"{value:,}", size="6", weight="bold"), + rx.text(stat_name, size="4", weight="medium"), + spacing="1", + height="100%", + align_items="start", + width="100%" + ), + height="100%", + spacing="4", + align="center", + width="100%", + ), + rx.hstack( + rx.hstack( + rx.icon(tag=arrow_icon, size=24, color=rx.color(arrow_color, 9)), + rx.text(f"{percentage_change}%", size="3", color=rx.color(arrow_color, 9), weight="medium"), + spacing="2", + align="center", + ), + rx.text(f"{change} from last month", size="2", color=rx.color("gray", 10)), + align="center", + width="100%", + ), + spacing="3", + ), + size="3", + width="100%", + max_width="21rem" + ) +``` + +## Variant 2 + +```python demo exec toggle +from reflex.components.radix.themes.base import LiteralAccentColor + +def stats_2(stat_name: str = "Orders", value: int = 6500, prev_value: int = 12000, icon: str = "shopping-cart", icon_color: LiteralAccentColor = "pink") -> rx.Component: + percentage_change = round(((value - prev_value) / prev_value) * 100, 2) if prev_value != 0 else 0 if value == 0 else float('inf') + arrow_icon = "trending-up" if value > prev_value else "trending-down" + arrow_color = "grass" if value > prev_value else "tomato" + return rx.card( + rx.hstack( + rx.vstack( + rx.hstack( + rx.hstack( + rx.icon(tag=icon, size=22, color=rx.color(icon_color, 11)), + rx.text(stat_name, size="4", weight="medium", color=rx.color("gray", 11)), + spacing="2", + align="center", + ), + rx.badge( + rx.icon(tag=arrow_icon, color=rx.color(arrow_color, 9)), + rx.text(f"{percentage_change}%", size="2", color=rx.color(arrow_color, 9), weight="medium"), + color_scheme=arrow_color, + radius="large", + align_items="center", + ), + justify="between", + width="100%", + ), + rx.hstack( + rx.heading(f"{value:,}", size="7", weight="bold"), + rx.text(f"from {prev_value:,}", size="3", color=rx.color("gray", 10)), + spacing="2", + align_items="end", + ), + align_items="start", + justify="between", + width="100%", + ), + align_items="start", + width="100%", + justify="between", + ), + size="3", + width="100%", + max_width="21rem", + ) +``` diff --git a/docs/recipes/content/top_banner.md b/docs/recipes/content/top_banner.md new file mode 100644 index 00000000000..66f5889f023 --- /dev/null +++ b/docs/recipes/content/top_banner.md @@ -0,0 +1,270 @@ +```python exec +import reflex as rx +``` + +# Top Banner + +Top banners are used to highlight important information or features at the top of a page. They are typically designed to grab the user's attention and can be used for announcements, navigation, or key messages. + +## Basic + +```python demo exec toggle +class TopBannerBasic(rx.ComponentState): + hide: bool = False + + @rx.event + def toggle(self): + self.hide = not self.hide + + @classmethod + def get_component(cls, **props): + return rx.cond( + ~cls.hide, + rx.hstack( + rx.flex( + rx.badge( + rx.icon("megaphone", size=18), + padding="0.30rem", + radius="full", + ), + rx.text( + "ReflexCon 2024 - ", + rx.link( + "Join us at the event!", + href="#", + underline="always", + display="inline", + underline_offset="2px", + ), + weight="medium", + ), + align="center", + margin="auto", + spacing="3", + ), + rx.icon( + "x", + cursor="pointer", + justify="end", + flex_shrink=0, + on_click=cls.toggle, + ), + wrap="nowrap", + # position="fixed", + justify="between", + width="100%", + # top="0", + align="center", + left="0", + # z_index="50", + padding="1rem", + background=rx.color("accent", 4), + **props, + ), + # Remove this in production + rx.icon_button( + rx.icon("eye"), + cursor="pointer", + on_click=cls.toggle, + ), + ) + +top_banner_basic = TopBannerBasic.create +``` + +## Sign up + +```python demo exec toggle +class TopBannerSignup(rx.ComponentState): + hide: bool = False + + @rx.event + def toggle(self): + self.hide = not self.hide + + @classmethod + def get_component(cls, **props): + return rx.cond( + ~cls.hide, + rx.flex( + rx.image( + src="https://web.reflex-assets.dev/other/logo.jpg", + width="2em", + height="auto", + border_radius="25%", + ), + rx.text( + "Web apps in pure Python. Deploy with a single command.", + weight="medium", + ), + rx.flex( + rx.button( + "Sign up", + cursor="pointer", + radius="large", + ), + rx.icon( + "x", + cursor="pointer", + justify="end", + flex_shrink=0, + on_click=cls.toggle, + ), + spacing="4", + align="center", + ), + wrap="nowrap", + # position="fixed", + flex_direction=["column", "column", "row"], + justify_content=["start", "space-between"], + width="100%", + # top="0", + spacing="2", + align_items=["start", "start", "center"], + left="0", + # z_index="50", + padding="1rem", + background=rx.color("accent", 4), + **props, + ), + # Remove this in production + rx.icon_button( + rx.icon("eye"), + cursor="pointer", + on_click=cls.toggle, + ), + ) + +top_banner_signup = TopBannerSignup.create +``` + +## Gradient + +```python demo exec toggle +class TopBannerGradient(rx.ComponentState): + hide: bool = False + + @rx.event + def toggle(self): + self.hide = not self.hide + + @classmethod + def get_component(cls, **props): + return rx.cond( + ~cls.hide, + rx.flex( + rx.text( + "The new Reflex version is now available! ", + rx.link( + "Read the release notes", + href="#", + underline="always", + display="inline", + underline_offset="2px", + ), + align_items=["start", "center"], + margin="auto", + spacing="3", + weight="medium", + ), + rx.icon( + "x", + cursor="pointer", + justify="end", + flex_shrink=0, + on_click=cls.toggle, + ), + wrap="nowrap", + # position="fixed", + justify="between", + width="100%", + # top="0", + align="center", + left="0", + # z_index="50", + padding="1rem", + background=f"linear-gradient(99deg, {rx.color('blue', 4)}, {rx.color('pink', 3)}, {rx.color('mauve', 3)})", + **props, + ), + # Remove this in production + rx.icon_button( + rx.icon("eye"), + cursor="pointer", + on_click=cls.toggle, + ), + ) + +top_banner_gradient = TopBannerGradient.create +``` + +## Newsletter + +```python demo exec toggle +class TopBannerNewsletter(rx.ComponentState): + hide: bool = False + + @rx.event + def toggle(self): + self.hide = not self.hide + + @classmethod + def get_component(cls, **props): + return rx.cond( + ~cls.hide, + rx.flex( + rx.text( + "Join our newsletter", + text_wrap="nowrap", + weight="medium", + ), + rx.input( + rx.input.slot(rx.icon("mail")), + rx.input.slot( + rx.icon_button( + rx.icon( + "arrow-right", + padding="0.15em", + ), + cursor="pointer", + radius="large", + size="2", + justify="end", + ), + padding_right="0", + ), + placeholder="Your email address", + type="email", + size="2", + radius="large", + ), + rx.icon( + "x", + cursor="pointer", + justify="end", + flex_shrink=0, + on_click=cls.toggle, + ), + wrap="nowrap", + # position="fixed", + flex_direction=["column", "row", "row"], + justify_content=["start", "space-between"], + width="100%", + # top="0", + spacing="2", + align_items=["start", "center", "center"], + left="0", + # z_index="50", + padding="1rem", + background=rx.color("accent", 4), + **props, + ), + # Remove this in production + rx.icon_button( + rx.icon("eye"), + cursor="pointer", + on_click=cls.toggle, + ), + ) + +top_banner_newsletter = TopBannerNewsletter.create +``` diff --git a/docs/recipes/layout/footer.md b/docs/recipes/layout/footer.md new file mode 100644 index 00000000000..6e4872f820b --- /dev/null +++ b/docs/recipes/layout/footer.md @@ -0,0 +1,280 @@ +```python exec +import reflex as rx +``` + +# Footer Bar + +A footer bar is a common UI element located at the bottom of a webpage. It typically contains information about the website, such as contact details and links to other pages or sections of the site. + +## Basic + +```python demo exec toggle +def footer_item(text: str, href: str) -> rx.Component: + return rx.link(rx.text(text, size="3"), href=href) + +def footer_items_1() -> rx.Component: + return rx.flex( + rx.heading("PRODUCTS", size="4", weight="bold", as_="h3"), + footer_item("Web Design", "/#"), + footer_item("Web Development", "/#"), + footer_item("E-commerce", "/#"), + footer_item("Content Management", "/#"), + footer_item("Mobile Apps", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def footer_items_2() -> rx.Component: + return rx.flex( + rx.heading("RESOURCES", size="4", weight="bold", as_="h3"), + footer_item("Blog", "/#"), + footer_item("Case Studies", "/#"), + footer_item("Whitepapers", "/#"), + footer_item("Webinars", "/#"), + footer_item("E-books", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def social_link(icon: str, href: str) -> rx.Component: + return rx.link(rx.icon(icon), href=href) + +def socials() -> rx.Component: + return rx.flex( + social_link("instagram", "/#"), + social_link("twitter", "/#"), + social_link("facebook", "/#"), + social_link("linkedin", "/#"), + spacing="3", + justify="end", + width="100%" + ) + +def footer() -> rx.Component: + return rx.el.footer( + rx.vstack( + rx.flex( + rx.vstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), + align_items="center" + ), + rx.text("© 2024 Reflex, Inc", size="3", white_space="nowrap", weight="medium"), + spacing="4", + align_items=["center", "center", "start"] + ), + footer_items_1(), + footer_items_2(), + justify="between", + spacing="6", + flex_direction=["column", "column", "row"], + width="100%" + ), + rx.divider(), + rx.hstack( + rx.hstack( + footer_item("Privacy Policy", "/#"), + footer_item("Terms of Service", "/#"), + spacing="4", + align="center", + width="100%" + ), + socials(), + justify="between", + width="100%" + ), + spacing="5", + width="100%" + ), + width="100%" + ) +``` + +## Newsletter form + +```python demo exec toggle +def footer_item(text: str, href: str) -> rx.Component: + return rx.link(rx.text(text, size="3"), href=href) + +def footer_items_1() -> rx.Component: + return rx.flex( + rx.heading("PRODUCTS", size="4", weight="bold", as_="h3"), + footer_item("Web Design", "/#"), + footer_item("Web Development", "/#"), + footer_item("E-commerce", "/#"), + footer_item("Content Management", "/#"), + footer_item("Mobile Apps", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def footer_items_2() -> rx.Component: + return rx.flex( + rx.heading("RESOURCES", size="4", weight="bold", as_="h3"), + footer_item("Blog", "/#"), + footer_item("Case Studies", "/#"), + footer_item("Whitepapers", "/#"), + footer_item("Webinars", "/#"), + footer_item("E-books", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def social_link(icon: str, href: str) -> rx.Component: + return rx.link(rx.icon(icon), href=href) + +def socials() -> rx.Component: + return rx.flex( + social_link("instagram", "/#"), + social_link("twitter", "/#"), + social_link("facebook", "/#"), + social_link("linkedin", "/#"), + spacing="3", + justify_content=["center", "center", "end"], + width="100%" + ) + +def footer_newsletter() -> rx.Component: + return rx.el.footer( + rx.vstack( + rx.flex( + footer_items_1(), + footer_items_2(), + rx.vstack( + rx.text("JOIN OUR NEWSLETTER", size="4", + weight="bold"), + rx.hstack( + rx.input(placeholder="Your email address", type="email", size="3"), + rx.icon_button(rx.icon("arrow-right", padding="0.15em"), size="3"), + spacing="1", + justify="center", + width="100%" + ), + align_items=["center", "center", "start"], + justify="center", + height="100%" + ), + justify="between", + spacing="6", + flex_direction=["column", "column", "row"], + width="100%" + ), + rx.divider(), + rx.flex( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.text("© 2024 Reflex, Inc", size="3", white_space="nowrap", weight="medium"), + spacing="2", + align="center", + justify_content=["center", "center", "start"], + width="100%" + ), + socials(), + spacing="4", + flex_direction=["column", "column", "row"], + width="100%" + ), + spacing="5", + width="100%" + ), + width="100%" + ) +``` + +## Three columns + +```python demo exec toggle +def footer_item(text: str, href: str) -> rx.Component: + return rx.link(rx.text(text, size="3"), href=href) + +def footer_items_1() -> rx.Component: + return rx.flex( + rx.heading("PRODUCTS", size="4", weight="bold", as_="h3"), + footer_item("Web Design", "/#"), + footer_item("Web Development", "/#"), + footer_item("E-commerce", "/#"), + footer_item("Content Management", "/#"), + footer_item("Mobile Apps", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def footer_items_2() -> rx.Component: + return rx.flex( + rx.heading("RESOURCES", size="4", weight="bold", as_="h3"), + footer_item("Blog", "/#"), + footer_item("Case Studies", "/#"), + footer_item("Whitepapers", "/#"), + footer_item("Webinars", "/#"), + footer_item("E-books", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def footer_items_3() -> rx.Component: + return rx.flex( + rx.heading("ABOUT US", size="4", weight="bold", as_="h3"), + footer_item("Our Team", "/#"), + footer_item("Careers", "/#"), + footer_item("Contact Us", "/#"), + footer_item("Privacy Policy", "/#"), + footer_item("Terms of Service", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def social_link(icon: str, href: str) -> rx.Component: + return rx.link(rx.icon(icon), href=href) + +def socials() -> rx.Component: + return rx.flex( + social_link("instagram", "/#"), + social_link("twitter", "/#"), + social_link("facebook", "/#"), + social_link("linkedin", "/#"), + spacing="3", + justify_content=["center", "center", "end"], + width="100%" + ) + +def footer_three_columns() -> rx.Component: + return rx.el.footer( + rx.vstack( + rx.flex( + footer_items_1(), + footer_items_2(), + footer_items_3(), + justify="between", + spacing="6", + flex_direction=["column", "column", "row"], + width="100%" + ), + rx.divider(), + rx.flex( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.text("© 2024 Reflex, Inc", size="3", white_space="nowrap", weight="medium"), + spacing="2", + align="center", + justify_content=["center", "center", "start"], + width="100%" + ), + socials(), + spacing="4", + flex_direction=["column", "column", "row"], + width="100%" + ), + spacing="5", + width="100%" + ), + width="100%" + ) +``` diff --git a/docs/recipes/layout/navbar.md b/docs/recipes/layout/navbar.md new file mode 100644 index 00000000000..3feed7f68c4 --- /dev/null +++ b/docs/recipes/layout/navbar.md @@ -0,0 +1,364 @@ +```python exec +import reflex as rx +``` + +# Navigation Bar + +A navigation bar, also known as a navbar, is a common UI element found at the top of a webpage or application. +It typically provides links or buttons to the main sections of a website or application, allowing users to easily navigate and access the different pages. + +Navigation bars are useful for web apps because they provide a consistent and intuitive way for users to navigate through the app. +Having a clear and consistent navigation structure can greatly improve the user experience by making it easy for users to find the information they need and access the different features of the app. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=2365&end=2627 +# Video: Example of Using the Navbar Recipe +``` + +## Basic + +```python demo exec toggle +def navbar_link(text: str, url: str) -> rx.Component: + return rx.link(rx.text(text, size="4", weight="medium"), href=url) + +def navbar() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_link("Home", "/#"), + navbar_link("About", "/#"), + navbar_link("Pricing", "/#"), + navbar_link("Contact", "/#"), + justify="end", + spacing="5" + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2em", + height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon("menu", size=30)), + rx.menu.content( + rx.menu.item("Home"), + rx.menu.item("About"), + rx.menu.item("Pricing"), + rx.menu.item("Contact"), + ), + justify="end" + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## Dropdown + +```python demo exec toggle +def navbar_link(text: str, url: str) -> rx.Component: + return rx.link(rx.text(text, size="4", weight="medium"), href=url) + +def navbar_dropdown() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_link("Home", "/#"), + rx.menu.root( + rx.menu.trigger( + rx.button(rx.text("Services", size="4", weight="medium"), rx.icon( + "chevron-down"), weight="medium", variant="ghost", size="3"), + ), + rx.menu.content( + rx.menu.item("Service 1"), + rx.menu.item("Service 2"), + rx.menu.item("Service 3"), + ), + ), + navbar_link("Pricing", "/#"), + navbar_link("Contact", "/#"), + justify="end", + spacing="5" + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon("menu", size=30)), + rx.menu.content( + rx.menu.item("Home"), + rx.menu.sub( + rx.menu.sub_trigger("Services"), + rx.menu.sub_content( + rx.menu.item("Service 1"), + rx.menu.item("Service 2"), + rx.menu.item("Service 3"), + ), + ), + rx.menu.item("About"), + rx.menu.item("Pricing"), + rx.menu.item("Contact"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## Search bar + +```python demo exec toggle +def navbar_searchbar() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.input( + rx.input.slot(rx.icon("search")), + placeholder="Search...", + type="search", size="2", + justify="end", + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.input( + rx.input.slot(rx.icon("search")), + placeholder="Search...", + type="search", size="2", + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## Icons + +```python demo exec toggle +def navbar_icons_item(text: str, icon: str, url: str) -> rx.Component: + return rx.link(rx.hstack(rx.icon(icon), rx.text(text, size="4", weight="medium")), href=url) + +def navbar_icons_menu_item(text: str, icon: str, url: str) -> rx.Component: + return rx.link(rx.hstack(rx.icon(icon, size=16), rx.text(text, size="3", weight="medium")), href=url) + +def navbar_icons() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_icons_item("Home", "home", "/#"), + navbar_icons_item("Pricing", "coins", "/#"), + navbar_icons_item("Contact", "mail", "/#"), + navbar_icons_item("Services", "layers", "/#"), + spacing="6", + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon("menu", size=30)), + rx.menu.content( + navbar_icons_menu_item("Home", "home", "/#"), + navbar_icons_menu_item("Pricing", "coins", "/#"), + navbar_icons_menu_item("Contact", "mail", "/#"), + navbar_icons_menu_item("Services", "layers", "/#"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## Buttons + +```python demo exec toggle +def navbar_link(text: str, url: str) -> rx.Component: + return rx.link(rx.text(text, size="4", weight="medium"), href=url) + +def navbar_buttons() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_link("Home", "/#"), + navbar_link("About", "/#"), + navbar_link("Pricing", "/#"), + navbar_link("Contact", "/#"), + spacing="5", + ), + rx.hstack( + rx.button("Sign Up", size="3", variant="outline"), + rx.button("Log In", size="3"), + spacing="4", + justify="end", + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon("menu", size=30)), + rx.menu.content( + rx.menu.item("Home"), + rx.menu.item("About"), + rx.menu.item("Pricing"), + rx.menu.item("Contact"), + rx.menu.separator(), + rx.menu.item("Log in"), + rx.menu.item("Sign up"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## User profile + +```python demo exec toggle +def navbar_link(text: str, url: str) -> rx.Component: + return rx.link(rx.text(text, size="4", weight="medium"), href=url) + +def navbar_user() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_link("Home", "/#"), + navbar_link("About", "/#"), + navbar_link("Pricing", "/#"), + navbar_link("Contact", "/#"), + spacing="5", + ), + rx.menu.root( + rx.menu.trigger(rx.icon_button( + rx.icon("user"), size="2", radius="full")), + rx.menu.content( + rx.menu.item("Settings"), + rx.menu.item("Earnings"), + rx.menu.separator(), + rx.menu.item("Log out"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon_button( + rx.icon("user"), size="2", radius="full")), + rx.menu.content( + rx.menu.item("Settings"), + rx.menu.item("Earnings"), + rx.menu.separator(), + rx.menu.item("Log out"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` diff --git a/docs/recipes/layout/sidebar.md b/docs/recipes/layout/sidebar.md new file mode 100644 index 00000000000..8183ab1e276 --- /dev/null +++ b/docs/recipes/layout/sidebar.md @@ -0,0 +1,420 @@ +```python exec +import reflex as rx + +def sidebar_item(text: str, icon: str, href: str) -> rx.Component: + return rx.link( + rx.hstack( + rx.icon(icon), + rx.text(text, size="4"), + width="100%", + padding_x="0.5rem", + padding_y="0.75rem", + align="center", + style={ + "_hover": { + "bg": rx.color("accent", 4), + "color": rx.color("accent", 11), + }, + "border-radius": "0.5em", + }, + ), + href=href, + underline="none", + weight="medium", + width="100%" + ) + +def sidebar_items() -> rx.Component: + return rx.vstack( + sidebar_item("Dashboard", "layout-dashboard", "/#"), + sidebar_item("Projects", "square-library", "/#"), + sidebar_item("Analytics", "bar-chart-4", "/#"), + sidebar_item("Messages", "mail", "/#"), + spacing="1", + width="100%" + ) +``` + +# Sidebar + +Similar to a navigation bar, a sidebar is a common UI element found on the side of a webpage or application. It typically contains links to different sections of the site or app. + +## Basic + +```python demo exec toggle +def sidebar_item(text: str, icon: str, href: str) -> rx.Component: + return rx.link( + rx.hstack( + rx.icon(icon), + rx.text(text, size="4"), + width="100%", + padding_x="0.5rem", + padding_y="0.75rem", + align="center", + style={ + "_hover": { + "bg": rx.color("accent", 4), + "color": rx.color("accent", 11), + }, + "border-radius": "0.5em", + }, + ), + href=href, + underline="none", + weight="medium", + width="100%" + ) + +def sidebar_items() -> rx.Component: + return rx.vstack( + sidebar_item("Dashboard", "layout-dashboard", "/#"), + sidebar_item("Projects", "square-library", "/#"), + sidebar_item("Analytics", "bar-chart-4", "/#"), + sidebar_item("Messages", "mail", "/#"), + spacing="1", + width="100%" + ) + +def sidebar() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.vstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.25em", + height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), + align="center", + justify="start", + padding_x="0.5rem", + width="100%" + ), + sidebar_items(), + spacing="5", + #position="fixed", + # left="0px", + # top="0px", + # z_index="5", + padding_x="1em", + padding_y="1.5em", + bg=rx.color("accent", 3), + align="start", + #height="100%", + height="650px", + width="16em", + ), + ), + rx.mobile_and_tablet( + rx.drawer.root( + rx.drawer.trigger(rx.icon("align-justify", size=30)), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.box( + rx.drawer.close(rx.icon("x", size=30)), + width="100%", + ), + sidebar_items(), + spacing="5", + width="100%", + ), + top="auto", + right="auto", + height="100%", + width="20em", + padding="1.5em", + bg=rx.color("accent", 2) + ), + width="100%", + ), + direction="left" + ), + padding="1em", + ), + ) +``` + +## Bottom user profile + +```python demo exec toggle +def sidebar_item(text: str, icon: str, href: str) -> rx.Component: + return rx.link( + rx.hstack( + rx.icon(icon), + rx.text(text, size="4"), + width="100%", + padding_x="0.5rem", + padding_y="0.75rem", + align="center", + style={ + "_hover": { + "bg": rx.color("accent", 4), + "color": rx.color("accent", 11), + }, + "border-radius": "0.5em", + }, + ), + href=href, + underline="none", + weight="medium", + width="100%" + ) + +def sidebar_items() -> rx.Component: + return rx.vstack( + sidebar_item("Dashboard", "layout-dashboard", "/#"), + sidebar_item("Projects", "square-library", "/#"), + sidebar_item("Analytics", "bar-chart-4", "/#"), + sidebar_item("Messages", "mail", "/#"), + spacing="1", + width="100%" + ) + +def sidebar_bottom_profile() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.vstack( + rx.hstack( + rx.image(src="https://web.reflex-assets.dev/other/logo.jpg", width="2.25em", + height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), + align="center", + justify="start", + padding_x="0.5rem", + width="100%" + ), + sidebar_items(), + rx.spacer(), + rx.vstack( + rx.vstack( + sidebar_item("Settings", "settings", "/#"), + sidebar_item("Log out", "log-out", "/#"), + spacing="1", + width="100%" + ), + rx.divider(), + rx.hstack( + rx.icon_button(rx.icon("user"), size="3", radius="full"), + rx.vstack( + rx.box( + rx.text("My account", size="3", weight="bold"), + rx.text("user@reflex.dev", size="2", weight="medium"), + width="100%" + ), + spacing="0", + align="start", + justify="start", + width="100%" + ), + padding_x="0.5rem", + align="center", + justify="start", + width="100%", + ), + width="100%", + spacing="5", + ), + spacing="5", + #position="fixed", + # left="0px", + # top="0px", + # z_index="5", + padding_x="1em", + padding_y="1.5em", + bg=rx.color("accent", 3), + align="start", + #height="100%", + height="650px", + width="16em", + ), + ), + rx.mobile_and_tablet( + rx.drawer.root( + rx.drawer.trigger(rx.icon("align-justify", size=30)), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.box( + rx.drawer.close(rx.icon("x", size=30)), + width="100%", + ), + sidebar_items(), + rx.spacer(), + rx.vstack( + rx.vstack( + sidebar_item("Settings", "settings", "/#"), + sidebar_item("Log out", "log-out", "/#"), + width="100%", + spacing="1", + ), + rx.divider(margin="0"), + rx.hstack( + rx.icon_button(rx.icon("user"), size="3", radius="full"), + rx.vstack( + rx.box( + rx.text("My account", size="3", weight="bold"), + rx.text("user@reflex.dev", size="2", weight="medium"), + width="100%" + ), + spacing="0", + justify="start", + width="100%", + ), + padding_x="0.5rem", + align="center", + justify="start", + width="100%", + ), + width="100%", + spacing="5", + ), + spacing="5", + width="100%", + ), + top="auto", + right="auto", + height="100%", + width="20em", + padding="1.5em", + bg=rx.color("accent", 2) + ), + width="100%", + ), + direction="left" + ), + padding="1em", + ), + ) +``` + +## Top user profile + +```python demo exec toggle +def sidebar_item(text: str, icon: str, href: str) -> rx.Component: + return rx.link( + rx.hstack( + rx.icon(icon), + rx.text(text, size="4"), + width="100%", + padding_x="0.5rem", + padding_y="0.75rem", + align="center", + style={ + "_hover": { + "bg": rx.color("accent", 4), + "color": rx.color("accent", 11), + }, + "border-radius": "0.5em", + }, + ), + href=href, + underline="none", + weight="medium", + width="100%" + ) + +def sidebar_items() -> rx.Component: + return rx.vstack( + sidebar_item("Dashboard", "layout-dashboard", "/#"), + sidebar_item("Projects", "square-library", "/#"), + sidebar_item("Analytics", "bar-chart-4", "/#"), + sidebar_item("Messages", "mail", "/#"), + spacing="1", + width="100%" + ) + +def sidebar_top_profile() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.vstack( + rx.hstack( + rx.icon_button(rx.icon("user"), size="3", radius="full"), + rx.vstack( + rx.box( + rx.text("My account", size="3", weight="bold"), + rx.text("user@reflex.dev", size="2", weight="medium"), + width="100%" + ), + spacing="0", + justify="start", + width="100%", + ), + rx.spacer(), + rx.icon_button(rx.icon("settings"), size="2", + variant="ghost", color_scheme="gray"), + padding_x="0.5rem", + align="center", + width="100%", + ), + sidebar_items(), + rx.spacer(), + sidebar_item("Help & Support", "life-buoy", "/#"), + spacing="5", + #position="fixed", + # left="0px", + # top="0px", + # z_index="5", + padding_x="1em", + padding_y="1.5em", + bg=rx.color("accent", 3), + align="start", + #height="100%", + height="650px", + width="16em", + ), + ), + rx.mobile_and_tablet( + rx.drawer.root( + rx.drawer.trigger(rx.icon("align-justify", size=30)), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.box( + rx.drawer.close(rx.icon("x", size=30)), + width="100%", + ), + sidebar_items(), + rx.spacer(), + rx.vstack( + sidebar_item("Help & Support", "life-buoy", "/#"), + rx.divider(margin="0"), + rx.hstack( + rx.icon_button(rx.icon("user"), size="3", radius="full"), + rx.vstack( + rx.box( + rx.text("My account", size="3", weight="bold"), + rx.text("user@reflex.dev", size="2", weight="medium"), + width="100%" + ), + spacing="0", + justify="start", + width="100%", + ), + padding_x="0.5rem", + align="center", + justify="start", + width="100%", + ), + width="100%", + spacing="5", + ), + spacing="5", + width="100%", + ), + top="auto", + right="auto", + height="100%", + width="20em", + padding="1.5em", + bg=rx.color("accent", 2) + ), + width="100%", + ), + direction="left" + ), + padding="1em", + ), + ) +``` diff --git a/docs/recipes/others/checkboxes.md b/docs/recipes/others/checkboxes.md new file mode 100644 index 00000000000..8b55f81c111 --- /dev/null +++ b/docs/recipes/others/checkboxes.md @@ -0,0 +1,67 @@ +```python exec +import reflex as rx +``` + +# Smart Checkboxes Group + +A smart checkboxes group where you can track all checked boxes, as well as place a limit on how many checks are possible. + +## Recipe + +```python eval +rx.center(rx.image(src="https://web.reflex-assets.dev/templates/smart_checkboxes.webp")) +``` + +This recipe use a `dict[str, bool]` for the checkboxes state tracking. +Additionally, the limit that prevent the user from checking more boxes than allowed with a computed var. + +```python +class CBoxeState(rx.State): + + choices: dict[str, bool] = \{k: False for k in ["Choice A", "Choice B", "Choice C"]} + _check_limit = 2 + + def check_choice(self, value, index): + self.choices[index] = value + + @rx.var + def choice_limit(self): + return sum(self.choices.values()) >= self._check_limit + + @rx.var + def checked_choices(self): + choices = [l for l, v in self.choices.items() if v] + return " / ".join(choices) if choices else "None" + +import reflex as rx + + +def render_checkboxes(values, limit, handler): + return rx.vstack( + rx.foreach( + values, + lambda choice: rx.checkbox( + choice[0], + checked=choice[1], + disabled=~choice[1] & limit, + on_change=lambda val: handler(val, choice[0]), + ), + ) + ) + + +def index() -> rx.Component: + + return rx.center( + rx.vstack( + rx.text("Make your choices (2 max):"), + render_checkboxes( + CBoxeState.choices, + CBoxeState.choice_limit, + CBoxeState.check_choice, + ), + rx.text("Your choices: ", CBoxeState.checked_choices), + ), + height="100vh", + ) +``` diff --git a/docs/recipes/others/chips.md b/docs/recipes/others/chips.md new file mode 100644 index 00000000000..c72ab54aa50 --- /dev/null +++ b/docs/recipes/others/chips.md @@ -0,0 +1,247 @@ +```python exec +import reflex as rx + +``` + +# Chips + +Chips are compact elements that represent small pieces of information, such as tags or categories. They are commonly used to select multiple items from a list or to filter content. + +## Status + +```python demo exec toggle +from reflex.components.radix.themes.base import LiteralAccentColor + +status_chip_props = { + "radius": "full", + "variant": "outline", + "size": "3", +} + +def status_chip(status: str, icon: str, color: LiteralAccentColor) -> rx.Component: + return rx.badge( + rx.icon(icon, size=18), + status, + color_scheme=color, + **status_chip_props, + ) + +def status_chips_group() -> rx.Component: + return rx.hstack( + status_chip("Info", "info", "blue"), + status_chip("Success", "circle-check", "green"), + status_chip("Warning", "circle-alert", "yellow"), + status_chip("Error", "circle-x", "red"), + wrap="wrap", + spacing="2", + ) +``` + +## Single selection + +```python demo exec toggle +chip_props = { + "radius": "full", + "variant": "soft", + "size": "3", + "cursor": "pointer", + "style": {"_hover": {"opacity": 0.75}}, +} + +available_items = ["2:00", "3:00", "4:00", "5:00"] + +class SingleSelectionChipsState(rx.State): + selected_item: str = "" + + @rx.event + def set_selected_item(self, value: str): + self.selected_item = value + +def unselected_item(item: str) -> rx.Component: + return rx.badge( + item, + color_scheme="gray", + **chip_props, + on_click=SingleSelectionChipsState.set_selected_item(item), + ) + +def selected_item(item: str) -> rx.Component: + return rx.badge( + rx.icon("check", size=18), + item, + color_scheme="mint", + **chip_props, + on_click=SingleSelectionChipsState.set_selected_item(""), + ) + +def item_chip(item: str) -> rx.Component: + return rx.cond( + SingleSelectionChipsState.selected_item == item, + selected_item(item), + unselected_item(item), + ) + +def item_selector() -> rx.Component: + return rx.vstack( + rx.hstack( + rx.icon("clock", size=20), + rx.heading( + "Select your reservation time:", size="4" + ), + spacing="2", + align="center", + width="100%", + ), + rx.hstack( + rx.foreach(available_items, item_chip), + wrap="wrap", + spacing="2", + ), + align_items="start", + spacing="4", + width="100%", + ) +``` + +## Multiple selection + +This example demonstrates selecting multiple skills from a list. It includes buttons to add all skills, clear selected skills, and select a random number of skills. + +```python demo exec toggle +import random +from reflex.components.radix.themes.base import LiteralAccentColor + +chip_props = { + "radius": "full", + "variant": "surface", + "size": "3", + "cursor": "pointer", + "style": {"_hover": {"opacity": 0.75}}, +} + +skills = [ + "Data Management", + "Networking", + "Security", + "Cloud", + "DevOps", + "Data Science", + "AI", + "ML", + "Robotics", + "Cybersecurity", +] + +class BasicChipsState(rx.State): + selected_items: list[str] = skills[:3] + + @rx.event + def add_selected(self, item: str): + self.selected_items.append(item) + + @rx.event + def remove_selected(self, item: str): + self.selected_items.remove(item) + + @rx.event + def add_all_selected(self): + self.selected_items = list(skills) + + @rx.event + def clear_selected(self): + self.selected_items.clear() + + @rx.event + def random_selected(self): + self.selected_items = random.sample(skills, k=random.randint(1, len(skills))) + +def action_button(icon: str, label: str, on_click: callable, color_scheme: LiteralAccentColor) -> rx.Component: + return rx.button( + rx.icon(icon, size=16), + label, + variant="soft", + size="2", + on_click=on_click, + color_scheme=color_scheme, + cursor="pointer", + ) + +def selected_item_chip(item: str) -> rx.Component: + return rx.badge( + item, + rx.icon("circle-x", size=18), + color_scheme="green", + **chip_props, + on_click=BasicChipsState.remove_selected(item), + ) + +def unselected_item_chip(item: str) -> rx.Component: + return rx.cond( + BasicChipsState.selected_items.contains(item), + rx.fragment(), + rx.badge( + item, + rx.icon("circle-plus", size=18), + color_scheme="gray", + **chip_props, + on_click=BasicChipsState.add_selected(item), + ), + ) + +def items_selector() -> rx.Component: + return rx.vstack( + rx.flex( + rx.hstack( + rx.icon("lightbulb", size=20), + rx.heading( + "Skills" + f" ({BasicChipsState.selected_items.length()})", size="4" + ), + spacing="1", + align="center", + width="100%", + justify_content=["end", "start"], + ), + rx.hstack( + action_button( + "plus", "Add All", BasicChipsState.add_all_selected, "green" + ), + action_button( + "trash", "Clear All", BasicChipsState.clear_selected, "tomato" + ), + action_button( + "shuffle", "", BasicChipsState.random_selected, "gray" + ), + spacing="2", + justify="end", + width="100%", + ), + justify="between", + flex_direction=["column", "row"], + align="center", + spacing="2", + margin_bottom="10px", + width="100%", + ), + # Selected Items + rx.flex( + rx.foreach( + BasicChipsState.selected_items, + selected_item_chip, + ), + wrap="wrap", + spacing="2", + justify_content="start", + ), + rx.divider(), + # Unselected Items + rx.flex( + rx.foreach(skills, unselected_item_chip), + wrap="wrap", + spacing="2", + justify_content="start", + ), + justify_content="start", + align_items="start", + width="100%", + ) +``` diff --git a/docs/recipes/others/dark_mode_toggle.md b/docs/recipes/others/dark_mode_toggle.md new file mode 100644 index 00000000000..c648941157e --- /dev/null +++ b/docs/recipes/others/dark_mode_toggle.md @@ -0,0 +1,33 @@ +```python exec +import reflex as rx +from reflex.style import set_color_mode, color_mode +``` + +# Dark Mode Toggle + +The Dark Mode Toggle component lets users switch between light and dark themes. + +```python demo exec toggle +import reflex as rx +from reflex.style import set_color_mode, color_mode + +def dark_mode_toggle() -> rx.Component: + return rx.segmented_control.root( + rx.segmented_control.item( + rx.icon(tag="monitor", size=20), + value="system", + ), + rx.segmented_control.item( + rx.icon(tag="sun", size=20), + value="light", + ), + rx.segmented_control.item( + rx.icon(tag="moon", size=20), + value="dark", + ), + on_change=set_color_mode, + variant="classic", + radius="large", + value=color_mode, + ) +``` diff --git a/docs/recipes/others/pricing_cards.md b/docs/recipes/others/pricing_cards.md new file mode 100644 index 00000000000..0d88ae4824c --- /dev/null +++ b/docs/recipes/others/pricing_cards.md @@ -0,0 +1,198 @@ +```python exec +import reflex as rx +``` + +# Pricing Cards + +A pricing card shows the price of a product or service. It typically includes a title, description, price, features, and a purchase button. + +## Basic + +```python demo exec toggle +def feature_item(text: str) -> rx.Component: + return rx.hstack(rx.icon("check", color=rx.color("grass", 9)), rx.text(text, size="4")) + +def features() -> rx.Component: + return rx.vstack( + feature_item("24/7 customer support"), + feature_item("Daily backups"), + feature_item("Advanced analytics"), + feature_item("Customizable templates"), + feature_item("Priority email support"), + width="100%", + align_items="start", + ) + +def pricing_card_beginner() -> rx.Component: + return rx.vstack( + rx.vstack( + rx.text("Beginner", weight="bold", size="6"), + rx.text("Ideal choice for personal use & for your next project.", size="4", opacity=0.8, align="center"), + rx.hstack( + rx.text("$39", weight="bold", font_size="3rem", trim="both"), + rx.text("/month", size="4", opacity=0.8, trim="both"), + width="100%", + align_items="end", + justify="center" + ), + width="100%", + align="center", + spacing="6", + ), + features(), + rx.button("Get started", size="3", variant="solid", width="100%", color_scheme="blue"), + spacing="6", + border=f"1.5px solid {rx.color('gray', 5)}", + background=rx.color("gray", 1), + padding="28px", + width="100%", + max_width="400px", + justify="center", + border_radius="0.5rem", + ) +``` + +## Comparison cards + +```python demo exec toggle +def feature_item(feature: str) -> rx.Component: + return rx.hstack( + rx.icon("check", color=rx.color("blue", 9), size=21), + rx.text(feature, size="4", weight="regular"), + ) + + +def standard_features() -> rx.Component: + return rx.vstack( + feature_item("40 credits for image generation"), + feature_item("Credits never expire"), + feature_item("High quality images"), + feature_item("Commercial license"), + spacing="3", + width="100%", + align_items="start", + ) + + +def popular_features() -> rx.Component: + return rx.vstack( + feature_item("250 credits for image generation"), + feature_item("+30% Extra free credits"), + feature_item("Credits never expire"), + feature_item("High quality images"), + feature_item("Commercial license"), + spacing="3", + width="100%", + align_items="start", + ) + + +def pricing_card_standard() -> rx.Component: + return rx.vstack( + rx.hstack( + rx.hstack( + rx.text( + "$14.99", + trim="both", + as_="s", + size="3", + weight="regular", + opacity=0.8, + ), + rx.text("$3.99", trim="both", size="6", weight="regular"), + width="100%", + spacing="2", + align_items="end", + ), + height="35px", + align_items="center", + justify="between", + width="100%", + ), + rx.text( + "40 Image Credits", + weight="bold", + size="7", + width="100%", + text_align="left", + ), + standard_features(), + rx.spacer(), + rx.button( + "Purchase", + size="3", + variant="outline", + width="100%", + color_scheme="blue", + ), + spacing="6", + border=f"1.5px solid {rx.color('gray', 5)}", + background=rx.color("gray", 1), + padding="28px", + width="100%", + max_width="400px", + min_height="475px", + border_radius="0.5rem", + ) + + +def pricing_card_popular() -> rx.Component: + return rx.vstack( + rx.hstack( + rx.hstack( + rx.text( + "$69.99", + trim="both", + as_="s", + size="3", + weight="regular", + opacity=0.8, + ), + rx.text("$18.99", trim="both", size="6", weight="regular"), + width="100%", + spacing="2", + align_items="end", + ), + rx.badge( + "POPULAR", + size="2", + radius="full", + variant="soft", + color_scheme="blue", + ), + align_items="center", + justify="between", + height="35px", + width="100%", + ), + rx.text( + "250 Image Credits", + weight="bold", + size="7", + width="100%", + text_align="left", + ), + popular_features(), + rx.spacer(), + rx.button("Purchase", size="3", width="100%", color_scheme="blue"), + spacing="6", + border=f"1.5px solid {rx.color('blue', 6)}", + background=rx.color("blue", 1), + padding="28px", + width="100%", + max_width="400px", + min_height="475px", + border_radius="0.5rem", + ) + + +def pricing_cards() -> rx.Component: + return rx.flex( + pricing_card_standard(), + pricing_card_popular(), + spacing="4", + flex_direction=["column", "column", "row"], + width="100%", + align_items="center", + ) +``` diff --git a/docs/recipes/others/speed_dial.md b/docs/recipes/others/speed_dial.md new file mode 100644 index 00000000000..4d2086ec080 --- /dev/null +++ b/docs/recipes/others/speed_dial.md @@ -0,0 +1,454 @@ +```python exec +import reflex as rx +``` + +# Speed Dial + +A speed dial is a component that allows users to quickly access frequently used actions or pages. It is often used in the bottom right corner of the screen. + +# Vertical + +```python demo exec toggle +class SpeedDialVertical(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.tooltip( + rx.icon_button( + rx.icon(icon, padding="2px"), + variant="soft", + color_scheme="gray", + size="3", + cursor="pointer", + radius="full", + ), + side="left", + content=text, + ) + + def menu() -> rx.Component: + return rx.vstack( + menu_item("copy", "Copy"), + menu_item("download", "Download"), + menu_item("share-2", "Share"), + position="absolute", + bottom="100%", + spacing="2", + padding_bottom="10px", + left="0", + direction="column-reverse", + align_items="center", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + ), + variant="solid", + color_scheme="blue", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + +speed_dial_vertical = SpeedDialVertical.create + +def render_vertical(): + return rx.box( + speed_dial_vertical(), + height="250px", + position="relative", + width="100%", + ) +``` + +# Horizontal + +```python demo exec toggle +class SpeedDialHorizontal(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.tooltip( + rx.icon_button( + rx.icon(icon, padding="2px"), + variant="soft", + color_scheme="gray", + size="3", + cursor="pointer", + radius="full", + ), + side="top", + content=text, + ) + + def menu() -> rx.Component: + return rx.hstack( + menu_item("copy", "Copy"), + menu_item("download", "Download"), + menu_item("share-2", "Share"), + position="absolute", + bottom="0", + spacing="2", + padding_right="10px", + right="100%", + direction="row-reverse", + align_items="center", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + class_name="dial", + ), + variant="solid", + color_scheme="green", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + +speed_dial_horizontal = SpeedDialHorizontal.create + +def render_horizontal(): + return rx.box( + speed_dial_horizontal(), + height="250px", + position="relative", + width="100%", + ) +``` + +# Vertical with text + +```python demo exec toggle +class SpeedDialVerticalText(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.hstack( + rx.text(text, weight="medium"), + rx.icon_button( + rx.icon(icon, padding="2px"), + variant="soft", + color_scheme="gray", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + opacity="0.75", + _hover={ + "opacity": "1", + }, + align_items="center", + ) + + def menu() -> rx.Component: + return rx.vstack( + menu_item("copy", "Copy"), + menu_item("download", "Download"), + menu_item("share-2", "Share"), + position="absolute", + bottom="100%", + spacing="2", + padding_bottom="10px", + right="0", + direction="column-reverse", + align_items="end", + justify_content="end", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + class_name="dial", + ), + variant="solid", + color_scheme="crimson", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + +speed_dial_vertical_text = SpeedDialVerticalText.create + +def render_vertical_text(): + return rx.box( + speed_dial_vertical_text(), + height="250px", + position="relative", + width="100%", + ) +``` + +# Reveal animation + +```python demo exec toggle +class SpeedDialReveal(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.tooltip( + rx.icon_button( + rx.icon(icon, padding="2px"), + variant="soft", + color_scheme="gray", + size="3", + cursor="pointer", + radius="full", + style={ + "animation": rx.cond(cls.is_open, "reveal 0.3s ease both", "none"), + "@keyframes reveal": { + "0%": { + "opacity": "0", + "transform": "scale(0)", + }, + "100%": { + "opacity": "1", + "transform": "scale(1)", + }, + }, + }, + ), + side="left", + content=text, + ) + + def menu() -> rx.Component: + return rx.vstack( + menu_item("copy", "Copy"), + menu_item("download", "Download"), + menu_item("share-2", "Share"), + position="absolute", + bottom="100%", + spacing="2", + padding_bottom="10px", + left="0", + direction="column-reverse", + align_items="center", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + class_name="dial", + ), + variant="solid", + color_scheme="violet", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + +speed_dial_reveal = SpeedDialReveal.create + +def render_reveal(): + return rx.box( + speed_dial_reveal(), + height="250px", + position="relative", + width="100%", + ) +``` + +# Menu + +```python demo exec toggle +class SpeedDialMenu(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.hstack( + rx.icon(icon, padding="2px"), + rx.text(text, weight="medium"), + align="center", + opacity="0.75", + cursor="pointer", + position="relative", + _hover={ + "opacity": "1", + }, + width="100%", + align_items="center", + ) + + def menu() -> rx.Component: + return rx.box( + rx.card( + rx.vstack( + menu_item("copy", "Copy"), + rx.divider(margin="0"), + menu_item("download", "Download"), + rx.divider(margin="0"), + menu_item("share-2", "Share"), + direction="column-reverse", + align_items="end", + justify_content="end", + ), + box_shadow="0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", + ), + position="absolute", + bottom="100%", + right="0", + padding_bottom="10px", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + class_name="dial", + ), + variant="solid", + color_scheme="orange", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + + +speed_dial_menu = SpeedDialMenu.create + +def render_menu(): + return rx.box( + speed_dial_menu(), + height="250px", + position="relative", + width="100%", + ) +``` diff --git a/docs/state/overview.md b/docs/state/overview.md new file mode 100644 index 00000000000..7e32f9c8960 --- /dev/null +++ b/docs/state/overview.md @@ -0,0 +1,200 @@ +```python exec +import reflex as rx +def definition(title, *children): + return rx.vstack( + rx.heading(title, font_size="1em", font_weight="bold", color=rx.color("mauve", 12)), + *children, + color=rx.color("mauve", 10), + padding="1em", + border=f"1px solid {rx.color('mauve', 4)}", + background_color=rx.color("mauve", 2), + border_radius="8px", + _hover={"border": f"1px solid {rx.color('mauve', 5)}", "background_color": rx.color("mauve", 3)}, + align_items="start", + ) +``` + +# State + +State allows us to create interactive apps that can respond to user input. +It defines the variables that can change over time, and the functions that can modify them. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=1206&end=1869 +# Video: State Overview +``` + +## State Basics + +You can define state by creating a class that inherits from `rx.State`: + +```python +import reflex as rx + + +class State(rx.State): + """Define your app state here.""" +``` + +A state class is made up of two parts: vars and event handlers. + +**Vars** are variables in your app that can change over time. + +**Event handlers** are functions that modify these vars in response to events. + +These are the main concepts to understand how state works in Reflex: + +```python eval +rx.grid( + definition( + "Base Var", + rx.list.unordered( + rx.list.item("Any variable in your app that can change over time."), + rx.list.item( + "Defined as a field in a ", rx.code("State"), " class" + ), + rx.list.item("Can only be modified by event handlers."), + ), + ), + definition( + "Computed Var", + rx.list.unordered( + rx.list.item("Vars that change automatically based on other vars."), + rx.list.item( + "Defined as functions using the ", + rx.code("@rx.var"), + " decorator.", + ), + rx.list.item( + "Cannot be set by event handlers, are always recomputed when the state changes." + ), + ), + ), + definition( + "Event Trigger", + rx.list.unordered( + rx.list.item( + "A user interaction that triggers an event, such as a button click." + ), + rx.list.item( + "Defined as special component props, such as ", + rx.code("on_click"), + ".", + ), + rx.list.item("Can be used to trigger event handlers."), + ), + ), + definition( + "Event Handlers", + rx.list.unordered( + rx.list.item( + "Functions that update the state in response to events." + ), + rx.list.item( + "Defined as methods in the ", rx.code("State"), " class." + ), + rx.list.item( + "Can be called by event triggers, or by other event handlers." + ), + ), + ), + margin_bottom="1em", + spacing="2", + columns="2", +) +``` + +## Example + +Here is a example of how to use state within a Reflex app. +Click the text to change its color. + +```python demo exec +class ExampleState(rx.State): + + # A base var for the list of colors to cycle through. + colors: list[str] = ["black", "red", "green", "blue", "purple"] + + # A base var for the index of the current color. + index: int = 0 + + @rx.event + def next_color(self): + """An event handler to go to the next color.""" + # Event handlers can modify the base vars. + # Here we reference the base vars `colors` and `index`. + self.index = (self.index + 1) % len(self.colors) + + @rx.var + def color(self)-> str: + """A computed var that returns the current color.""" + # Computed vars update automatically when the state changes. + return self.colors[self.index] + + +def index(): + return rx.heading( + "Welcome to Reflex!", + # Event handlers can be bound to event triggers. + on_click=ExampleState.next_color, + # State vars can be bound to component props. + color=ExampleState.color, + _hover={"cursor": "pointer"}, + ) +``` + +The base vars are `colors` and `index`. They are the only vars in the app that +may be directly modified within event handlers. + +There is a single computed var, `color`, that is a function of the base vars. It +will be computed automatically whenever the base vars change. + +The heading component links its `on_click` event to the +`ExampleState.next_color` event handler, which increments the color index. + +```md alert success +# With Reflex, you never have to write an API. + +All interactions between the frontend and backend are handled through events. +``` + +```md alert info +# State vs. Instance? + +When building the UI of your app, reference vars and event handlers via the state class (`ExampleState`). + +When writing backend event handlers, access and set vars via the instance (`self`). +``` + +```md alert warning +# Cannot print a State var. + +The code `print(ExampleState.index)` will not work because the State var values are only known at compile time. +``` + +## Client States + +Each user who opens your app has a unique ID and their own copy of the state. +This means that each user can interact with the app and modify the state +independently of other users. + +Because Reflex internally creates a new instance of the state for each user, your code should +never directly initialize a state class. + +```md alert info +# Try opening an app in multiple tabs to see how the state changes independently. +``` + +All user state is stored on the server, and all event handlers are executed on +the server. Reflex uses websockets to send events to the server, and to send +state updates back to the client. + +## Helper Methods + +Similar to backend vars, any method defined in a State class that begins with an +underscore `_` is considered a helper method. Such methods are not usable as +event triggers, but may be called from other event handler methods within the +state. + +Functionality that should only be available on the backend, such as an +authenticated action, should use helper methods to ensure it is not accidentally +or maliciously triggered by the client. diff --git a/docs/state_structure/component_state.md b/docs/state_structure/component_state.md new file mode 100644 index 00000000000..12a097b4207 --- /dev/null +++ b/docs/state_structure/component_state.md @@ -0,0 +1,232 @@ +```python exec +import reflex as rx +``` + +# Component State + +_New in version 0.4.6_. + +Defining a subclass of `rx.ComponentState` creates a special type of state that is tied to an +instance of a component, rather than existing globally in the app. A Component State combines +[UI code](/docs/ui/overview) with state [Vars](/docs/vars/base_vars) and +[Event Handlers](/docs/events/events_overview), +and is useful for creating reusable components which operate independently of each other. + +```md alert warning +# ComponentState cannot be used inside `rx.foreach()` as it will only create one state instance for all elements in the loop. Each iteration of the foreach will share the same state, which may lead to unexpected behavior. +``` + +## Using ComponentState + +```python demo exec +class ReusableCounter(rx.ComponentState): + count: int = 0 + + @rx.event + def set_count(self, value: int): + self.count = value + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def decrement(self): + self.count -= 1 + + @classmethod + def get_component(cls, **props): + return rx.hstack( + rx.button("Decrement", on_click=cls.decrement), + rx.text(cls.count), + rx.button("Increment", on_click=cls.increment), + **props, + ) + +reusable_counter = ReusableCounter.create + +def multiple_counters(): + return rx.vstack( + reusable_counter(), + reusable_counter(), + reusable_counter(), + ) +``` + +The vars and event handlers defined on the `ReusableCounter` +class are treated similarly to a normal State class, but will be scoped to the component instance. Each time a +`reusable_counter` is created, a new state class for that instance of the component is also created. + +The `get_component` classmethod is used to define the UI for the component and link it up to the State, which +is accessed via the `cls` argument. Other states may also be referenced by the returned component, but +`cls` will always be the instance of the `ComponentState` that is unique to the component being returned. + +## Passing Props + +Similar to a normal Component, the `ComponentState.create` classmethod accepts the arbitrary +`*children` and `**props` arguments, and by default passes them to your `get_component` classmethod. +These arguments may be used to customize the component, either by applying defaults or +passing props to certain subcomponents. + +```python eval +rx.divider() +``` + +In the following example, we implement an editable text component that allows the user to click on +the text to turn it into an input field. If the user does not provide their own `value` or `on_change` +props, then the defaults defined in the `EditableText` class will be used. + +```python demo exec +class EditableText(rx.ComponentState): + text: str = "Click to edit" + original_text: str + editing: bool = False + + @rx.event + def set_text(self, value: str): + self.text = value + + @rx.event + def start_editing(self, original_text: str): + self.original_text = original_text + self.editing = True + + @rx.event + def stop_editing(self): + self.editing = False + self.original_text = "" + + @classmethod + def get_component(cls, **props): + # Pop component-specific props with defaults before passing **props + value = props.pop("value", cls.text) + on_change = props.pop("on_change", cls.set_text) + cursor = props.pop("cursor", "pointer") + + # Set the initial value of the State var. + initial_value = props.pop("initial_value", None) + if initial_value is not None: + # Update the pydantic model to use the initial value as default. + cls.__fields__["text"].default = initial_value + + # Form elements for editing, saving and reverting the text. + edit_controls = rx.hstack( + rx.input( + value=value, + on_change=on_change, + **props, + ), + rx.icon_button( + rx.icon("x"), + on_click=[ + on_change(cls.original_text), + cls.stop_editing, + ], + type="button", + color_scheme="red", + ), + rx.icon_button(rx.icon("check")), + align="center", + width="100%", + ) + + # Return the text or the form based on the editing Var. + return rx.cond( + cls.editing, + rx.form( + edit_controls, + on_submit=lambda _: cls.stop_editing(), + ), + rx.text( + value, + on_click=cls.start_editing(value), + cursor=cursor, + **props, + ), + ) + + +editable_text = EditableText.create + + +def editable_text_example(): + return rx.vstack( + editable_text(), + editable_text(initial_value="Edit me!", color="blue"), + editable_text(initial_value="Reflex is fun", font_family="monospace", width="100%"), + ) +``` + +```python eval +rx.divider() +``` + +Because this `EditableText` component is designed to be reusable, it can handle the case +where the `value` and `on_change` are linked to a normal global state. + +```python exec +# Hack because flexdown re-inits modules +EditableText._per_component_state_instance_count = 4 +``` + +```python demo exec +class EditableTextDemoState(rx.State): + value: str = "Global state text" + + @rx.event + def set_value(self, value: str): + self.value = value + +def editable_text_with_global_state(): + return rx.vstack( + editable_text(value=EditableTextDemoState.value, on_change=EditableTextDemoState.set_value), + rx.text(EditableTextDemoState.value.upper()), + ) +``` + +## Accessing the State + +The underlying state class of a `ComponentState` is accessible via the `.State` attribute. To use it, +assign an instance of the component to a local variable, then include that instance in the page. + +```python exec +# Hack because flexdown re-inits modules +ReusableCounter._per_component_state_instance_count = 4 +``` + +```python demo exec +def counter_sum(): + counter1 = reusable_counter() + counter2 = reusable_counter() + return rx.vstack( + rx.text(f"Total: {counter1.State.count + counter2.State.count}"), + counter1, + counter2, + ) +``` + +```python eval +rx.divider() +``` + +Other components can also affect a `ComponentState` by referencing its event handlers or vars +via the `.State` attribute. + +```python exec +# Hack because flexdown re-inits modules +ReusableCounter._per_component_state_instance_count = 6 +``` + +```python demo exec +def extended_counter(): + counter1 = reusable_counter() + return rx.vstack( + counter1, + rx.hstack( + rx.icon_button(rx.icon("step_back"), on_click=counter1.State.set_count(0)), + rx.icon_button(rx.icon("plus"), on_click=counter1.State.increment), + rx.button("Double", on_click=counter1.State.set_count(counter1.State.count * 2)), + rx.button("Triple", on_click=counter1.State.set_count(counter1.State.count * 3)), + ), + ) +``` diff --git a/docs/state_structure/mixins.md b/docs/state_structure/mixins.md new file mode 100644 index 00000000000..34b90624590 --- /dev/null +++ b/docs/state_structure/mixins.md @@ -0,0 +1,328 @@ +```python exec +import reflex as rx +``` + +# State Mixins + +State mixins allow you to define shared functionality that can be reused across multiple State classes. This is useful for creating reusable components, shared business logic, or common state patterns. + +## What are State Mixins? + +A state mixin is a State class marked with `mixin=True` that cannot be instantiated directly but can be inherited by other State classes. Mixins provide a way to share: + +- Base variables +- Computed variables +- Event handlers +- Backend variables + +## Basic Mixin Definition + +To create a state mixin, inherit from `rx.State` and pass `mixin=True`: + +```python demo exec +class CounterMixin(rx.State, mixin=True): + count: int = 0 + + @rx.var + def count_display(self) -> str: + return f"Count: {self.count}" + + @rx.event + def increment(self): + self.count += 1 + +class MyState(CounterMixin, rx.State): + name: str = "App" + +def counter_example(): + return rx.vstack( + rx.heading(MyState.name), + rx.text(MyState.count_display), + rx.button("Increment", on_click=MyState.increment), + spacing="4", + align="center", + ) +``` + +In this example, `MyState` automatically inherits the `count` variable, `count_display` computed variable, and `increment` event handler from `CounterMixin`. + +## Multiple Mixin Inheritance + +You can inherit from multiple mixins to combine different pieces of functionality: + +```python demo exec +class TimestampMixin(rx.State, mixin=True): + last_updated: str = "" + + @rx.event + def update_timestamp(self): + import datetime + self.last_updated = datetime.datetime.now().strftime("%H:%M:%S") + +class LoggingMixin(rx.State, mixin=True): + log_messages: list[str] = [] + + @rx.event + def log_message(self, message: str): + self.log_messages.append(message) + +class CombinedState(CounterMixin, TimestampMixin, LoggingMixin, rx.State): + app_name: str = "Multi-Mixin App" + + @rx.event + def increment_with_log(self): + self.increment() + self.update_timestamp() + self.log_message(f"Count incremented to {self.count}") + +def multi_mixin_example(): + return rx.vstack( + rx.heading(CombinedState.app_name), + rx.text(CombinedState.count_display), + rx.text(f"Last updated: {CombinedState.last_updated}"), + rx.button("Increment & Log", on_click=CombinedState.increment_with_log), + rx.cond( + CombinedState.log_messages.length() > 0, + rx.vstack( + rx.foreach( + CombinedState.log_messages[-3:], + rx.text + ), + spacing="1" + ), + rx.text("No logs yet") + ), + spacing="4", + align="center", + ) +``` + +## Backend Variables in Mixins + +Mixins can also include backend variables (prefixed with `_`) that are not sent to the client: + +```python demo exec +class DatabaseMixin(rx.State, mixin=True): + _db_connection: dict = {} # Backend only + user_count: int = 0 # Sent to client + + @rx.event + def fetch_user_count(self): + # Simulate database query + self.user_count = len(self._db_connection.get("users", [])) + +class AppState(DatabaseMixin, rx.State): + app_title: str = "User Management" + +def database_example(): + return rx.vstack( + rx.heading(AppState.app_title), + rx.text(f"User count: {AppState.user_count}"), + rx.button("Fetch Users", on_click=AppState.fetch_user_count), + spacing="4", + align="center", + ) +``` + +Backend variables are useful for storing sensitive data, database connections, or other server-side state that shouldn't be exposed to the client. + +## Computed Variables in Mixins + +Computed variables in mixins work the same as in regular State classes: + +```python demo exec +class FormattingMixin(rx.State, mixin=True): + value: float = 0.0 + + @rx.var + def formatted_value(self) -> str: + return f"${self.value:.2f}" + + @rx.var + def is_positive(self) -> bool: + return self.value > 0 + +class PriceState(FormattingMixin, rx.State): + product_name: str = "Widget" + + @rx.event + def set_price(self, price: str): + try: + self.value = float(price) + except ValueError: + self.value = 0.0 + +def formatting_example(): + return rx.vstack( + rx.heading(f"Product: {PriceState.product_name}"), + rx.text(f"Price: {PriceState.formatted_value}"), + rx.text(f"Positive: {PriceState.is_positive}"), + rx.input( + placeholder="Enter price", + on_blur=PriceState.set_price, + ), + spacing="4", + align="center", + ) +``` + +## Nested Mixin Inheritance + +Mixins can inherit from other mixins to create hierarchical functionality: + +```python demo exec +class BaseMixin(rx.State, mixin=True): + base_value: str = "base" + +class ExtendedMixin(BaseMixin, mixin=True): + extended_value: str = "extended" + + @rx.var + def combined_value(self) -> str: + return f"{self.base_value}-{self.extended_value}" + +class FinalState(ExtendedMixin, rx.State): + final_value: str = "final" + +def nested_mixin_example(): + return rx.vstack( + rx.text(f"Base: {FinalState.base_value}"), + rx.text(f"Extended: {FinalState.extended_value}"), + rx.text(f"Combined: {FinalState.combined_value}"), + rx.text(f"Final: {FinalState.final_value}"), + spacing="4", + align="center", + ) +``` + +This pattern allows you to build complex functionality by composing simpler mixins. + +## Best Practices + +```md alert info +# Mixin Design Guidelines + +- **Single Responsibility**: Each mixin should have a focused purpose +- **Avoid Deep Inheritance**: Keep mixin hierarchies shallow for clarity +- **Document Dependencies**: If mixins depend on specific variables, document them +- **Test Mixins**: Create test cases for mixin functionality +- **Naming Convention**: Use descriptive names ending with "Mixin" +``` + +## Limitations + +```md alert warning +# Important Limitations + +- Mixins cannot be instantiated directly - they must be inherited by concrete State classes +- Variable name conflicts between mixins are resolved by method resolution order (MRO) +- Mixins cannot override methods from the base State class +- The `mixin=True` parameter is required when defining a mixin +``` + +## Common Use Cases + +State mixins are particularly useful for: + +- **Form Validation**: Shared validation logic across forms +- **UI State Management**: Common modal, loading, or notification patterns +- **Logging**: Centralized logging and debugging +- **API Integration**: Shared HTTP client functionality +- **Data Formatting**: Consistent data presentation across components + +```python demo exec +class ValidationMixin(rx.State, mixin=True): + errors: dict[str, str] = {} + is_loading: bool = False + + @rx.event + def validate_email(self, email: str) -> bool: + if "@" not in email or "." not in email: + self.errors["email"] = "Invalid email format" + return False + self.errors.pop("email", None) + return True + + @rx.event + def validate_required(self, field: str, value: str) -> bool: + if not value.strip(): + self.errors[field] = f"{field.title()} is required" + return False + self.errors.pop(field, None) + return True + + @rx.event + def clear_errors(self): + self.errors = {} + +class ContactFormState(ValidationMixin, rx.State): + name: str = "" + email: str = "" + message: str = "" + + def set_name(self, value: str): + self.name = value + + def set_email(self, value: str): + self.email = value + + def set_message(self, value: str): + self.message = value + + @rx.event + def submit_form(self): + self.clear_errors() + valid_name = self.validate_required("name", self.name) + valid_email = self.validate_email(self.email) + valid_message = self.validate_required("message", self.message) + + if valid_name and valid_email and valid_message: + self.is_loading = True + yield rx.sleep(1) + self.is_loading = False + self.name = "" + self.email = "" + self.message = "" + +def validation_example(): + return rx.vstack( + rx.heading("Contact Form"), + rx.input( + placeholder="Name", + value=ContactFormState.name, + on_change=ContactFormState.set_name, + ), + rx.cond( + ContactFormState.errors.contains("name"), + rx.text(ContactFormState.errors["name"], color="red"), + ), + rx.input( + placeholder="Email", + value=ContactFormState.email, + on_change=ContactFormState.set_email, + ), + rx.cond( + ContactFormState.errors.contains("email"), + rx.text(ContactFormState.errors["email"], color="red"), + ), + rx.text_area( + placeholder="Message", + value=ContactFormState.message, + on_change=ContactFormState.set_message, + ), + rx.cond( + ContactFormState.errors.contains("message"), + rx.text(ContactFormState.errors["message"], color="red"), + ), + rx.button( + "Submit", + on_click=ContactFormState.submit_form, + loading=ContactFormState.is_loading, + ), + spacing="4", + align="center", + width="300px", + ) +``` + +By using state mixins, you can create modular, reusable state logic that keeps your application organized and reduces code duplication. diff --git a/docs/state_structure/overview.md b/docs/state_structure/overview.md new file mode 100644 index 00000000000..53060cbc907 --- /dev/null +++ b/docs/state_structure/overview.md @@ -0,0 +1,235 @@ +```python exec +import reflex as rx +from typing import Any +``` + +# Substates + +Substates allow you to break up your state into multiple classes to make it more manageable. This is useful as your app +grows, as it allows you to think about each page as a separate entity. Substates also allow you to share common state +resources, such as variables or event handlers. + +When a particular state class becomes too large, breaking it up into several substates can bring performance +benefits by only loading parts of the state that are used to handle a certain event. + +## Multiple States + +One common pattern is to create a substate for each page in your app. +This allows you to think about each page as a separate entity, and makes it easier to manage your code as your app grows. + +To create a substate, simply inherit from `rx.State` multiple times: + +```python +# index.py +import reflex as rx + +class IndexState(rx.State): + """Define your main state here.""" + data: str = "Hello World" + + +@rx.page() +def index(): + return rx.box(rx.text(IndexState.data)) + +# signup.py +import reflex as rx + + +class SignupState(rx.State): + """Define your signup state here.""" + username: str = "" + password: str = "" + + def signup(self): + ... + + +@rx.page() +def signup_page(): + return rx.box( + rx.input(value=SignupState.username), + rx.input(value=SignupState.password), + ) + +# login.py +import reflex as rx + +class LoginState(rx.State): + """Define your login state here.""" + username: str = "" + password: str = "" + + def login(self): + ... + +@rx.page() +def login_page(): + return rx.box( + rx.input(value=LoginState.username), + rx.input(value=LoginState.password), + ) +``` + +Separating the states is purely a matter of organization. You can still access the state from other pages by importing the state class. + +```python +# index.py + +import reflex as rx + +from signup import SignupState + +... + +def index(): + return rx.box( + rx.text(IndexState.data), + rx.input(value=SignupState.username), + rx.input(value=SignupState.password), + ) +``` + +## Accessing Arbitrary States + +An event handler in a particular state can access and modify vars in another state instance by calling +the `get_state` async method and passing the desired state class. If the requested state is not already loaded, +it will be loaded and deserialized on demand. + +In the following example, the `GreeterState` accesses the `SettingsState` to get the `salutation` and uses it +to update the `message` var. + +Notably, the widget that sets the salutation does NOT have to load the `GreeterState` when handling the +input `on_change` event, which improves performance. + +```python demo exec +class SettingsState(rx.State): + salutation: str = "Hello" + + def set_salutation(self, value: str): + self.salutation = value + +def set_salutation_popover(): + return rx.popover.root( + rx.popover.trigger( + rx.icon_button(rx.icon("settings")), + ), + rx.popover.content( + rx.input( + value=SettingsState.salutation, + on_change=SettingsState.set_salutation + ), + ), + ) + + +class GreeterState(rx.State): + message: str = "" + + @rx.event + async def handle_submit(self, form_data: dict[str, Any]): + settings = await self.get_state(SettingsState) + self.message = f"{settings.salutation} {form_data['name']}" + + +def index(): + return rx.vstack( + rx.form( + rx.vstack( + rx.hstack( + rx.input(placeholder="Name", id="name"), + set_salutation_popover(), + ), + rx.button("Submit"), + ), + reset_on_submit=True, + on_submit=GreeterState.handle_submit, + ), + rx.text(GreeterState.message), + ) +``` + +### Accessing Individual Var Values + +In addition to accessing entire state instances with `get_state`, you can retrieve individual variable values using the `get_var_value` method: + +```python +# Access a var value from another state +value = await self.get_var_value(OtherState.some_var) +``` + +This async method is particularly useful when you only need a specific value rather than loading the entire state. Using `get_var_value` can be more efficient than `get_state` when: + +1. You only need to access a single variable from another state +2. The other state contains a large amount of data +3. You want to avoid loading unnecessary data into memory + +Here's an example that demonstrates how to use `get_var_value` to access data between states: + +```python demo exec +# Define a state that holds a counter value +class CounterState(rx.State): + # This variable will be accessed from another state + count: int = 0 + + @rx.event + async def increment(self): + # Increment the counter when the button is clicked + self.count += 1 + +# Define a separate state that will display information +class DisplayState(rx.State): + # This will show the current count value + message: str = "" + + @rx.event + async def show_count(self): + # Use get_var_value to access just the count variable from CounterState + # This is more efficient than loading the entire state with get_state + current = await self.get_var_value(CounterState.count) + self.message = f"Current count: {current}" + +def var_value_example(): + return rx.vstack( + rx.heading("Get Var Value Example"), + rx.hstack( + # This button calls DisplayState.show_count to display the current count + rx.button("Get Count Value", on_click=DisplayState.show_count), + # This button calls CounterState.increment to increase the counter + rx.button("Increment", on_click=CounterState.increment), + ), + # Display the message from DisplayState + rx.text(DisplayState.message), + width="100%", + align="center", + spacing="4", + ) +``` + +In this example: + +1. We have two separate states: `CounterState` which manages a counter, and `DisplayState` which displays information +2. When you click "Increment", it calls `CounterState.increment()` to increase the counter value +3. When you click "Show Count", it calls `DisplayState.show_count()` which uses `get_var_value` to retrieve just the count value from `CounterState` without loading the entire state +4. The current count is then displayed in the message + +This pattern is useful when you have multiple states that need to interact with each other but don't need to access all of each other's data. + +If the var is not retrievable, `get_var_value` will raise an `UnretrievableVarValueError`. + +## Performance Implications + +When an event handler is called, Reflex will load the data not only for the substate containing +the event handler, but also all of its substates and parent states as well. +If a state has a large number of substates or contains a large amount of data, it can slow down processing +of events associated with that state. + +For optimal performance, keep a flat structure with most substate classes directly inheriting from `rx.State`. +Only inherit from another state when the parent holds data that is commonly used by the substate. +Implementing different parts of the app with separate, unconnected states ensures that only the necessary +data is loaded for processing events for a particular page or component. + +Avoid defining computed vars inside a state that contains a large amount of data, as +states with computed vars are always loaded to ensure the values are recalculated. +When using computed vars, it better to define them in a state that directly inherits from `rx.State` and +does not have other states inheriting from it, to avoid loading unnecessary data. diff --git a/docs/state_structure/shared_state.md b/docs/state_structure/shared_state.md new file mode 100644 index 00000000000..4d8d067b8e7 --- /dev/null +++ b/docs/state_structure/shared_state.md @@ -0,0 +1,215 @@ +```python exec +import reflex as rx +``` + +# Shared State + +_New in version 0.8.23_. + +Defining a subclass of `rx.SharedState` creates a special type of state that may be shared by multiple clients. Shared State is useful for creating real-time collaborative applications where multiple users need to see and interact with the same data simultaneously. + +## Using SharedState + +An `rx.SharedState` subclass behaves similarly to a normal `rx.State` subclass and will be private to each client until it is explicitly linked to a given token. Once linked, any changes made to the Shared State by one client will be propagated to all other clients sharing the same token. + +```md alert info +# What should be used as a token? + +A token can be any string that uniquely identifies a group of clients that should share the same state. Common choices include room IDs, document IDs, or user group IDs. Ensure that the token is securely generated and managed to prevent unauthorized access to shared state. +``` + +```md alert warning +# Linked token cannot contain underscore (\_) characters. + +Underscore characters are currently used as an internal delimiter for tokens and will raise an exception if used for linked states. + +This is a temporary restriction and will be removed in a future release. +``` + +### Linking Shared State + +An `rx.SharedState` subclass can be linked to a token using the `_link_to` method, which is async and returns the linked state instance. After linking, subsequent events triggered against the shared state will be executed in the context of the linked state. To unlink from the token, return the result of awaiting the `_unlink` method. + +To try out the collaborative counter example, open this page in a second or third browser tab and click the "Link" button. You should see the count increment in all tabs when you click the "Increment" button in any of them. + +```python demo exec +class CollaborativeCounter(rx.SharedState): + count: int = 0 + + @rx.event + async def toggle_link(self): + if self._linked_to: + return await self._unlink() + else: + linked_state = await self._link_to("shared-global-counter") + linked_state.count += 1 # Increment count on link + + @rx.var + def is_linked(self) -> bool: + return bool(self._linked_to) + +def shared_state_example(): + return rx.vstack( + rx.text(f"Collaborative Count: {CollaborativeCounter.count}"), + rx.cond( + CollaborativeCounter.is_linked, + rx.button("Unlink", on_click=CollaborativeCounter.toggle_link), + rx.button("Link", on_click=CollaborativeCounter.toggle_link), + ), + rx.button("Increment", on_click=CollaborativeCounter.set_count(CollaborativeCounter.count + 1)), + ) +``` + +```md alert info +# Computed vars may reference SharedState + +Computed vars in other states may reference shared state data using `get_state`, just like private states. This allows private states to provide personalized views of shared data. + +Whenever the shared state is updated, any computed vars depending on it will be re-evaluated in the context of each client's private state. +``` + +### Identifying Clients + +Each client linked to a shared state can be uniquely identified by their `self.router.session.client_token`. Shared state events should _never_ rely on identifiers passed in as parameters, as these can be spoofed from the client. Instead, always use the `client_token` to identify the client triggering the event. + +```python demo exec +import uuid + +class SharedRoom(rx.SharedState): + shared_room: str = rx.LocalStorage() + _users: dict[str, str] = {} + + @rx.var + def user_list(self) -> str: + return ", ".join(self._users.values()) + + @rx.event + async def join(self, username: str): + if not self.shared_room: + self.shared_room = f"shared-room-{uuid.uuid4()}" + linked_state = await self._link_to(self.shared_room) + linked_state._users[self.router.session.client_token] = username + + @rx.event + async def leave(self): + if self._linked_to: + return await self._unlink() + + +class PrivateState(rx.State): + @rx.event + def handle_submit(self, form_data: dict): + return SharedRoom.join(form_data["username"]) + + @rx.var + async def user_in_room(self) -> bool: + shared_state = await self.get_state(SharedRoom) + return self.router.session.client_token in shared_state._users + + +def shared_room_example(): + return rx.vstack( + rx.text("Shared Room"), + rx.text(f"Users: {SharedRoom.user_list}"), + rx.cond( + PrivateState.user_in_room, + rx.button("Leave Room", on_click=SharedRoom.leave), + rx.form( + rx.input(placeholder="Enter your name", name="username"), + rx.button("Join Room"), + on_submit=PrivateState.handle_submit, + ), + ), + ) +``` + +```md alert warning +# Store sensitive data in backend-only vars with an underscore prefix + +Shared State data is synchronized to all linked clients, so avoid storing sensitive information (e.g., client_tokens, user credentials, personal data) in frontend vars, which would expose them to all users and allow them to be modified outside of explicit event handlers. Instead, use backend-only vars (prefixed with an underscore) to keep sensitive data secure on the server side and provide controlled access through event handlers and computed vars. +``` + +### Introspecting Linked Clients + +An `rx.SharedState` subclass has two attributes for determining link status and peers, which are updated during linking and unlinking, and come with some caveats. + +**`_linked_to: str`** + +Provides the token that the state is currently linked to, or empty string if not linked. + +This attribute is only set on the linked state instance returned by `_link_to`. It will be an empty string on any unlinked shared state instances. However, if another state links to a client's private token, then the `_linked_to` attribute will be set to the client's token rather than an empty string. + +When `_linked_to` equals `self.router.session.client_token`, it is assumed that the current client is unlinked, but another client has linked to this client's private state. Although this is possible, it is generally discouraged to link shared states to private client tokens. + +**`_linked_from: set[str]`** + +A set of client tokens that are currently linked to this shared state instance. + +This attribute is only updated during `_link_to` and `_unlink` calls. In situations where unlinking occurs otherwise, such as client disconnects, `self.reset()` is called, or state expires on the backend, `_linked_from` may contain stale client tokens that are no longer linked. These can be cleaned periodically by checking if the tokens still exist in `app.event_namespace.token_to_sid`. + +## Guidelines and Best Practices + +### Keep Shared State Minimal + +When defining a shared state, aim to keep it as minimal as possible. Only include the data and methods that need to be shared between clients. This helps reduce complexity and potential synchronization issues. + +Linked states are always loaded into the tree for each event on each linked client and large states take longer to serialize and transmit over the network. Because linked states are regularly loaded in the context of many clients, they incur higher lock contention, so minimizing loading time also reduces lock waiting time for other clients. + +### Prefer Backend-Only Vars in Shared State + +A shared state should primarily use backend-only vars (prefixed with an underscore) to store shared data. Often, not all users of the shared state need visibility into all of the data in the shared state. Use computed vars to provide sanitized access to shared data as needed. + +```python +from typing import Literal + +class SharedGameState(rx.SharedState): + # Sensitive user metadata stored in backend-only variable. + _players: dict[str, Literal["X", "O"]] = {} + + @rx.event + def make_move(self, x: int, y: int): + # Identify users by client_token, never by arguments passed to the event. + player_token = self.router.session.client_token + player_piece = self._players.get(player_token) +``` + +```md alert warning +# Do Not Trust Event Handler Arguments + +The client can send whatever data it wants to event handlers, so never rely on arguments passed to event handlers for sensitive information such as user identity or permissions. Always use secure identifiers like `self.router.session.client_token` to identify the client triggering the event. +``` + +### Expose Per-User Data via Private States + +If certain data in the shared state needs to be personalized for each user, prefer to expose that data through computed vars defined in private states. This allows each user to have their own view of the shared data without exposing sensitive information to other users. It also reduces the amount of unrelated data sent to each client and improves caching performance by keeping each user's view cached in their own private state, rather than always recomputing the shared state vars for each user that needs to have their information updated. + +Use async computed vars with `get_state` to access shared state data from private states. + +```python +class UserGameState(rx.State): + @rx.var + async def player_piece(self) -> str | None: + shared_state = await self.get_state(SharedGameState) + return shared_state._players.get(self.router.session.client_token) +``` + +### Use Dynamic Routes for Linked Tokens + +It is often convenient to define dynamic routes that include the linked token as part of the URL path. This allows users to easily share links to specific shared state instances. The dynamic route can use `on_load` to link the shared state to the token extracted from the URL. + +```python +class SharedRoom(rx.SharedState): + async def on_load(self): + # `self.room_id` is the automatically defined dynamic route var. + await self._link_to(self.room_id.replace("_", "-") or "default-room") + + +def room_page(): ... + + +app.add_route( + room_page, + path="/room/[room_id]", + on_load=SharedRoom.on_load, +) +``` diff --git a/docs/styling/common-props.md b/docs/styling/common-props.md new file mode 100644 index 00000000000..24d940f1718 --- /dev/null +++ b/docs/styling/common-props.md @@ -0,0 +1,227 @@ +# Style and Layout Props + +```python exec +import reflex as rx +cell_style = {"font_family": "Instrument Sans", "font_style": "normal", "font_weight": "500", "font_size": "14px", "line_height": "1.5", "letter_spacing": "-0.0125em", "color": "var(--c-slate-11)"} +c_color = lambda color, shade: f"var(--c-{color}-{shade})" + +props = { + "align": { + "description": "In a flex, it controls the alignment of items on the cross axis and in a grid layout, it controls the alignment of items on the block axis within their grid area (equivalent to align_items)", + "values": ["stretch", "center", "start", "end", "flex-start", "baseline"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/align-items", + }, + "backdrop_filter": { + "description": "Lets you apply graphical effects such as blurring or color shifting to the area behind an element", + "values": ["url(commonfilters.svg#filter)", "blur(2px)", "hue-rotate(120deg)", "drop-shadow(4px 4px 10px blue)"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter", + }, + "background": { + "description": "Sets all background style properties at once, such as color, image, origin and size, or repeat method (equivalent to bg)", + "values": ["green", "radial-gradient(crimson, skyblue)", "no-repeat url('../lizard.png')"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/background", + }, + "background_color": { + "description": "Sets the background color of an element", + "values": ["brown", "rgb(255, 255, 128)", "#7499ee"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/background-color", + }, + "background_image": { + "description": "Sets one or more background images on an element", + "values": ["url('../lizard.png')", "linear-gradient(#e66465, #9198e5)"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/background-image", + }, + "border": { + "description": "Sets an element's border, which sets the values of border_width, border_style, and border_color.", + "values": ["solid", "dashed red", "thick double #32a1ce", "4mm ridge rgba(211, 220, 50, .6)"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border", + }, + "border_top / border_bottom / border_right / border_left": { + "description": "Sets an element's top / bottom / right / left border. It sets the values of border-(top / bottom / right / left)-width, border-(top / bottom / right / left)-style and border-(top / bottom / right / left)-color", + "values": ["solid", "dashed red", "thick double #32a1ce", "4mm ridge rgba(211, 220, 50, .6)"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-bottom", + }, + "border_color": { + "description": "Sets the color of an element's border (each side can be set individually using border_top_color, border_right_color, border_bottom_color, and border_left_color)", + "values": ["red", "red #32a1ce", "red rgba(170, 50, 220, .6) green", "red yellow green transparent"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-color", + }, + "border_radius": { + "description": "Rounds the corners of an element's outer border edge and you can set a single radius to make circular corners, or two radii to make elliptical corners", + "values": ["30px", "25% 10%", "10% 30% 50% 70%", "10% / 50%"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius", + }, + "border_width": { + "description": "Sets the width of an element's border", + "values": ["thick", "1em", "4px 1.25em", "0 4px 8px 12px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-width", + }, + "box_shadow": { + "description": "Adds shadow effects around an element's frame. You can set multiple effects separated by commas. A box shadow is described by X and Y offsets relative to the element, blur and spread radius, and color", + "values": ["10px 5px 5px red", "60px -16px teal", "12px 12px 2px 1px rgba(0, 0, 255, .2)", "3px 3px red, -1em 0 .4em olive;"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow", + }, + + "color": { + "description": "Sets the foreground color value of an element's text", + "values": ["rebeccapurple", "rgb(255, 255, 128)", "#00a400"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/color", + }, + "display": { + "description": "Sets whether an element is treated as a block or inline box and the layout used for its children, such as flow layout, grid or flex", + "values": ["block", "inline", "inline-block", "flex", "inline-flex", "grid", "inline-grid", "flow-root"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/display", + }, + "flex_grow": { + "description": " Sets the flex grow factor, which specifies how much of the flex container's remaining space should be assigned to the flex item's main size", + "values": ["1", "2", "3"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/flex-grow", + }, + "height": { + "description": "Sets an element's height", + "values": ["150px", "20em", "75%", "auto"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/height", + }, + "justify": { + "description": "Defines how the browser distributes space between and around content items along the main-axis of a flex container, and the inline axis of a grid container (equivalent to justify_content)", + "values": ["start", "center", "flex-start", "space-between", "space-around", "space-evenly", "stretch"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content", + }, + "margin": { + "description": "Sets the margin area (creates extra space around an element) on all four sides of an element", + "values": ["1em", "5% 0", "10px 50px 20px", "10px 50px 20px 0"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/margin", + }, + "margin_x / margin_y": { + "description": "Sets the margin area (creates extra space around an element) along the x-axis / y-axis and a positive value places it farther from its neighbors, while a negative value places it closer", + "values": ["1em", "10%", "10px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/margin", + }, + "margin_top / margin_right / margin_bottom / margin_left ": { + "description": "Sets the margin area (creates extra space around an element) on the top / right / bottom / left of an element", + "values": ["1em", "10%", "10px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/margin-top", + }, + "max_height / min_height": { + "description": "Sets the maximum / minimum height of an element and prevents the used value of the height property from becoming larger / smaller than the value specified for max_height / min_height", + "values": ["150px", "7em", "75%"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/max-height", + }, + "max_width / min_width": { + "description": "Sets the maximum / minimum width of an element and prevents the used value of the width property from becoming larger / smaller than the value specified for max_width / min_width", + "values": ["150px", "20em", "75%"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/max-width", + }, + "padding": { + "description": "Sets the padding area (creates extra space within an element) on all four sides of an element at once", + "values": ["1em", "10px 50px 30px 0", "0", "10px 50px 20px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/padding", + }, + "padding_x / padding_y": { + "description": "Creates extra space within an element along the x-axis / y-axis", + "values": ["1em", "10%", "10px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/padding", + }, + "padding_top / padding_right / padding_bottom / padding_left ": { + "description": "Sets the height of the padding area on the top / right / bottom / left of an element", + "values": ["1em", "10%", "20px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/padding-top", + }, + "position": { + "description": "Sets how an element is positioned in a document and the top, right, bottom, and left properties determine the final location of positioned elements", + "values": ["static", "relative", "absolute", "fixed", "sticky"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/position", + }, + "text_align": { + "description": "Sets the horizontal alignment of the inline-level content inside a block element or table-cell box", + "values": ["start", "end", "center", "justify", "left", "right"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/text-align", + }, + "text_wrap": { + "description": "Controls how text inside an element is wrapped", + "values": ["wrap", "nowrap", "balance", "pretty"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/text-wrap", + }, + "top / bottom / right / left": { + "description": "Sets the vertical / horizontal position of a positioned element. It does not effect non-positioned elements.", + "values": ["0", "4em", "10%", "20px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/top", + }, + "width": { + "description": "Sets an element's width", + "values": ["150px", "20em", "75%", "auto"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/width", + }, + "white_space": { + "description": "Sets how white space inside an element is handled", + "values": ["normal", "nowrap", "pre", "break-spaces"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/white-space", + }, + "word_break": { + "description": "Sets whether line breaks appear wherever the text would otherwise overflow its content box", + "values": ["normal", "break-all", "keep-all", "break-word"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/word-break", + }, + "z_index": { + "description": "Sets the z-order of a positioned element and its descendants or flex and grid items, and overlapping elements with a larger z-index cover those with a smaller one", + "values": ["auto", "1", "5", "200"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/z-index", + }, + + +} + + +def show_props(key, props_dict): + prop_details = props_dict[key] + return rx.table.row( + rx.table.cell( + rx.link( + rx.hstack( + rx.code(key, style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}), + rx.icon("square_arrow_out_up_right", color=c_color("slate", 9), size=15, flex_shrink="0"), + align="center" + ), + href=prop_details["link"], + is_external=True, + ), + justify="start",), + rx.table.cell(prop_details["description"], justify="start", style=cell_style), + rx.table.cell(rx.hstack(*[rx.code(value, style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}) for value in prop_details["values"]], flex_wrap="wrap"), justify="start",), + justify="center", + align="center", + + ) + +``` + +Any [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) prop can be used in a component in Reflex. This is a short list of the most commonly used props. To see all CSS props that can be used check out this [documentation](https://developer.mozilla.org/en-US/docs/Web/CSS). + +Hyphens in CSS property names may be replaced by underscores to use as valid python identifiers, i.e. the CSS prop `z-index` would be used as `z_index` in Reflex. + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell( + "Prop", justify="center" + ), + rx.table.column_header_cell( + "Description", + justify="center", + + ), + rx.table.column_header_cell( + "Potential Values", + justify="center", + ), + ) + ), + rx.table.body( + *[show_props(key, props) for key in props] + ), + width="100%", + padding_x="0", + size="1", +) +``` diff --git a/docs/styling/custom-stylesheets.md b/docs/styling/custom-stylesheets.md new file mode 100644 index 00000000000..de8aecce63d --- /dev/null +++ b/docs/styling/custom-stylesheets.md @@ -0,0 +1,190 @@ +```python exec +import reflex as rx +``` + +# Custom Stylesheets + +Reflex allows you to add custom stylesheets. Simply pass the URLs of the stylesheets to `rx.App`: + +```python +app = rx.App( + stylesheets=[ + "https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css", + ], +) +``` + +## Local Stylesheets + +You can also add local stylesheets. Just put the stylesheet under [`assets/`](/docs/assets/upload_and_download_files) and pass the path to the stylesheet to `rx.App`: + +```python +app = rx.App( + stylesheets=[ + "/styles.css", # This path is relative to assets/ + ], +) +``` + +```md alert warning +# Always use a leading slash (/) when referencing files in the assets directory. + +Without a leading slash the path is considered relative to the current page route and may +not work for routes containing more than one path component, like `/blog/my-cool-post`. +``` + +## Styling with CSS + +You can use CSS variables directly in your Reflex app by passing them alongside the appropriae props. Create a `style.css` file inside the `assets` folder with the following lines: + +```css +:root { + --primary-color: blue; + --accent-color: green; +} +``` + +Then, after referencing the CSS file within the `stylesheets` props of `rx.App`, you can access the CSS props directly like this + +```python +app = rx.App( + theme=rx.theme(appearance="light"), + stylesheets=["/style.css"], +) +app.add_page( + rx.center( + rx.text("CSS Variables!"), + width="100%", + height="100vh", + bg="var(--primary-color)", + ), + "/", +) +``` + +## SASS/SCSS Support + +Reflex supports SASS/SCSS stylesheets alongside regular CSS. This allows you to use more advanced styling features like variables, nesting, mixins, and more. + +### Using SASS/SCSS Files + +To use SASS/SCSS files in your Reflex app: + +1. Create a `.sass` or `.scss` file in your `assets` directory +2. Reference the file in your `rx.App` configuration just like you would with CSS files + +```python +app = rx.App( + stylesheets=[ + "/styles.scss", # This path is relative to assets/ + "/sass/main.sass", # You can organize files in subdirectories + ], +) +``` + +Reflex automatically detects the file extension and compiles these files to CSS using the `libsass` package. + +### Example SASS/SCSS File + +Here's an example of a SASS file (`assets/styles.scss`) that demonstrates some of the features: + +```scss +// Variables +$primary-color: #3498db; +$secondary-color: #2ecc71; +$padding: 16px; + +// Nesting +.container { + background-color: $primary-color; + padding: $padding; + + .button { + background-color: $secondary-color; + padding: $padding / 2; + + &:hover { + opacity: 0.8; + } + } +} + +// Mixins +@mixin flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.centered-box { + @include flex-center; + height: 100px; +} +``` + +### Dependency Requirement + +The `libsass` package is required for SASS/SCSS compilation. If it's not installed, Reflex will show an error message. You can install it with: + +```bash +pip install "libsass>=0.23.0" +``` + +This package is included in the default Reflex installation, so you typically don't need to install it separately. + +## Fonts + +You can take advantage of Reflex's support for custom stylesheets to add custom fonts to your app. + +In this example, we will use the [JetBrains Mono]({"https://fonts.google.com/specimen/JetBrains+Mono"}) font from Google Fonts. First, add the stylesheet with the font to your app. You can get this link from the "Get embed code" section of the Google font page. + +```python +app = rx.App( + stylesheets=[ + "https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap", + ], +) +``` + +Then you can use the font in your component by setting the `font_family` prop. + +```python demo +rx.text( + "Check out my font", + font_family="JetBrains Mono", + font_size="1.5em", +) +``` + +## Local Fonts + +By making use of the two previous points, we can also make a stylesheet that allow you to use a font hosted on your server. + +If your font is called `MyFont.otf`, copy it in `assets/fonts`. + +Now we have the font ready, let's create the stylesheet `myfont.css`. + +```css +@font-face { + font-family: MyFont; + src: url("/fonts/MyFont.otf") format("opentype"); +} + +@font-face { + font-family: MyFont; + font-weight: bold; + src: url("/fonts/MyFont.otf") format("opentype"); +} +``` + +Add the reference to your new Stylesheet in your App. + +```python +app = rx.App( + stylesheets=[ + "/fonts/myfont.css", # This path is relative to assets/ + ], +) +``` + +And that's it! You can now use `MyFont` like any other FontFamily to style your components. diff --git a/docs/styling/layout.md b/docs/styling/layout.md new file mode 100644 index 00000000000..e3e29f2c302 --- /dev/null +++ b/docs/styling/layout.md @@ -0,0 +1,152 @@ +```python exec +import reflex as rx +``` + +# Layout Components + +Layout components such as `rx.flex`, `rx.container`, `rx.box`, etc. are used to organize and structure the visual presentation of your application. This page gives a breakdown of when and how each of these components might be used. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=3311&end=3853 +# Video: Example of Laying Out the Main Content of a Page +``` + +## Box + +`rx.box` is a generic component that can apply any CSS style to its children. It's a building block that can be used to apply a specific layout or style property. + +**When to use:** Use `rx.box` when you need to apply specific styles or constraints to a part of your interface. + +```python demo +rx.box( + rx.box( + "CSS color", + background_color="red", + border_radius="2px", + width="50%", + margin="4px", + padding="4px", + ), + rx.box( + "Radix Color", + background_color=rx.color("tomato", 3), + border_radius="5px", + width="80%", + margin="12px", + padding="12px", + ), + text_align="center", + width="100%", +) +``` + +## Stack + +`rx.stack` is a layout component that arranges its children in a single column or row, depending on the direction. It’s useful for consistent spacing between elements. + +**When to use:** Use `rx.stack` when you need to lay out a series of components either vertically or horizontally with equal spacing. + +```python demo +rx.flex( + rx.stack( + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="20%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="30%", + ), + flex_direction="row", + width="100%", + ), + rx.stack( + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="20%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="30%", + ), + flex_direction="column", + width="100%", + ), + width="100%", +) +``` + +## Flex + +The `rx.flex` component is used to create a flexible box layout, inspired by [CSS Flexbox](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox). It's ideal for designing a layout where the size of the items can grow and shrink dynamically based on the available space. + +**When to use:** Use `rx.flex` when you need a responsive layout that adjusts the size and position of child components dynamically. + +```python demo +rx.flex( + rx.card("Card 1"), + rx.card("Card 2"), + rx.card("Card 3"), + spacing="2", + width="100%", +) +``` + +## Grid + +`rx.grid` components are used to create complex responsive layouts based on a grid system, similar to [CSS Grid Layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout). + +**When to use:** Use `rx.grid` when dealing with complex layouts that require rows and columns, especially when alignment and spacing among multiple axes are needed. + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card(f"Card {i + 1}", height="10vh"), + ), + columns="3", + spacing="4", + width="100%", +) +``` + +## Container + +The `rx.container` component typically provides padding and fixes the maximum width of the content inside it, often used to center content on large screens. + +**When to use:** Use `rx.container` for wrapping your application’s content in a centered block with some padding. + +```python demo +rx.box( + rx.container( + rx.card( + "This content is constrained to a max width of 448px.", + width="100%", + ), + size="1", + ), + rx.container( + rx.card( + "This content is constrained to a max width of 688px.", + width="100%", + ), + size="2", + ), + rx.container( + rx.card( + "This content is constrained to a max width of 880px.", + width="100%", + ), + size="3", + ), + background_color="var(--gray-3)", + width="100%", +) +``` diff --git a/docs/styling/overview.md b/docs/styling/overview.md new file mode 100644 index 00000000000..960ad3d169f --- /dev/null +++ b/docs/styling/overview.md @@ -0,0 +1,184 @@ +```python exec +import reflex as rx +``` + +# Styling + +Reflex components can be styled using the full power of [CSS]({"https://www.w3schools.com/css/"}). + +There are three main ways to add style to your app and they take precedence in the following order: + +1. **Inline:** Styles applied to a single component instance. +2. **Component:** Styles applied to components of a specific type. +3. **Global:** Styles applied to all components. + +```md alert success +# Style keys can be any valid CSS property name. + +To be consistent with Python standards, you can specify keys in `snake_case`. +``` + +## Global Styles + +You can pass a style dictionary to your app to apply base styles to all components. + +For example, you can set the default font family and font size for your app here just once rather than having to set it on every component. + +```python +style = { + "font_family": "Comic Sans MS", + "font_size": "16px", +} + +app = rx.App(style=style) +``` + +## Component Styles + +In your style dictionary, you can also specify default styles for specific component types or arbitrary CSS classes and IDs. + +```python +style = { + # Set the selection highlight color globally. + "::selection": { + "background_color": accent_color, + }, + # Apply global css class styles. + ".some-css-class": { + "text_decoration": "underline", + }, + # Apply global css id styles. + "#special-input": \{"width": "20vw"}, + # Apply styles to specific components. + rx.text: { + "font_family": "Comic Sans MS", + }, + rx.divider: { + "margin_bottom": "1em", + "margin_top": "0.5em", + }, + rx.heading: { + "font_weight": "500", + }, + rx.code: { + "color": "green", + }, +} + +app = rx.App(style=style) +``` + +Using style dictionaries like this, you can easily create a consistent theme for your app. + +```md alert warning +# Watch out for underscores in class names and IDs + +Reflex automatically converts `snake_case` identifiers into `camelCase` format when applying styles. To ensure consistency, it is recommended to use the dash character or camelCase identifiers in your own class names and IDs. To style third-party libraries relying on underscore class names, an external stylesheet should be used. See [custom stylesheets](/docs/styling/custom-stylesheets) for how to reference external CSS files. +``` + +## Inline Styles + +Inline styles apply to a single component instance. They are passed in as regular props to the component. + +```python demo +rx.text( + "Hello World", + background_image="linear-gradient(271.68deg, #EE756A 0.75%, #756AEE 88.52%)", + background_clip="text", + font_weight="bold", + font_size="2em", +) +``` + +Children components inherit inline styles unless they are overridden by their own inline styles. + +```python demo +rx.box( + rx.hstack( + rx.button("Default Button"), + rx.button("Red Button", color="red"), + ), + color="blue", +) +``` + +### Style Prop + +Inline styles can also be set with a `style` prop. This is useful for reusing styles between multiple components. + +```python exec +text_style = { + "color": "green", + "font_family": "Comic Sans MS", + "font_size": "1.2em", + "font_weight": "bold", + "box_shadow": "rgba(240, 46, 170, 0.4) 5px 5px, rgba(240, 46, 170, 0.3) 10px 10px", +} +``` + +```python +text_style={text_style} +``` + +```python demo +rx.vstack( + rx.text("Hello", style=text_style), + rx.text("World", style=text_style), +) +``` + +```python exec +style1 = { + "color": "green", + "font_family": "Comic Sans MS", + "border_radius": "10px", + "background_color": "rgb(107,99,246)", +} +style2 = { + "color": "white", + "border": "5px solid #EE756A", + "padding": "10px", +} +``` + +```python +style1={style1} +style2={style2} +``` + +```python demo +rx.box( + "Multiple Styles", + style=[style1, style2], +) +``` + +The style dictionaries are applied in the order they are passed in. This means that styles defined later will override styles defined earlier. + +## Theming + +As of Reflex 'v0.4.0', you can now theme your Reflex web apps. To learn more checkout the [Theme docs](/docs/styling/theming). + +The `Theme` component is used to change the theme of the application. The `Theme` can be set directly in your rx.App. + +```python +app = rx.App( + theme=rx.theme( + appearance="light", has_background=True, radius="large", accent_color="teal" + ) +) +``` + +Additionally you can modify the theme of your app through using the `Theme Panel` component which can be found in the [Theme Panel docs](/docs/library/other/theme). + +## Special Styles + +We support all of Chakra UI's [pseudo styles]({"https://v2.chakra-ui.com/docs/styled-system/style-props#pseudo"}). + +Below is an example of text that changes color when you hover over it. + +```python demo +rx.box( + rx.text("Hover Me", _hover={"color": "red"}), +) +``` diff --git a/docs/styling/responsive.md b/docs/styling/responsive.md new file mode 100644 index 00000000000..feb6cd1d97b --- /dev/null +++ b/docs/styling/responsive.md @@ -0,0 +1,172 @@ +```python exec +import reflex as rx +cell_style = {"font_family": "Instrument Sans", "font_style": "normal", "font_weight": "500", "font_size": "14px", "line_height": "1.5", "letter_spacing": "-0.0125em", "color": "var(--c-slate-11)"} +``` + +# Responsive + +Reflex apps can be made responsive to look good on mobile, tablet, and desktop. + +You can pass a list of values to any style property to specify its value on different screen sizes. + +```python demo +rx.text( + "Hello World", + color=["orange", "red", "purple", "blue", "green"], +) +``` + +The text will change color based on your screen size. If you are on desktop, try changing the size of your browser window to see the color change. + +_New in 0.5.6_ + +Responsive values can also be specified using `rx.breakpoints`. Each size maps to a corresponding key, the value of which will be applied when the screen size is greater than or equal to the named breakpoint. + +```python demo +rx.text( + "Hello World", + color=rx.breakpoints( + initial="orange", + sm="purple", + lg="green", + ), +) +``` + +Custom breakpoints in CSS units can be mapped to values per component using a dictionary instead of named parameters. + +```python +rx.text( + "Hello World", + color=rx.breakpoints({ + "0px": "orange", + "48em": "purple", + "80em": "green", + }), +) +``` + +For the Radix UI components' fields that supports responsive value, you can also use `rx.breakpoints` (note that custom breakpoints and list syntax aren't supported). + +```python demo +rx.grid( + rx.foreach( + list(range(6)), + lambda _: rx.box(bg_color="#a7db76", height="100px", width="100px") + ), + columns=rx.breakpoints( + initial="2", + sm="4", + lg="6" + ), + spacing="4" +) +``` + +## Setting Defaults + +The default breakpoints are shown below. + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Size"), + rx.table.column_header_cell("Width"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell(rx.code("initial", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)})), + rx.table.cell("0px", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("xs", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)})), + rx.table.cell("30em", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("sm", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)})), + rx.table.cell("48em", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("md", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)})), + rx.table.cell("62em", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("lg", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)})), + rx.table.cell("80em", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("xl", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)})), + rx.table.cell("96em", style=cell_style), + ), + ), + margin_bottom="1em", +) +``` + +You can customize them using the style property. + +```python +app = rx.App(style=\{"breakpoints": ["520px", "768px", "1024px", "1280px", "1640px"]\}) +``` + +## Showing Components Based on Display + +A common use case for responsive is to show different components based on the screen size. + +Reflex provides useful helper components for this. + +```python demo +rx.vstack( + rx.desktop_only( + rx.text("Desktop View"), + ), + rx.tablet_only( + rx.text("Tablet View"), + ), + rx.mobile_only( + rx.text("Mobile View"), + ), + rx.mobile_and_tablet( + rx.text("Visible on Mobile and Tablet"), + ), + rx.tablet_and_desktop( + rx.text("Visible on Desktop and Tablet"), + ), +) +``` + +## Specifying Display Breakpoints + +You can specify the breakpoints to use for the responsive components by using the `display` style property. + +```python demo +rx.vstack( + rx.text( + "Hello World", + color="green", + display=["none", "none", "none", "none", "flex"], + ), + rx.text( + "Hello World", + color="blue", + display=["none", "none", "none", "flex", "flex"], + ), + rx.text( + "Hello World", + color="red", + display=["none", "none", "flex", "flex", "flex"], + ), + rx.text( + "Hello World", + color="orange", + display=["none", "flex", "flex", "flex", "flex"], + ), + rx.text( + "Hello World", + color="yellow", + display=["flex", "flex", "flex", "flex", "flex"], + ), +) +``` diff --git a/docs/styling/tailwind.md b/docs/styling/tailwind.md new file mode 100644 index 00000000000..0839b840ebc --- /dev/null +++ b/docs/styling/tailwind.md @@ -0,0 +1,258 @@ +```python exec +import reflex as rx + +``` + +# Tailwind + +Reflex supports [Tailwind CSS]({"https://tailwindcss.com/"}) through a plugin system that provides better control and supports multiple Tailwind versions. + +## Plugin-Based Configuration + +The recommended way to use Tailwind CSS is through the plugin system: + +```python +import reflex as rx + +config = rx.Config( + app_name="myapp", + plugins=[ + rx.plugins.TailwindV4Plugin(), + ], +) +``` + +You can customize the Tailwind configuration by passing a config dictionary to the plugin: + +```python +import reflex as rx + +tailwind_config = { + "plugins": ["@tailwindcss/typography"], + "theme": { + "extend": { + "colors": { + "primary": "#3b82f6", + "secondary": "#64748b", + } + } + }, +} + +config = rx.Config( + app_name="myapp", + plugins=[ + rx.plugins.TailwindV4Plugin(tailwind_config), + ], +) +``` + +```````md alert info +## Migration from Legacy Configuration + +If you're currently using the legacy `tailwind` configuration parameter, you should migrate to using the plugin system: + +**Old approach (legacy):** + +```python +config = rx.Config( + app_name="my_app", + tailwind={ + "plugins": ["@tailwindcss/typography"], + "theme": {"extend": {"colors": {"primary": "#3b82f6"}}}, + }, +) +``` +``````` + +**New approach (plugin-based):** + +```python +tailwind_config = { + "plugins": ["@tailwindcss/typography"], + "theme": {"extend": {"colors": {"primary": "#3b82f6"}}}, +} + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV4Plugin(tailwind_config), + ], +) +``` + +```` + +### Choosing Between Tailwind Versions + +Reflex supports both Tailwind CSS v3 and v4: + +- **TailwindV4Plugin**: The recommended choice for new projects. Includes the latest features and performance improvements and is used by default in new Reflex templates. +- **TailwindV3Plugin**: Still supported for existing projects. Use this if you need compatibility with older Tailwind configurations. + +```python +# For Tailwind CSS v4 (recommended for new projects) +config = rx.Config( + app_name="myapp", + plugins=[rx.plugins.TailwindV4Plugin()], +) + +# For Tailwind CSS v3 (existing projects) +config = rx.Config( + app_name="myapp", + plugins=[rx.plugins.TailwindV3Plugin()], +) +```` + +All Tailwind configuration options are supported. + +You can use any of the [utility classes]({"https://tailwindcss.com/docs/utility-first"}) under the `class_name` prop: + +```python demo +rx.box( + "Hello World", + class_name="text-4xl text-center text-blue-500", +) +``` + +## Disabling Tailwind + +To disable Tailwind in your project, simply don't include any Tailwind plugins in your configuration. This will prevent Tailwind styles from being applied to your application. + +## Custom theme + +You can integrate custom Tailwind themes within your Reflex app as well. The setup process is similar to the CSS Styling method mentioned above, with only a few minor variations. + +Begin by creating a CSS file inside your `assets` folder. Inside the CSS file, include the following Tailwind directives: + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: blue; + --foreground: green; +} + +.dark { + --background: darkblue; + --foreground: lightgreen; +} +``` + +We define a couple of custom CSS variables (`--background` and `--foreground`) that will be used throughout your app for styling. These variables can be dynamically updated based on the theme. + +Tailwind defaults to light mode, but to handle dark mode, you can define a separate set of CSS variables under the `.dark` class. + +Tailwind Directives (`@tailwind base`, `@tailwind components`, `@tailwind utilities`): These are essential Tailwind CSS imports that enable the default base styles, components, and utility classes. + +Next, you'll need to configure Tailwind in your `rxconfig.py` file to ensure that the Reflex app uses your custom Tailwind setup. + +```python +import reflex as rx + +tailwind_config = { + "plugins": ["@tailwindcss/typography"], + "theme": { + "extend": { + "colors": { + "background": "var(--background)", + "foreground": "var(--foreground)" + }, + } + }, +} + +config = rx.Config( + app_name="app", + plugins=[ + rx.plugins.TailwindV4Plugin(tailwind_config), + ], +) +``` + +In the theme section, we're extending the default Tailwind theme to include custom colors. Specifically, we're referencing the CSS variables (`--background` and `--foreground`) that were defined earlier in your CSS file. + +The `rx.Config` object is used to initialize and configure your Reflex app. Here, we're passing the `tailwind_config` dictionary to ensure Tailwind's custom setup is applied to the app. + +Finally, to apply your custom styles and Tailwind configuration, you need to reference the CSS file you created in your `assets` folder inside the `rx.App` setup. This will allow you to use the custom properties (variables) directly within your Tailwind classes. + +In your `app.py` (or main application file), make the following changes: + +```python +app = rx.App( + theme=rx.theme(appearance="light"), + stylesheets=["/style.css"], +) +app.add_page( + rx.center( + rx.text("Tailwind & Reflex!"), + class_name="bg-background w-full h-[100vh]", + ), + "/", +) +``` + +The `bg-background` class uses the `--background` variable (defined in the CSS file), which will be applied as the background color. + +## Dynamic Styling + +You can style a component based of a condition using `rx.cond` or `rx.match`. + +```python demo exec +class TailwindState(rx.State): + active = False + + @rx.event + def toggle_active(self): + self.active = not self.active + +def tailwind_demo(): + return rx.el.div( + rx.el.button( + "Click me", + on_click=TailwindState.toggle_active, + class_name=( + "px-4 py-2 text-white rounded-md", + rx.cond( + TailwindState.active, + "bg-red-500", + "bg-blue-500", + ), + ), + ), + ) +``` + +## Using Tailwind Classes from the State + +When using Tailwind with Reflex, it's important to understand that class names must be statically defined in your code for Tailwind to properly compile them. If you dynamically generate class names from state variables or functions at runtime, Tailwind won't be able to detect these classes during the build process, resulting in missing styles in your application. + +For example, this won't work correctly because the class names are defined in the state: + +```python demo exec +class TailwindState(rx.State): + active = False + + @rx.var + def button_class(self) -> str: + return "bg-accent" if self.active else "bg-secondary" + + @rx.event + def toggle_active(self): + self.active = not self.active + +def tailwind_demo(): + return rx.el.button( + f"Click me: {TailwindState.active}", + class_name=TailwindState.button_class, + on_click=TailwindState.toggle_active, + ) +``` + +## Using Tailwind with Reflex Core Components + +Reflex core components are built on Radix Themes, which means they come with pre-defined styling. When you apply Tailwind classes to these components, you may encounter styling conflicts or unexpected behavior as the Tailwind styles compete with the built-in Radix styles. + +For the best experience when using Tailwind CSS in your Reflex application, we recommend using the lower-level `rx.el` components. These components don't have pre-applied styles, giving you complete control over styling with Tailwind classes without any conflicts. Check the list of HTML components [here](/docs/library/other/html). diff --git a/docs/styling/theming.md b/docs/styling/theming.md new file mode 100644 index 00000000000..41fdf36fd38 --- /dev/null +++ b/docs/styling/theming.md @@ -0,0 +1,202 @@ +```python exec +import reflex as rx +cell_style = {"font_family": "Instrument Sans", "font_style": "normal", "font_weight": "500", "font_size": "14px", "line_height": "1.5", "letter_spacing": "-0.0125em", "color": "var(--c-slate-11)"} +``` + +# Theming + +As of Reflex `v0.4.0`, you can now theme your Reflex applications. The core of our theming system is directly based on the [Radix Themes](https://www.radix-ui.com) library. This allows you to easily change the theme of your application along with providing a default light and dark theme. Themes cause all the components to have a unified color appearance. + +## Overview + +The `Theme` component is used to change the theme of the application. The `Theme` can be set directly in your rx.App. + +```python +app = rx.App( + theme=rx.theme( + appearance="light", has_background=True, radius="large", accent_color="teal" + ) +) +``` + +Here are the props that can be passed to the `rx.theme` component: + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name", class_name="table-header"), + rx.table.column_header_cell("Type", class_name="table-header"), + rx.table.column_header_cell("Description", class_name="table-header"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(rx.code("has_background", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.code("Bool", style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)}, class_name="code-style")), + rx.table.cell("Whether to apply the themes background color to the theme node. Defaults to True.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("appearance", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.code('"inherit" | "light" | "dark"', style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)}, class_name="code-style")), + rx.table.cell("The appearance of the theme. Can be 'light' or 'dark'. Defaults to 'light'.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("accent_color", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.code("Str", style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)}, class_name="code-style")), + rx.table.cell("The primary color used for default buttons, typography, backgrounds, etc.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("gray_color", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.code("Str", style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)}, class_name="code-style")), + rx.table.cell("The secondary color used for default buttons, typography, backgrounds, etc.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("panel_background", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.code('"solid" | "translucent"', style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)}, class_name="code-style")), + rx.table.cell('Whether panel backgrounds are translucent: "solid" | "translucent" (default).', style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("radius", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.code('"none" | "small" | "medium" | "large" | "full"', style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)})), + rx.table.cell("The radius of the theme. Can be 'small', 'medium', or 'large'. Defaults to 'medium'.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("scaling", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.code('"90%" | "95%" | "100%" | "105%" | "110%"', style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)}, class_name="code-style")), + rx.table.cell("Scale of all theme items.", style=cell_style), + ), + ), + variant="surface", + margin_y="1em", +) + +``` + +Additionally you can modify the theme of your app through using the `Theme Panel` component which can be found in the [Theme Panel docs](/docs/library/other/theme). + +## Colors + +### Color Scheme + +On a high-level, component `color_scheme` inherits from the color specified in the theme. This means that if you change the theme, the color of the component will also change. Available colors can be found [here](https://www.radix-ui.com/colors). + +You can also specify the `color_scheme` prop. + +```python demo +rx.flex( + rx.button( + "Hello World", + color_scheme="tomato", + ), + rx.button( + "Hello World", + color_scheme="teal", + ), + spacing="2" +) +``` + +### Shades + +Sometime you may want to use a specific shade of a color from the theme. This is recommended vs using a hex color directly as it will automatically change when the theme changes appearance change from light/dark. + +To access a specific shade of color from the theme, you can use the `rx.color`. When switching to light and dark themes, the color will automatically change. Shades can be accessed by using the color name and the shade number. The shade number ranges from 1 to 12. Additionally, they can have their alpha value set by using the `True` parameter it defaults to `False`. A full list of colors can be found [here](https://www.radix-ui.com/colors). + +```python demo +rx.flex( + rx.button( + "Hello World", + color=rx.color("grass", 1), + background_color=rx.color("grass", 7), + border_color=f"1px solid {rx.color('grass', 1)}", + ), + spacing="2" +) +``` + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Type"), + rx.table.column_header_cell("Description"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(rx.code("color", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.code("Str", style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)}, class_name="code-style")), + rx.table.cell("The color to use. Can be any valid accent color or 'accent' to reference the current theme color.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("shade", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.link(rx.code('1 - 12', style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)}, class_name="code-style"), href="https://www.radix-ui.com/colors")), + rx.table.cell("The shade of the color to use. Defaults to 7.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("alpha", style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)}, class_name="code-style")), + rx.table.cell(rx.code("Bool", style={"color": rx.color("gray", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('gray', 5)}", "background": rx.color("gray", 3)}, class_name="code-style")), + rx.table.cell("Whether to use the alpha value of the color. Defaults to False.", style=cell_style), + ) + ), + variant="surface", + margin_y="1em", +) + +``` + +### Regular Colors + +You can also use standard hex, rgb, and rgba colors. + +```python demo +rx.flex( + rx.button( + "Hello World", + color="white", + background_color="#87CEFA", + border="1px solid rgb(176,196,222)", + ), + spacing="2" +) +``` + +## Toggle Appearance + +To toggle between the light and dark mode manually, you can use the `toggle_color_mode` with the desired event trigger of your choice. + +```python + +from reflex.style import toggle_color_mode + + + +def index(): + return rx.button( + "Toggle Color Mode", + on_click=toggle_color_mode, + ) +``` + +## Appearance Conditional Rendering + +To render a different component depending on whether the app is in `light` mode or `dark` mode, you can use the `rx.color_mode_cond` component. The first component will be rendered if the app is in `light` mode and the second component will be rendered if the app is in `dark` mode. + +```python demo +rx.color_mode_cond( + light=rx.image(src="https://web.reflex-assets.dev/logos/light/reflex.svg", alt="Reflex Logo light", height="4em"), + dark=rx.image(src="https://web.reflex-assets.dev/logos/dark/reflex.svg", alt="Reflex Logo dark", height="4em"), +) +``` + +This can also be applied to props. + +```python demo +rx.button( + "Hello World", + color=rx.color_mode_cond(light="black", dark="white"), + background_color=rx.color_mode_cond(light="white", dark="black"), +) +``` diff --git a/docs/tr/README.md b/docs/tr/README.md deleted file mode 100644 index be888117332..00000000000 --- a/docs/tr/README.md +++ /dev/null @@ -1,248 +0,0 @@ -
-Reflex Logo -
- -### **✨ Saf Python'da performanslı, özelleştirilebilir web uygulamaları. Saniyeler içinde dağıtın. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex, saf Python'da tam yığın web uygulamaları oluşturmak için bir kütüphanedir. - -Temel özellikler: - -- **Saf Python** - Uygulamanızın ön uç ve arka uç kısımlarının tamamını Python'da yazın, Javascript öğrenmenize gerek yok. -- **Tam Esneklik** - Reflex ile başlamak kolaydır, ancak karmaşık uygulamalara da ölçeklenebilir. -- **Anında Dağıtım** - Oluşturduktan sonra, uygulamanızı [tek bir komutla](https://reflex.dev/docs/hosting/deploy-quick-start/) dağıtın veya kendi sunucunuzda barındırın. - -Reflex'in perde arkasında nasıl çalıştığını öğrenmek için [mimari sayfamıza](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) göz atın. - -## ⚙️ Kurulum - -Bir terminal açın ve çalıştırın (Python 3.10+ gerekir): - -```bash -pip install reflex -``` - -## 🥳 İlk Uygulamanı Oluştur - -`reflex`'i kurduğunuzda `reflex` komut satırı aracınıda kurmuş olursunuz. - -Kurulumun başarılı olduğunu test etmek için yeni bir proje oluşturun. (`my_app_name`'i proje ismiyle değiştirin.): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -Bu komut ile birlikte yeni oluşturduğunuz dizinde bir şablon uygulaması oluşturur. - -Uygulamanızı geliştirme modunda başlatabilirsiniz: - -```bash -reflex run -``` - -Uygulamanızın http://localhost:3000 adresinde çalıştığını görmelisiniz. - -Şimdi `my_app_name/my_app_name.py` yolundaki kaynak kodu düzenleyebilirsiniz. Reflex'in hızlı yenileme özelliği vardır, böylece kodunuzu kaydettiğinizde değişikliklerinizi anında görebilirsiniz. - -## 🫧 Örnek Uygulama - -Bir örnek üzerinden gidelim: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node) kullanarak bir görüntü oluşturma arayüzü oluşturalım. Basit olması açısından, yalnızca [OpenAI API](https://platform.openai.com/docs/api-reference/authentication)'ını kullanıyoruz, ancak bunu yerel olarak çalıştırılan bir ML modeliyle değiştirebilirsiniz. - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -İşte bunu oluşturmak için kodun tamamı. Her şey sadece bir Python dosyasıyla hazırlandı! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """Uygulama durumu.""" - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Prompt'tan görüntüyü alın.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Sayfa ve durumu uygulamaya ekleyin. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Daha Detaylı İceleyelim - -
-DALL-E uygulamasının arka uç ve ön uç kısımları arasındaki farkları açıklama. -
- -### **Reflex UI** - -UI (Kullanıcı Arayüzü) ile başlayalım. - -```python -def index(): - return rx.center( - ... - ) -``` - -Bu `index` fonkisyonu uygulamanın frontend'ini tanımlar. - -Frontend'i oluşturmak için `center`, `vstack`, `input`, ve `button` gibi farklı bileşenler kullanıyoruz. Karmaşık düzenler oluşturmak için bileşenleri birbirinin içine yerleştirilebiliriz. Ayrıca bunları CSS'nin tüm gücüyle şekillendirmek için anahtar kelime argümanları kullanabilirsiniz. - -Reflex, işinizi kolaylaştırmak için [60'tan fazla dahili bileşen](https://reflex.dev/docs/library) içerir. Aktif olarak yeni bileşen ekliyoruz ve [kendi bileşenlerinizi oluşturmak](https://reflex.dev/docs/wrapping-react/overview/) oldukça kolay. - -### **Durum (State)** - -Reflex arayüzünüzü durumunuzun bir fonksiyonu olarak temsil eder. - -```python -class State(rx.State): - """Uygulama durumu.""" - prompt = "" - image_url = "" - processing = False - complete = False -``` - -Durum (State), bir uygulamadaki değişebilen tüm değişkenleri (vars olarak adlandırılır) ve bunları değiştiren fonksiyonları tanımlar. - -Burada durum `prompt` ve `image_url`inden oluşur. Ayrıca düğmenin ne zaman devre dışı bırakılacağını (görüntü oluşturma sırasında) ve görüntünün ne zaman gösterileceğini belirtmek için `processing` ve `complete` booleanları da vardır. - -### **Olay İşleyicileri (Event Handlers)** - -```python -def get_image(self): - """Prompt'tan görüntüyü alın.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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 -``` - -Durum içinde, durum değişkenlerini değiştiren olay işleyicileri adı verilen fonksiyonları tanımlarız. Olay işleyicileri, Reflex'te durumu değiştirebilmemizi sağlar. Bir düğmeye tıklamak veya bir metin kutusuna yazı yazmak gibi kullanıcı eylemlerine yanıt olarak çağrılabilirler. Bu eylemlere olay denir. - -DALL·E uygulamamız OpenAI API'ından bu görüntüyü almak için `get_image` adlı bir olay işleyicisine sahiptir. Bir olay işleyicisinin ortasında `yield`ın kullanılması UI'ın güncellenmesini sağlar. Aksi takdirde UI olay işleyicisinin sonunda güncellenecektir. - -### **Yönlendirme (Routing)** - -En sonunda uygulamamızı tanımlıyoruz. - -```python -app = rx.App() -``` - -Uygulamamızın kök dizinine index bileşeninden bir sayfa ekliyoruz. Ayrıca sayfa önizlemesinde/tarayıcı sekmesinde görünecek bir başlık ekliyoruz. - -```python -app.add_page(index, title="DALL-E") -``` - -Daha fazla sayfa ekleyerek çok sayfalı bir uygulama oluşturabilirsiniz. - -## 📑 Kaynaklar - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Durum - -Reflex, Aralık 2022'de Pynecone adıyla piyasaya sürüldü. - -2025'in başından itibaren, Reflex uygulamaları için en iyi barındırma deneyimini sunmak amacıyla [Reflex Cloud](https://cloud.reflex.dev) hizmete girmiştir. Bunu geliştirmeye ve daha fazla özellik eklemeye devam edeceğiz. - -Reflex'in her hafta yeni sürümleri ve özellikleri geliyor! Güncel kalmak için bu depoyu :star: yıldızlamayı ve :eyes: izlediğinizden emin olun. - -## Katkı - -Her boyuttaki katkıları memnuniyetle karşılıyoruz! Aşağıda Reflex topluluğuna adım atmanın bazı yolları mevcut. - -- **Discord Kanalımıza Katılın**: [Discord'umuz](https://discord.gg/T5WSbC2YtQ), Reflex projeniz hakkında yardım almak ve nasıl katkıda bulunabileceğinizi tartışmak için en iyi yerdir. -- **GitHub Discussions**: Eklemek istediğiniz özellikler veya kafa karıştırıcı, açıklığa kavuşturulması gereken şeyler hakkında konuşmanın harika bir yolu. -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) hataları bildirmenin mükemmel bir yoludur. Ayrıca mevcut bir sorunu deneyip çözebilir ve bir PR (Pull Requests) gönderebilirsiniz. - -Beceri düzeyiniz veya deneyiminiz ne olursa olsun aktif olarak katkıda bulunacak kişiler arıyoruz. Katkı sağlamak için katkı sağlama rehberimize bakabilirsiniz: [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## Hepsi Katkıda Bulunanlar Sayesinde: - - - - - -## Lisans - -Reflex açık kaynaklıdır ve [Apache License 2.0](/LICENSE) altında lisanslıdır. diff --git a/docs/ui/overview.md b/docs/ui/overview.md new file mode 100644 index 00000000000..54f929a46e2 --- /dev/null +++ b/docs/ui/overview.md @@ -0,0 +1,79 @@ +```python exec +import reflex as rx +``` + +# UI Overview + +Components are the building blocks for your app's user interface (UI). They are the visual elements that make up your app, like buttons, text, and images. + +## Component Basics + +Components are made up of children and props. + +```md definition +# Children + +- Text or other Reflex components nested inside a component. +- Passed as **positional arguments**. + +# Props + +- Attributes that affect the behavior and appearance of a component. +- Passed as **keyword arguments**. +``` + +Let's take a look at the `rx.text` component. + +```python demo +rx.text('Hello World!', color='blue', font_size="1.5em") +``` + +Here `"Hello World!"` is the child text to display, while `color` and `font_size` are props that modify the appearance of the text. + +```md alert success +# Regular Python data types can be passed in as children to components. This is useful for passing in text, numbers, and other simple data types. +``` + +## Another Example + +Now let's take a look at a more complex component, which has other components nested inside it. The `rx.vstack` component is a container that arranges its children vertically with space between them. + +```python demo +rx.vstack( + rx.heading("Sample Form"), + rx.input(placeholder="Name"), + rx.checkbox("Subscribe to Newsletter"), +) +``` + +Some props are specific to a component. For example, the `header` and `content` props of the `rx.accordion.item` component show the heading and accordion content details of the accordion respectively. + +Styling props like `color` are shared across many components. + +```md alert info +# You can find all the props for a component by checking its documentation page in the [component library](/docs/library). +``` + +## Pages + +Reflex apps are organized into pages, each of which maps to a different URL. + +Pages are defined as functions that return a component. By default, the function name will be used as the path, but you can also specify a route explicitly. + +```python +def index(): + return rx.text('Root Page') + + +def about(): + return rx.text('About Page') + + +app = rx.App() +app.add_page(index, route="/") +app.add_page(about, route="/about") +``` + +In this example we add a page called `index` at the root route. +If you `reflex run` the app, you will see the `index` page at `http://localhost:3000`. +Similarly, the `about` page will be available at `http://localhost:3000/about`. diff --git a/docs/utility_methods/exception_handlers.md b/docs/utility_methods/exception_handlers.md new file mode 100644 index 00000000000..b0b62f2f532 --- /dev/null +++ b/docs/utility_methods/exception_handlers.md @@ -0,0 +1,43 @@ +# Exception handlers + +_Added in v0.5.7_ + +Exceptions handlers are functions that can be assigned to your app to handle exceptions that occur during the application runtime. +They are useful for customizing the response when an error occurs, logging errors, and performing cleanup tasks. + +## Types + +Reflex support two type of exception handlers `frontend_exception_handler` and `backend_exception_handler`. + +They are used to handle exceptions that occur in the `frontend` and `backend` respectively. + +The `frontend` errors are coming from the JavaScript side of the application, while `backend` errors are coming from the the event handlers on the Python side. + +## Register an Exception Handler + +To register an exception handler, assign it to `app.frontend_exception_handler` or `app.backend_exception_handler` to assign a function that will handle the exception. + +The expected signature for an error handler is `def handler(exception: Exception)`. + +```md alert warning +# Only named functions are supported as exception handler. +``` + +## Examples + +```python +import reflex as rx + +def custom_frontend_handler(exception: Exception) -> None: + # My custom logic for frontend errors + print("Frontend Error: " + str(exception)) + +def custom_backend_handler(exception: Exception) -> Optional[rx.event.EventSpec]: + # My custom logic for backend errors + print("Backend Error: " + str(exception)) + +app = rx.App( + frontend_exception_handler = custom_frontend_handler, + backend_exception_handler = custom_backend_handler + ) +``` diff --git a/docs/utility_methods/lifespan_tasks.md b/docs/utility_methods/lifespan_tasks.md new file mode 100644 index 00000000000..bf5f84eb8ad --- /dev/null +++ b/docs/utility_methods/lifespan_tasks.md @@ -0,0 +1,84 @@ +# Lifespan Tasks + +_Added in v0.5.2_ + +Lifespan tasks are coroutines that run when the backend server is running. They +are useful for setting up the initial global state of the app, running periodic +tasks, and cleaning up resources when the server is shut down. + +Lifespan tasks are defined as async coroutines or async contextmanagers. To avoid +blocking the event thread, never use `time.sleep` or perform non-async I/O within +a lifespan task. + +In dev mode, lifespan tasks will stop and restart when a hot-reload occurs. + +## Tasks + +Any async coroutine can be used as a lifespan task. It will be started when the +backend comes up and will run until it returns or is cancelled due to server +shutdown. Long-running tasks should catch `asyncio.CancelledError` to perform +any necessary clean up. + +```python +async def long_running_task(foo, bar): + print(f"Starting \{foo} \{bar} task") + some_api = SomeApi(foo) + try: + while True: + updates = some_api.poll_for_updates() + other_api.push_changes(updates, bar) + await asyncio.sleep(5) # add some polling delay to avoid running too often + except asyncio.CancelledError: + some_api.close() # clean up the API if needed + print("Task was stopped") +``` + +### Register the Task + +To register a lifespan task, use `app.register_lifespan_task(coro_func, **kwargs)`. +Any keyword arguments specified during registration will be passed to the task. + +If the task accepts the special argument, `app`, it will be an instance of the `FastAPI` object +associated with the app. + +```python +app = rx.App() +app.register_lifespan_task(long_running_task, foo=42, bar=os.environ["BAR_PARAM"]) +``` + +## Context Managers + +Lifespan tasks can also be defined as async contextmanagers. This is useful for +setting up and tearing down resources and behaves similarly to the ASGI lifespan +protocol. + +Code up to the first `yield` will run when the backend comes up. As the backend +is shutting down, the code after the `yield` will run to clean up. + +Here is an example borrowed from the FastAPI docs and modified to work with this +interface. + +```python +from contextlib import asynccontextmanager + + +def fake_answer_to_everything_ml_model(x: float): + return x * 42 + + +ml_models = \{} + + +@asynccontextmanager +async def setup_model(app: FastAPI): + # Load the ML model + ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model + yield + # Clean up the ML models and release the resources + ml_models.clear() + +... + +app = rx.App() +app.register_lifespan_task(setup_model) +``` diff --git a/docs/utility_methods/other_methods.md b/docs/utility_methods/other_methods.md new file mode 100644 index 00000000000..e821faccf32 --- /dev/null +++ b/docs/utility_methods/other_methods.md @@ -0,0 +1,14 @@ +# Other Methods + +- `reset`: set all Vars to their default value for the given state (including substates). +- `get_value`: returns the value of a Var **without tracking changes to it**. This is useful + for serialization where the tracking wrapper is considered unserializable. +- `dict`: returns all state Vars (and substates) as a dictionary. This is + used internally when a page is first loaded and needs to be "hydrated" and + sent to the client. + +## Special Attributes + +- `dirty_vars`: a set of all Var names that have been modified since the last + time the state was sent to the client. This is used internally to determine + which Vars need to be sent to the client after processing an event. diff --git a/docs/utility_methods/router_attributes.md b/docs/utility_methods/router_attributes.md new file mode 100644 index 00000000000..edf0acfc55c --- /dev/null +++ b/docs/utility_methods/router_attributes.md @@ -0,0 +1,116 @@ +```python exec box +import reflex as rx +cell_style = {"font_family": "Instrument Sans", "font_style": "normal", "font_weight": "500", "font_size": "14px", "line_height": "1.5", "letter_spacing": "-0.0125em", "color": "var(--c-slate-11)"} + +class RouterState(rx.State): + pass + + +router_data = [ + {"name": "rx.State.router.page.host", "value": RouterState.router.page.host}, + {"name": "rx.State.router.page.path", "value": RouterState.router.page.path}, + {"name": "rx.State.router.page.raw_path", "value": RouterState.router.page.raw_path}, + {"name": "rx.State.router.page.full_path", "value": RouterState.router.page.full_path}, + {"name": "rx.State.router.page.full_raw_path", "value": RouterState.router.page.full_raw_path}, + {"name": "rx.State.router.page.params", "value": RouterState.router.page.params.to_string()}, + {"name": "rx.State.router.session.client_token", "value": RouterState.router.session.client_token}, + {"name": "rx.State.router.session.session_id", "value": RouterState.router.session.session_id}, + {"name": "rx.State.router.session.client_ip", "value": RouterState.router.session.client_ip}, + {"name": "rx.State.router.headers.host", "value": RouterState.router.headers.host}, + {"name": "rx.State.router.headers.origin", "value": RouterState.router.headers.origin}, + {"name": "rx.State.router.headers.upgrade", "value": RouterState.router.headers.upgrade}, + {"name": "rx.State.router.headers.connection", "value": RouterState.router.headers.connection}, + {"name": "rx.State.router.headers.cookie", "value": RouterState.router.headers.cookie}, + {"name": "rx.State.router.headers.pragma", "value": RouterState.router.headers.pragma}, + {"name": "rx.State.router.headers.cache_control", "value": RouterState.router.headers.cache_control}, + {"name": "rx.State.router.headers.user_agent", "value": RouterState.router.headers.user_agent}, + {"name": "rx.State.router.headers.sec_websocket_version", "value": RouterState.router.headers.sec_websocket_version}, + {"name": "rx.State.router.headers.sec_websocket_key", "value": RouterState.router.headers.sec_websocket_key}, + {"name": "rx.State.router.headers.sec_websocket_extensions", "value": RouterState.router.headers.sec_websocket_extensions}, + {"name": "rx.State.router.headers.accept_encoding", "value": RouterState.router.headers.accept_encoding}, + {"name": "rx.State.router.headers.accept_language", "value": RouterState.router.headers.accept_language}, + {"name": "rx.State.router.headers.raw_headers", "value": RouterState.router.headers.raw_headers.to_string()}, + ] + +``` + +# State Utility Methods + +The state object has several methods and attributes that return information +about the current page, session, or state. + +## Router Attributes + +The `self.router` attribute has several sub-attributes that provide various information: + +- `router.page`: data about the current page and route + - `host`: The hostname and port serving the current page (frontend). + - `path`: The path of the current page (for dynamic pages, this will contain the slug) + - `raw_path`: The path of the page displayed in the browser (including params and dynamic values) + - `full_path`: `path` with `host` prefixed + - `full_raw_path`: `raw_path` with `host` prefixed + - `params`: Dictionary of query params associated with the request + +- `router.session`: data about the current session + - `client_token`: UUID associated with the current tab's token. Each tab has a unique token. + - `session_id`: The ID associated with the client's websocket connection. Each tab has a unique session ID. + - `client_ip`: The IP address of the client. Many users may share the same IP address. + +- `router.headers`: headers associated with the websocket connection. These values can only change when the websocket is re-established (for example, during page refresh). + - `host`: The hostname and port serving the websocket (backend). + - `origin`: The origin of the request. + - `upgrade`: The upgrade header for websocket connections. + - `connection`: The connection header. + - `cookie`: The cookie header. + - `pragma`: The pragma header. + - `cache_control`: The cache control header. + - `user_agent`: The user agent string of the client. + - `sec_websocket_version`: The websocket version. + - `sec_websocket_key`: The websocket key. + - `sec_websocket_extensions`: The websocket extensions. + - `accept_encoding`: The accepted encodings. + - `accept_language`: The accepted languages. + - `raw_headers`: A mapping of all HTTP headers as a frozen dictionary. This provides access to any header that was sent with the request, not just the common ones listed above. + +### Example Values on this Page + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Value"), + ), + ), + rx.table.body( + *[ + rx.table.row( + rx.table.cell(item["name"], style=cell_style), + rx.table.cell(rx.code(item["value"], style={"color": rx.color("violet", 11), "border_radius": "0.25rem", "border": f"1px solid {rx.color('violet', 5)}", "background": rx.color("violet", 3)})), + ) + for item in router_data + ] + ), + variant="surface", + margin_y="1em", + ) +``` + +### Accessing Raw Headers + +The `raw_headers` attribute provides access to all HTTP headers as a frozen dictionary. This is useful when you need to access headers that are not explicitly defined in the `HeaderData` class: + +```python box +# Access a specific header +custom_header_value = self.router.headers.raw_headers.get("x-custom-header", "") + +# Example of accessing common headers +user_agent = self.router.headers.raw_headers.get("user-agent", "") +content_type = self.router.headers.raw_headers.get("content-type", "") +authorization = self.router.headers.raw_headers.get("authorization", "") + +# You can also check if a header exists +has_custom_header = "x-custom-header" in self.router.headers.raw_headers +``` + +This is particularly useful for accessing custom headers or when working with specific HTTP headers that are not part of the standard set exposed as direct attributes. diff --git a/docs/vars/base_vars.md b/docs/vars/base_vars.md new file mode 100644 index 00000000000..c8367f2df4b --- /dev/null +++ b/docs/vars/base_vars.md @@ -0,0 +1,225 @@ +```python exec +import random +import time + +import reflex as rx + +``` + +# Base Vars + +Vars are any fields in your app that may change over time. A Var is directly +rendered into the frontend of the app. + +Base vars are defined as fields in your State class. + +They can have a preset default value. If you don't provide a default value, you +must provide a type annotation. + +```md alert warning +# State Vars should provide type annotations. + +Reflex relies on type annotations to determine the type of state vars during the compilation process. +``` + +```python demo exec +class TickerState(rx.State): + ticker: str ="AAPL" + price: str = "$150" + + +def ticker_example(): + return rx.center( + rx.vstack( + rx.heading(TickerState.ticker, size="3"), + rx.text(f"Current Price: {TickerState.price}", font_size="md"), + rx.text("Change: 4%", color="green"), + ), + + ) +``` + +In this example `ticker` and `price` are base vars in the app, which can be modified at runtime. + +```md alert warning +# Vars must be JSON serializable. + +Vars are used to communicate between the frontend and backend. They must be primitive Python types, Plotly figures, Pandas dataframes, or [a custom defined type](/docs/vars/custom_vars). +``` + +## Accessing state variables on different pages + +State is just a python class and so can be defined on one page and then imported and used on another. Below we define `TickerState` class on the page `state.py` and then import it and use it on the page `index.py`. + +```python +# state.py + +class TickerState(rx.State): + ticker: str = "AAPL" + price: str = "$150" +``` + +```python +# index.py +from .state import TickerState + +def ticker_example(): + return rx.center( + rx.vstack( + rx.heading(TickerState.ticker, size="3"), + rx.text(f"Current Price: \{TickerState.price}", font_size="md"), + rx.text("Change: 4%", color="green"), + ), + + ) +``` + +## Backend-only Vars + +Any Var in a state class that starts with an underscore (`_`) is considered backend +only and will **not be synchronized with the frontend**. Data associated with a +specific session that is _not directly rendered on the frontend should be stored +in a backend-only var_ to reduce network traffic and improve performance. + +They have the advantage that they don't need to be JSON serializable, however +they must still be pickle-able to be used with redis in prod mode. They are +not directly renderable on the frontend, and **may be used to store sensitive +values that should not be sent to the client**. + +```md alert warning +# Protect auth data and sensitive state in backend-only vars. + +Regular vars and computed vars should **only** be used for rendering the state +of your app in the frontend. Having any type of permissions or authenticated state based on +a regular var presents a security risk as you may assume these have shared control +with the frontend (client) due to default setter methods. + +For improved security, `state_auto_setters=False` may be set in `rxconfig.py` +to prevent the automatic generation of setters for regular vars, however, the +client will still be able to locally modify the contents of frontend vars as +they are presented in the UI. +``` + +For example, a backend-only var is used to store a large data structure which is +then paged to the frontend using cached vars. + +```python demo exec +import numpy as np + + +class BackendVarState(rx.State): + _backend: np.ndarray = np.array([random.randint(0, 100) for _ in range(100)]) + offset: int = 0 + limit: int = 10 + + @rx.var(cache=True) + def page(self) -> list[int]: + return [ + int(x) # explicit cast to int + for x in self._backend[self.offset : self.offset + self.limit] + ] + + @rx.var(cache=True) + def page_number(self) -> int: + return (self.offset // self.limit) + 1 + (1 if self.offset % self.limit else 0) + + @rx.var(cache=True) + def total_pages(self) -> int: + return len(self._backend) // self.limit + (1 if len(self._backend) % self.limit else 0) + + @rx.event + def prev_page(self): + self.offset = max(self.offset - self.limit, 0) + + @rx.event + def next_page(self): + if self.offset + self.limit < len(self._backend): + self.offset += self.limit + + @rx.event + def generate_more(self): + self._backend = np.append(self._backend, [random.randint(0, 100) for _ in range(random.randint(0, 100))]) + + @rx.event + def set_limit(self, value: str): + self.limit = int(value) + +def backend_var_example(): + return rx.vstack( + rx.hstack( + rx.button( + "Prev", + on_click=BackendVarState.prev_page, + ), + rx.text(f"Page {BackendVarState.page_number} / {BackendVarState.total_pages}"), + rx.button( + "Next", + on_click=BackendVarState.next_page, + ), + rx.text("Page Size"), + rx.input( + width="5em", + value=BackendVarState.limit, + on_change=BackendVarState.set_limit, + ), + rx.button("Generate More", on_click=BackendVarState.generate_more), + ), + rx.list( + rx.foreach( + BackendVarState.page, + lambda x, ix: rx.text(f"_backend[{ix + BackendVarState.offset}] = {x}"), + ), + ), + ) +``` + +## Using rx.field / rx.Field to improve type hinting for vars + +When defining state variables you can use `rx.Field[T]` to annotate the variable's type. Then, you can initialize the variable using `rx.field(default_value)`, where `default_value` is an instance of type `T`. + +This approach makes the variable's type explicit, aiding static analysis tools in type checking. In addition, it shows you what methods are allowed to modify the variable in your frontend code, as they are listed in the type hint. + +Below are two examples: + +```python +import reflex as rx + +app = rx.App() + + +class State(rx.State): + x: rx.Field[bool] = rx.field(False) + + def flip(self): + self.x = not self.x + + +@app.add_page +def index(): + return rx.vstack( + rx.button("Click me", on_click=State.flip), + rx.text(State.x), + rx.text(~State.x), + ) +``` + +Here `State.x`, as it is typed correctly as a `boolean` var, gets better code completion, i.e. here we get options such as `to_string()` or `equals()`. + +```python +import reflex as rx + +app = rx.App() + + +class State(rx.State): + x: rx.Field[dict[str, list[int]]] = rx.field(default_factory=dict) + + +@app.add_page +def index(): + return rx.vstack( + rx.text(State.x.values()[0][0]), + ) +``` + +Here `State.x`, as it is typed correctly as a `dict` of `str` to `list` of `int` var, gets better code completion, i.e. here we get options such as `contains()`, `keys()`, `values()`, `items()` or `merge()`. diff --git a/docs/vars/computed_vars.md b/docs/vars/computed_vars.md new file mode 100644 index 00000000000..ce1bef92d13 --- /dev/null +++ b/docs/vars/computed_vars.md @@ -0,0 +1,190 @@ +```python exec +import random +import time +import asyncio + +import reflex as rx +``` + +# Computed Vars + +Computed vars have values derived from other properties on the backend. They are +defined as methods in your State class with the `@rx.var` decorator. + +Try typing in the input box and clicking out. + +```python demo exec id=upper +class UppercaseState(rx.State): + text: str = "hello" + + def set_text(self, value: str): + self.text = value + + @rx.var + def upper_text(self) -> str: + # This will be recomputed whenever `text` changes. + return self.text.upper() + + +def uppercase_example(): + return rx.vstack( + rx.heading(UppercaseState.upper_text), + rx.input(on_blur=UppercaseState.set_text, placeholder="Type here..."), + ) +``` + +Here, `upper_text` is a computed var that always holds the upper case version of `text`. + +We recommend always using type annotations for computed vars. + +## Cached Vars + +By default, all computed vars are cached (`cache=True`). A cached var is only +recomputed when the other state vars it depends on change. This is useful for +expensive computations, but in some cases it may not update when you expect it to. + +To create a computed var that recomputes on every state update regardless of +dependencies, use `@rx.var(cache=False)`. + +Previous versions of Reflex had a `@rx.cached_var` decorator, which is now replaced +by the `cache` argument of `@rx.var` (which defaults to `True`). + +```python demo exec +class CachedVarState(rx.State): + counter_a: int = 0 + counter_b: int = 0 + + @rx.var(cache=False) + def last_touch_time(self) -> str: + # This is updated anytime the state is updated. + return time.strftime("%H:%M:%S") + + @rx.event + def increment_a(self): + self.counter_a += 1 + + @rx.var(cache=True) + def last_counter_a_update(self) -> str: + # This is updated only when `counter_a` changes. + return f"{self.counter_a} at {time.strftime('%H:%M:%S')}" + + @rx.event + def increment_b(self): + self.counter_b += 1 + + @rx.var(cache=True) + def last_counter_b_update(self) -> str: + # This is updated only when `counter_b` changes. + return f"{self.counter_b} at {time.strftime('%H:%M:%S')}" + + +def cached_var_example(): + return rx.vstack( + rx.text(f"State touched at: {CachedVarState.last_touch_time}"), + rx.text(f"Counter A: {CachedVarState.last_counter_a_update}"), + rx.text(f"Counter B: {CachedVarState.last_counter_b_update}"), + rx.hstack( + rx.button("Increment A", on_click=CachedVarState.increment_a), + rx.button("Increment B", on_click=CachedVarState.increment_b), + ), + ) +``` + +In this example `last_touch_time` uses `cache=False` to ensure it updates any +time the state is modified. `last_counter_a_update` is a cached computed var (using +the default `cache=True`) that only depends on `counter_a`, so it only gets recomputed +when `counter_a` changes. Similarly `last_counter_b_update` only depends on `counter_b`, +and thus is updated only when `counter_b` changes. + +## Async Computed Vars + +Async computed vars allow you to use asynchronous operations in your computed vars. +They are defined as async methods in your State class with the same `@rx.var` decorator. +Async computed vars are useful for operations that require asynchronous processing, such as: + +- Fetching data from external APIs +- Database operations +- File I/O operations +- Any other operations that benefit from async/await + +```python demo exec +class AsyncVarState(rx.State): + count: int = 0 + + @rx.var + async def delayed_count(self) -> int: + # Simulate an async operation like an API call + await asyncio.sleep(0.5) + return self.count * 2 + + @rx.event + def increment(self): + self.count += 1 + + +def async_var_example(): + return rx.vstack( + rx.heading("Async Computed Var Example"), + rx.text(f"Count: {AsyncVarState.count}"), + rx.text(f"Delayed count (x2): {AsyncVarState.delayed_count}"), + rx.button("Increment", on_click=AsyncVarState.increment), + spacing="4", + ) +``` + +In this example, `delayed_count` is an async computed var that returns the count multiplied by 2 after a simulated delay. +When the count changes, the async computed var is automatically recomputed. + +### Caching Async Computed Vars + +Just like regular computed vars, async computed vars can also be cached. This is especially +useful for expensive async operations like API calls or database queries. + +```python demo exec +class AsyncCachedVarState(rx.State): + user_id: int = 1 + refresh_trigger: int = 0 + + @rx.var(cache=True) + async def user_data(self) -> dict: + # In a real app, this would be an API call + await asyncio.sleep(1) # Simulate network delay + + # Simulate different user data based on user_id + users = { + 1: {"name": "Alice", "email": "alice@example.com"}, + 2: {"name": "Bob", "email": "bob@example.com"}, + 3: {"name": "Charlie", "email": "charlie@example.com"}, + } + + return users.get(self.user_id, {"name": "Unknown", "email": "unknown"}) + + @rx.event + def change_user(self): + # Cycle through users 1-3 + self.user_id = (self.user_id % 3) + 1 + + @rx.event + def force_refresh(self): + # This will not affect user_data dependencies, but will trigger a state update + self.refresh_trigger += 1 + + +def async_cached_var_example(): + return rx.vstack( + rx.heading("Cached Async Computed Var Example"), + rx.text(f"User ID: {AsyncCachedVarState.user_id}"), + rx.text(f"User Name: {AsyncCachedVarState.user_data['name']}"), + rx.text(f"User Email: {AsyncCachedVarState.user_data['email']}"), + rx.hstack( + rx.button("Change User", on_click=AsyncCachedVarState.change_user), + rx.button("Force Refresh (No Effect)", on_click=AsyncCachedVarState.force_refresh), + ), + rx.text("Note: The cached async var only updates when user_id changes, not when refresh_trigger changes."), + spacing="4", + ) +``` + +In this example, `user_data` is a cached async computed var that simulates fetching user data. +It is only recomputed when `user_id` changes, not when other state variables like `refresh_trigger` change. +This demonstrates how caching works with async computed vars to optimize performance for expensive operations. diff --git a/docs/vars/custom_vars.md b/docs/vars/custom_vars.md new file mode 100644 index 00000000000..ef7603b9c30 --- /dev/null +++ b/docs/vars/custom_vars.md @@ -0,0 +1,81 @@ +```python exec +import reflex as rx +import dataclasses +from typing import TypedDict + +``` + +# Custom Vars + +As mentioned in the [vars page](/docs/vars/base_vars), Reflex vars must be JSON serializable. + +This means we can support any Python primitive types, as well as lists, dicts, and tuples. However, you can also create more complex var types using dataclasses (recommended), TypedDict, or Pydantic models. + +## Defining a Type + +In this example, we will create a custom var type for storing translations using a dataclass. + +Once defined, we can use it as a state var, and reference it from within a component. + +```python demo exec +import googletrans +import dataclasses +from typing import TypedDict + +@dataclasses.dataclass +class Translation: + original_text: str + translated_text: str + +class TranslationState(rx.State): + input_text: str = "Hola Mundo" + current_translation: Translation = Translation(original_text="", translated_text="") + + # Explicitly define the setter method + def set_input_text(self, value: str): + self.input_text = value + + @rx.event + def translate(self): + self.current_translation.original_text = self.input_text + self.current_translation.translated_text = googletrans.Translator().translate(self.input_text, dest="en").text + +def translation_example(): + return rx.vstack( + rx.input( + on_blur=TranslationState.set_input_text, + default_value=TranslationState.input_text, + placeholder="Text to translate...", + ), + rx.button("Translate", on_click=TranslationState.translate), + rx.text(TranslationState.current_translation.translated_text), + ) +``` + +## Alternative Approaches + +### Using TypedDict + +You can also use TypedDict for defining custom var types: + +```python +from typing import TypedDict + +class Translation(TypedDict): + original_text: str + translated_text: str +``` + +### Using Pydantic Models + +Pydantic models are another option for complex data structures: + +```python +from pydantic import BaseModel + +class Translation(BaseModel): + original_text: str + translated_text: str +``` + +For complex data structures, dataclasses are recommended as they provide a clean, type-safe way to define custom var types with good IDE support. diff --git a/docs/vars/var-operations.md b/docs/vars/var-operations.md new file mode 100644 index 00000000000..28606fa4e53 --- /dev/null +++ b/docs/vars/var-operations.md @@ -0,0 +1,554 @@ +```python exec +import random +import time + +import numpy as np + +import reflex as rx + +``` + +# Var Operations + +Var operations transform the placeholder representation of the value on the +frontend and provide a way to perform basic operations on the Var without having +to define a computed var. + +Within your frontend components, you cannot use arbitrary Python functions on +the state vars. For example, the following code will **not work.** + +```python +class State(rx.State): + number: int + +def index(): + # This will be compiled before runtime, before `State.number` has a known value. + # Since `float` is not a valid var operation, this will throw an error. + rx.text(float(State.number)) +``` + +This is because we compile the frontend to Javascript, but the value of `State.number` +is only known at runtime. + +In this example below we use a var operation to concatenate a `string` with a `var`, meaning we do not have to do in within state as a computed var. + +```python demo exec +coins = ["BTC", "ETH", "LTC", "DOGE"] + +class VarSelectState(rx.State): + selected: str = "DOGE" + + def set_selected(self, value: str): + self.selected = value + +def var_operations_example(): + return rx.vstack( + # Using a var operation to concatenate a string with a var. + rx.heading("I just bought a bunch of " + VarSelectState.selected), + # Using an f-string to interpolate a var. + rx.text(f"{VarSelectState.selected} is going to the moon!"), + rx.select( + coins, + value=VarSelectState.selected, + on_change=VarSelectState.set_selected, + ) + ) +``` + +```md alert success +# Vars support many common operations. + +They can be used for arithmetic, string concatenation, inequalities, indexing, and more. See the [full list of supported operations](/docs/api-reference/var/). +``` + +## Supported Operations + +Var operations allow us to change vars on the front-end without having to create more computed vars on the back-end in the state. + +Some simple examples are the `==` var operator, which is used to check if two vars are equal and the `to_string()` var operator, which is used to convert a var to a string. + +```python demo exec + +fruits = ["Apple", "Banana", "Orange", "Mango"] + +class EqualsState(rx.State): + selected: str = "Apple" + favorite: str = "Banana" + + def set_selected(self, value: str): + self.selected = value + + +def var_equals_example(): + return rx.vstack( + rx.text(EqualsState.favorite.to_string() + " is my favorite fruit!"), + rx.select( + fruits, + value=EqualsState.selected, + on_change=EqualsState.set_selected, + ), + rx.cond( + EqualsState.selected == EqualsState.favorite, + rx.text("The selected fruit is equal to the favorite fruit!"), + rx.text("The selected fruit is not equal to the favorite fruit."), + ), + ) + +``` + +### Negate, Absolute and Length + +The `-` operator is used to get the negative version of the var. The `abs()` operator is used to get the absolute value of the var. The `.length()` operator is used to get the length of a list var. + +```python demo exec +import random + +class OperState(rx.State): + number: int + numbers_seen: list = [] + + @rx.event + def update(self): + self.number = random.randint(-100, 100) + self.numbers_seen.append(self.number) + +def var_operation_example(): + return rx.vstack( + rx.heading(f"The number: {OperState.number}", size="3"), + rx.hstack( + rx.text("Negated:", rx.badge(-OperState.number, variant="soft", color_scheme="green")), + rx.text("Absolute:", rx.badge(abs(OperState.number), variant="soft", color_scheme="blue")), + rx.text("Numbers seen:", rx.badge(OperState.numbers_seen.length(), variant="soft", color_scheme="red")), + ), + rx.button("Update", on_click=OperState.update), + ) +``` + +### Comparisons and Mathematical Operators + +All of the comparison operators are used as expected in python. These include `==`, `!=`, `>`, `>=`, `<`, `<=`. + +There are operators to add two vars `+`, subtract two vars `-`, multiply two vars `*` and raise a var to a power `pow()`. + +```python demo exec +import random + +class CompState(rx.State): + number_1: int + number_2: int + + @rx.event + def update(self): + self.number_1 = random.randint(-10, 10) + self.number_2 = random.randint(-10, 10) + +def var_comparison_example(): + + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Integer 1"), + rx.table.column_header_cell("Integer 2"), + rx.table.column_header_cell("Operation"), + rx.table.column_header_cell("Outcome"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 == Int 2"), + rx.table.cell((CompState.number_1 == CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 != Int 2"), + rx.table.cell((CompState.number_1 != CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 > Int 2"), + rx.table.cell((CompState.number_1 > CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 >= Int 2"), + rx.table.cell((CompState.number_1 >= CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2, ), + rx.table.cell("Int 1 < Int 2 "), + rx.table.cell((CompState.number_1 < CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 <= Int 2"), + rx.table.cell((CompState.number_1 <= CompState.number_2).to_string()), + ), + + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 + Int 2"), + rx.table.cell(f"{(CompState.number_1 + CompState.number_2)}"), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 - Int 2"), + rx.table.cell(f"{CompState.number_1 - CompState.number_2}"), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 * Int 2"), + rx.table.cell(f"{CompState.number_1 * CompState.number_2}"), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("pow(Int 1, Int2)"), + rx.table.cell(f"{pow(CompState.number_1, CompState.number_2)}"), + ), + ), + width="100%", + ), + rx.button("Update", on_click=CompState.update), + ) +``` + +### True Division, Floor Division and Remainder + +The operator `/` represents true division. The operator `//` represents floor division. The operator `%` represents the remainder of the division. + +```python demo exec +import random + +class DivState(rx.State): + number_1: float = 3.5 + number_2: float = 1.4 + + @rx.event + def update(self): + self.number_1 = round(random.uniform(5.1, 9.9), 2) + self.number_2 = round(random.uniform(0.1, 4.9), 2) + +def var_div_example(): + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Integer 1"), + rx.table.column_header_cell("Integer 2"), + rx.table.column_header_cell("Operation"), + rx.table.column_header_cell("Outcome"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(DivState.number_1), + rx.table.cell(DivState.number_2), + rx.table.cell("Int 1 / Int 2"), + rx.table.cell(f"{DivState.number_1 / DivState.number_2}"), + ), + rx.table.row( + rx.table.row_header_cell(DivState.number_1), + rx.table.cell(DivState.number_2), + rx.table.cell("Int 1 // Int 2"), + rx.table.cell(f"{DivState.number_1 // DivState.number_2}"), + ), + rx.table.row( + rx.table.row_header_cell(DivState.number_1), + rx.table.cell(DivState.number_2), + rx.table.cell("Int 1 % Int 2"), + rx.table.cell(f"{DivState.number_1 % DivState.number_2}"), + ), + ), + width="100%", + ), + rx.button("Update", on_click=DivState.update), + ) +``` + +### And, Or and Not + +In Reflex the `&` operator represents the logical AND when used in the front end. This means that it returns true only when both conditions are true simultaneously. +The `|` operator represents the logical OR when used in the front end. This means that it returns true when either one or both conditions are true. +The `~` operator is used to invert a var. It is used on a var of type `bool` and is equivalent to the `not` operator. + +```python demo exec +import random + +class LogicState(rx.State): + var_1: bool = True + var_2: bool = True + + @rx.event + def update(self): + self.var_1 = random.choice([True, False]) + self.var_2 = random.choice([True, False]) + +def var_logical_example(): + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Var 1"), + rx.table.column_header_cell("Var 2"), + rx.table.column_header_cell("Operation"), + rx.table.column_header_cell("Outcome"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(LogicState.var_1.to_string()), + rx.table.cell(LogicState.var_2.to_string()), + rx.table.cell("Logical AND (&)"), + rx.table.cell((LogicState.var_1 & LogicState.var_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(LogicState.var_1.to_string()), + rx.table.cell(LogicState.var_2.to_string()), + rx.table.cell("Logical OR (|)"), + rx.table.cell((LogicState.var_1 | LogicState.var_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(LogicState.var_1.to_string()), + rx.table.cell(LogicState.var_2.to_string()), + rx.table.cell("The invert of Var 1 (~)"), + rx.table.cell((~LogicState.var_1).to_string()), + ), + + ), + width="100%", + ), + rx.button("Update", on_click=LogicState.update), + ) +``` + +### Contains, Reverse and Join + +The 'in' operator is not supported for Var types, we must use the `Var.contains()` instead. When we use `contains`, the var must be of type: `dict`, `list`, `tuple` or `str`. +`contains` checks if a var contains the object that we pass to it as an argument. + +We use the `reverse` operation to reverse a list var. The var must be of type `list`. + +Finally we use the `join` operation to join a list var into a string. + +```python demo exec +class ListsState(rx.State): + list_1: list = [1, 2, 3, 4, 6] + list_2: list = [7, 8, 9, 10] + list_3: list = ["p","y","t","h","o","n"] + +def var_list_example(): + return rx.hstack( + rx.vstack( + rx.heading(f"List 1: {ListsState.list_1}", size="3"), + rx.text(f"List 1 Contains 3: {ListsState.list_1.contains(3)}"), + ), + rx.vstack( + rx.heading(f"List 2: {ListsState.list_2}", size="3"), + rx.text(f"Reverse List 2: {ListsState.list_2.reverse()}"), + ), + rx.vstack( + rx.heading(f"List 3: {ListsState.list_3}", size="3"), + rx.text(f"List 3 Joins: {ListsState.list_3.join()}"), + ), + ) +``` + +### Lower, Upper, Split + +The `lower` operator converts a string var to lowercase. The `upper` operator converts a string var to uppercase. The `split` operator splits a string var into a list. + +```python demo exec +class StringState(rx.State): + string_1: str = "PYTHON is FUN" + string_2: str = "react is hard" + + +def var_string_example(): + return rx.hstack( + rx.vstack( + rx.heading(f"List 1: {StringState.string_1}", size="3"), + rx.text(f"List 1 Lower Case: {StringState.string_1.lower()}"), + ), + rx.vstack( + rx.heading(f"List 2: {StringState.string_2}", size="3"), + rx.text(f"List 2 Upper Case: {StringState.string_2.upper()}"), + rx.text(f"Split String 2: {StringState.string_2.split()}"), + ), + ) +``` + +## Get Item (Indexing) + +Indexing is only supported for strings, lists, tuples, dicts, and dataframes. To index into a state var strict type annotations are required. + +```python +class GetItemState1(rx.State): + list_1: list = [50, 10, 20] + +def get_item_error_1(): + return rx.progress(value=GetItemState1.list_1[0]) +``` + +In the code above you would expect to index into the first index of the list_1 state var. In fact the code above throws the error: `Invalid var passed for prop value, expected type , got value of type typing.Any.` This is because the type of the items inside the list have not been clearly defined in the state. To fix this you change the list_1 definition to `list_1: list[int] = [50, 10, 20]` + +```python demo exec +class GetItemState1(rx.State): + list_1: list[int] = [50, 10, 20] + +def get_item_error_1(): + return rx.progress(value=GetItemState1.list_1[0]) +``` + +### Using with Foreach + +Errors frequently occur when using indexing and `foreach`. + +```python +class ProjectsState(rx.State): + projects: List[dict] = [ + { + "technologies": ["Next.js", "Prisma", "Tailwind", "Google Cloud", "Docker", "MySQL"] + }, + { + "technologies": ["Python", "Flask", "Google Cloud", "Docker"] + } + ] + +def get_badge(technology: str) -> rx.Component: + return rx.badge(technology, variant="soft", color_scheme="green") + +def project_item(project: dict): + return rx.box( + rx.hstack( + rx.foreach(project["technologies"], get_badge) + ), + ) + +def failing_projects_example() -> rx.Component: + return rx.box(rx.foreach(ProjectsState.projects, project_item)) +``` + +The code above throws the error `TypeError: Could not foreach over var of type Any. (If you are trying to foreach over a state var, add a type annotation to the var.)` + +We must change `projects: list[dict]` => `projects: list[dict[str, list]]` because while projects is annotated, the item in project["technologies"] is not. + +```python demo exec +class ProjectsState(rx.State): + projects: list[dict[str, list]] = [ + { + "technologies": ["Next.js", "Prisma", "Tailwind", "Google Cloud", "Docker", "MySQL"] + }, + { + "technologies": ["Python", "Flask", "Google Cloud", "Docker"] + } + ] + + +def projects_example() -> rx.Component: + def get_badge(technology: str) -> rx.Component: + return rx.badge(technology, variant="soft", color_scheme="green") + + def project_item(project: dict) -> rx.Component: + + return rx.box( + rx.hstack( + rx.foreach(project["technologies"], get_badge) + ), + ) + return rx.box(rx.foreach(ProjectsState.projects, project_item)) +``` + +The previous example had only a single type for each of the dictionaries `keys` and `values`. For complex multi-type data, you need to use a dataclass, as shown below. + +```python demo exec +import dataclasses + +@dataclasses.dataclass +class ActressType: + actress_name: str + age: int + pages: list[dict[str, str]] + +class MultiDataTypeState(rx.State): + """The app state.""" + actresses: list[ActressType] = [ + ActressType( + actress_name="Ariana Grande", + age=30, + pages=[ + {"url": "arianagrande.com"}, {"url": "https://es.wikipedia.org/wiki/Ariana_Grande"} + ] + ), + ActressType( + actress_name="Gal Gadot", + age=38, + pages=[ + {"url": "http://www.galgadot.com/"}, {"url": "https://es.wikipedia.org/wiki/Gal_Gadot"} + ] + ) + ] + +def actresses_example() -> rx.Component: + def showpage(page: dict[str, str]): + return rx.vstack( + rx.text(page["url"]), + ) + + def showlist(item: ActressType): + return rx.vstack( + rx.hstack( + rx.text(item.actress_name), + rx.text(item.age), + ), + rx.foreach(item.pages, showpage), + ) + return rx.box(rx.foreach(MultiDataTypeState.actresses, showlist)) + +``` + +Setting the type of `actresses` to be `actresses: list[dict[str,str]]` would fail as it cannot be understood that the `value` for the `pages key` is actually a `list`. + +## Combine Multiple Var Operations + +You can also combine multiple var operations together, as seen in the next example. + +```python demo exec +import random + +class VarNumberState(rx.State): + number: int + + @rx.event + def update(self): + self.number = random.randint(0, 100) + +def var_number_example(): + return rx.vstack( + rx.heading(f"The number is {VarNumberState.number}", size="5"), + # Var operations can be composed for more complex expressions. + rx.cond( + VarNumberState.number % 2 == 0, + rx.text("Even", color="green"), + rx.text("Odd", color="red"), + ), + rx.button("Update", on_click=VarNumberState.update), + ) +``` + +We could have made a computed var that returns the parity of `number`, but +it can be simpler just to use a var operation instead. + +Var operations may be generally chained to make compound expressions, however +some complex transformations not supported by var operations must use computed vars +to calculate the value on the backend. diff --git a/docs/vi/README.md b/docs/vi/README.md deleted file mode 100644 index b8bc6637d68..00000000000 --- a/docs/vi/README.md +++ /dev/null @@ -1,255 +0,0 @@ -
-Reflex Logo -
- -### **✨ Ứng dụng web hiệu suất cao, tùy chỉnh bằng Python thuần. Deploy trong vài giây. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex là một thư viện để xây dựng ứng dụng web toàn bộ bằng Python thuần. - -Các tính năng chính: - -- **Python thuần tuý** - Viết toàn bộ ứng dụng cả backend và frontend hoàn toàn bằng Python, không cần học JavaScript. -- **Full Flexibility** - Reflex dễ dàng để bắt đầu, nhưng cũng có thể mở rộng lên các ứng dụng phức tạp. -- **Deploy Instantly** - Sau khi xây dựng ứng dụng, bạn có thể triển khai bằng [một dòng lệnh](https://reflex.dev/docs/hosting/deploy-quick-start/) hoặc triển khai trên server của riêng bạn. - -Đọc [bài viết về kiến trúc hệ thống](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) để hiểu rõ các hoạt động của Reflex. - -## ⚙️ Cài đặt - -Mở cửa sổ lệnh và chạy (Yêu cầu Python phiên bản 3.10+): - -```bash -pip install reflex -``` - -## 🥳 Tạo ứng dụng đầu tiên - -Cài đặt `reflex` cũng như cài đặt công cụ dòng lệnh `reflex`. - -Kiểm tra việc cài đặt đã thành công hay chưa bằng cách tạo mới một ứng dụng. (Thay `my_app_name` bằng tên ứng dụng của bạn): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -Lệnh này tạo ra một ứng dụng mẫu trong một thư mục mới. - -Bạn có thể chạy ứng dụng ở chế độ phát triển. - -```bash -reflex run -``` - -Bạn có thể xem ứng dụng của bạn ở địa chỉ http://localhost:3000. - -Bạn có thể thay đổi mã nguồn ở `my_app_name/my_app_name.py`. Reflex nhanh chóng làm mới và bạn có thể thấy thay đổi trên ứng dụng của bạn ngay lập tức khi bạn lưu file. - -## 🫧 Ứng dụng ví dụ - -Bắt đầu với ví dụ: tạo một ứng dụng tạo ảnh bằng [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Để cho đơn giản, chúng ta sẽ sử dụng [OpenAI API](https://platform.openai.com/docs/api-reference/authentication), nhưng bạn có thể sử dụng model của chính bạn được triển khai trên local. - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -Đây là toàn bộ đoạn mã để xây dựng ứng dụng trên. Nó được viết hoàn toàn trong một file Python! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Hãy phân tích chi tiết. - -
-Explaining the differences between backend and frontend parts of the DALL-E app. -
- -### **Reflex UI** - -Bắt đầu với giao diện chính. - -```python -def index(): - return rx.center( - ... - ) -``` - -Hàm `index` định nghĩa phần giao diện chính của ứng dụng. - -Chúng tôi sử dụng các component (thành phần) khác nhau như `center`, `vstack`, `input` và `button` để xây dựng giao diện phía trước. -Các component có thể được lồng vào nhau để tạo ra các bố cục phức tạp. Và bạn cũng có thể sử dụng từ khoá `args` để tận dụng đầy đủ sức mạnh của CSS. - -Reflex có đến hơn [60 component được xây dựng sẵn](https://reflex.dev/docs/library) để giúp bạn bắt đầu. Chúng ta có thể tạo ra một component mới khá dễ dàng, thao khảo: [xây dựng component của riêng bạn](https://reflex.dev/docs/wrapping-react/overview/). - -### **State** - -Reflex biểu diễn giao diện bằng các hàm của state (trạng thái). - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -Một state định nghĩa các biến (được gọi là vars) có thể thay đổi trong một ứng dụng và cho phép các hàm có thể thay đổi chúng. - -Tại đây state được cấu thành từ một `prompt` và `image_url`. -Có cũng những biến boolean `processing` và `complete` -để chỉ ra khi nào tắt nút (trong quá trình tạo hình ảnh) -và khi nào hiển thị hình ảnh kết quả. - -### **Event Handlers** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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 -``` - -Với các state, chúng ta định nghĩa các hàm có thể thay đổi state vars được gọi là event handlers. Event handler là cách chúng ta có thể thay đổi state trong Reflex. Chúng có thể là phản hồi khi người dùng thao tác, chằng hạn khi nhấn vào nút hoặc khi đang nhập trong text box. Các hành động này được gọi là event. - -Ứng dụng DALL·E. của chúng ta có một event handler, `get_image` để lấy hình ảnh từ OpenAI API. Sử dụng từ khoá `yield` in ở giữa event handler để cập nhật giao diện. Hoặc giao diện có thể cập nhật ở cuối event handler. - -### **Routing** - -Cuối cùng, chúng ta định nghĩa một ứng dụng. - -```python -app = rx.App() -``` - -Chúng ta thêm một trang ở đầu ứng dụng bằng index component. Chúng ta cũng thêm tiêu đề của ứng dụng để hiển thị lên trình duyệt. - -```python -app.add_page(index, title="DALL-E") -``` - -Bạn có thể tạo một ứng dụng nhiều trang bằng cách thêm trang. - -## 📑 Tài liệu - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Status - -Reflex phát hành vào tháng 12/2022 với tên là Pynecone. - -Từ năm 2025, [Reflex Cloud](https://cloud.reflex.dev) đã ra mắt để cung cấp trải nghiệm lưu trữ tốt nhất cho các ứng dụng Reflex. Chúng tôi sẽ tiếp tục phát triển và triển khai thêm nhiều tính năng mới. - -Reflex ra phiên bản mới với các tính năng mới hàng tuần! Hãy :star: star và :eyes: watch repo này để thấy các cập nhật mới nhất. - -## Contributing - -Chúng tôi chào đón mọi đóng góp dù lớn hay nhỏ. Dưới đây là các cách để bắt đầu với cộng đồng Reflex. - -- **Discord**: [Discord](https://discord.gg/T5WSbC2YtQ) của chúng tôi là nơi tốt nhất để nhờ sự giúp đỡ và thảo luận các bạn có thể đóng góp. -- **GitHub Discussions**: Là cách tốt nhất để thảo luận về các tính năng mà bạn có thể đóng góp hoặc những điều bạn chưa rõ. -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) là nơi tốt nhất để thông báo. Ngoài ra bạn có thể sửa chữa các vấn đề bằng cách tạo PR. - -Chúng tôi luôn sẵn sàng tìm kiếm các contributor, bất kể kinh nghiệm. Để tham gia đóng góp, xin mời xem -[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## Xin cảm ơn các Contributors: - - - - - -## License - -Reflex là mã nguồn mở và sử dụng giấy phép [Apache License 2.0](/LICENSE). diff --git a/docs/wrapping-react/custom-code-and-hooks.md b/docs/wrapping-react/custom-code-and-hooks.md new file mode 100644 index 00000000000..fc18a5d84d5 --- /dev/null +++ b/docs/wrapping-react/custom-code-and-hooks.md @@ -0,0 +1,109 @@ +When wrapping a React component, you may need to define custom code or hooks that are specific to the component. This is done by defining the `add_custom_code`or `add_hooks` methods in your component class. + +## Custom Code + +Custom code is any JS code that need to be included in your page, but not necessarily in the component itself. This can include things like CSS styles, JS libraries, or any other code that needs to be included in the page. + +```python +class CustomCodeComponent(MyBaseComponent): + """MyComponent.""" + + def add_custom_code(self) -> list[str]: + """Add custom code to the component.""" + code1 = """const customVariable = "Custom code1";""" + code2 = """console.log(customVariable);""" + + return [code1, code2] +``` + +The above example will render the following JS code in the page: + +```javascript +/* import here */ + +const customVariable = "Custom code1"; +console.log(customVariable); + +/* rest of the page code */ +``` + +## Custom Hooks + +Custom hooks are any hooks that need to be included in your component. This can include things like `useEffect`, `useState`, or any other hooks from the library you are wrapping. + +- Simple hooks can be added as strings. +- More complex hooks that need to have special import or be written in a specific order can be added as `rx.Var` with a `VarData` object to specify the position of the hook. + - The `imports` attribute of the `VarData` object can be used to specify any imports that need to be included in the component. + - The `position` attribute of the `VarData` object can be set to `Hooks.HookPosition.PRE_TRIGGER` or `Hooks.HookPosition.POST_TRIGGER` to specify the position of the hook in the component. + +```md alert info +# The `position` attribute is only used for hooks that need to be written in a specific order. + +- If an event handler need to refer to a variable defined in a hook, the hook should be written before the event handler. +- If a hook need to refer to the memoized event handler by name, the hook should be written after the event handler. +``` + +```python +from reflex.vars.base import Var, VarData +from reflex_core.constants import Hooks +from reflex.components.el.elements import Div + +class ComponentWithHooks(Div, MyBaseComponent): + """MyComponent.""" + + def add_hooks(self) -> list[str| Var]: + """Add hooks to the component.""" + hooks = [] + hooks1 = """const customHookVariable = "some value";""" + hooks.append(hooks1) + + # A hook that need to be written before the memoized event handlers. + hooks2 = Var( + """useEffect(() => { + console.log("PreTrigger: " + customHookVariable); + }, []); + """, + _var_data=VarData( + imports=\{"react": ["useEffect"],\}, + position=Hooks.HookPosition.PRE_TRIGGER + ), + ) + hooks.append(hooks2) + + hooks3 = Var( + """useEffect(() => { + console.log("PostTrigger: " + customHookVariable); + }, []); + """, + _var_data=VarData( + imports=\{"react": ["useEffect"],\}, + position=Hooks.HookPosition.POST_TRIGGER + ), + ) + hooks.append(hooks3) + return hooks +``` + +The `ComponentWithHooks` will be rendered in the component in the following way: + +```javascript +export function Div_7178f430b7b371af8a12d8265d65ab9b() { + const customHookVariable = "some value"; + + useEffect(() => { + console.log("PreTrigger: " + customHookVariable); + }, []); + + /* memoized triggers such as on_click, on_change, etc will render here */ + + useEffect(() => { + console.log("PostTrigger: "+ customHookVariable); + }, []); + + return jsx("div", \{\}); +} +``` + +```md alert info +# You can mix custom code and hooks in the same component. Hooks can access a variable defined in the custom code, but custom code cannot access a variable defined in a hook. +``` diff --git a/docs/wrapping-react/example.md b/docs/wrapping-react/example.md new file mode 100644 index 00000000000..5cb34ace0a6 --- /dev/null +++ b/docs/wrapping-react/example.md @@ -0,0 +1,408 @@ +```python exec +import reflex as rx +from typing import Any +``` + +# Complex Example + +In this more complex example we will be wrapping `reactflow` a library for building node based applications like flow charts, diagrams, graphs, etc. + +## Import + +Lets start by importing the library [reactflow](https://www.npmjs.com/package/reactflow). Lets make a separate file called `reactflow.py` and add the following code: + +```python +import reflex as rx +from typing import Any, Dict, List, Union + +class ReactFlowLib(rx.Component): + """A component that wraps a react flow lib.""" + + library = "reactflow" + + def _get_custom_code(self) -> str: + return """import 'reactflow/dist/style.css'; + """ +``` + +Notice we also use the `_get_custom_code` method to import the css file that is needed for the styling of the library. + +## Components + +For this tutorial we will wrap three components from Reactflow: `ReactFlow`, `Background`, and `Controls`. Lets start with the `ReactFlow` component. + +Here we will define the `tag` and the `vars` that we will need to use the component. + +For this tutorial we will define `EventHandler` props `on_nodes_change` and `on_connect`, but you can find all the events that the component triggers in the [reactflow docs](https://reactflow.dev/docs/api/react-flow-props/#onnodeschange). + +```python +import reflex as rx +from typing import Any, Dict, List, Union + +class ReactFlowLib(rx.Component): + ... + +class ReactFlow(ReactFlowLib): + + tag = "ReactFlow" + + nodes: rx.Var[List[Dict[str, Any]]] + + edges: rx.Var[List[Dict[str, Any]]] + + fit_view: rx.Var[bool] + + nodes_draggable: rx.Var[bool] + + nodes_connectable: rx.Var[bool] + + nodes_focusable: rx.Var[bool] + + on_nodes_change: rx.EventHandler[lambda e0: [e0]] + + on_connect: rx.EventHandler[lambda e0: [e0]] +``` + +Now lets add the `Background` and `Controls` components. We will also create the components using the `create` method so that we can use them in our app. + +```python +import reflex as rx +from typing import Any, Dict, List, Union + +class ReactFlowLib(rx.Component): + ... + +class ReactFlow(ReactFlowLib): + ... + +class Background(ReactFlowLib): + + tag = "Background" + + color: rx.Var[str] + + gap: rx.Var[int] + + size: rx.Var[int] + + variant: rx.Var[str] + +class Controls(ReactFlowLib): + + tag = "Controls" + +react_flow = ReactFlow.create +background = Background.create +controls = Controls.create +``` + +## Building the App + +Now that we have our components lets build the app. + +Lets start by defining the initial nodes and edges that we will use in our app. + +```python +import reflex as rx +from .react_flow import react_flow, background, controls +import random +from collections import defaultdict +from typing import Any, Dict, List + + +initial_nodes = [ + \{ + 'id': '1', + 'type': 'input', + 'data': \{'label': '150'}, + 'position': \{'x': 250, 'y': 25}, + }, + \{ + 'id': '2', + 'data': \{'label': '25'}, + 'position': \{'x': 100, 'y': 125}, + }, + \{ + 'id': '3', + 'type': 'output', + 'data': \{'label': '5'}, + 'position': \{'x': 250, 'y': 250}, + }, +] + +initial_edges = [ + \{'id': 'e1-2', 'source': '1', 'target': '2', 'label': '*', 'animated': True}, + \{'id': 'e2-3', 'source': '2', 'target': '3', 'label': '+', 'animated': True}, +] +``` + +Next we will define the state of our app. We have four event handlers: `add_random_node`, `clear_graph`, `on_connect` and `on_nodes_change`. + +The `on_nodes_change` event handler is triggered when a node is selected and dragged. This function is used to update the position of a node during dragging. It takes a single argument `node_changes`, which is a list of dictionaries containing various types of metadata. For updating positions, the function specifically processes changes of type `position`. + +```python +class State(rx.State): + """The app state.""" + nodes: List[Dict[str, Any]] = initial_nodes + edges: List[Dict[str, Any]] = initial_edges + + @rx.event + def add_random_node(self): + new_node_id = f'\{len(self.nodes) + 1\}' + node_type = random.choice(['default']) + # Label is random number + label = new_node_id + x = random.randint(0, 500) + y = random.randint(0, 500) + + new_node = { + 'id': new_node_id, + 'type': node_type, + 'data': \{'label': label}, + 'position': \{'x': x, 'y': y}, + 'draggable': True, + } + self.nodes.append(new_node) + + @rx.event + def clear_graph(self): + self.nodes = [] # Clear the nodes list + self.edges = [] # Clear the edges list + + @rx.event + def on_connect(self, new_edge): + # Iterate over the existing edges + for i, edge in enumerate(self.edges): + # If we find an edge with the same ID as the new edge + if edge["id"] == f"e\{new_edge['source']}-\{new_edge['target']}": + # Delete the existing edge + del self.edges[i] + break + + # Add the new edge + self.edges.append({ + "id": f"e\{new_edge['source']}-\{new_edge['target']}", + "source": new_edge["source"], + "target": new_edge["target"], + "label": random.choice(["+", "-", "*", "/"]), + "animated": True, + }) + + @rx.event + def on_nodes_change(self, node_changes: List[Dict[str, Any]]): + # Receives a list of Nodes in case of events like dragging + map_id_to_new_position = defaultdict(dict) + + # Loop over the changes and store the new position + for change in node_changes: + if change["type"] == "position" and change.get("dragging") == True: + map_id_to_new_position[change["id"]] = change["position"] + + # Loop over the nodes and update the position + for i, node in enumerate(self.nodes): + if node["id"] in map_id_to_new_position: + new_position = map_id_to_new_position[node["id"]] + self.nodes[i]["position"] = new_position +``` + +Now lets define the UI of our app. We will use the `react_flow` component and pass in the `nodes` and `edges` from our state. We will also add the `on_connect` event handler to the `react_flow` component to handle when an edge is connected. + +```python +def index() -> rx.Component: + return rx.vstack( + react_flow( + background(), + controls(), + nodes_draggable=True, + nodes_connectable=True, + on_connect=lambda e0: State.on_connect(e0), + on_nodes_change=lambda e0: State.on_nodes_change(e0), + nodes=State.nodes, + edges=State.edges, + fit_view=True, + ), + rx.hstack( + rx.button("Clear graph", on_click=State.clear_graph, width="100%"), + rx.button("Add node", on_click=State.add_random_node, width="100%"), + width="100%", + ), + height="30em", + width="100%", + ) + + +# Add state and page to the app. +app = rx.App() +app.add_page(index) +``` + +```python exec +import reflex as rx +from typing import Any, Dict, List, Union +from collections import defaultdict +import random + +class ReactFlowLib(rx.Component): + """A component that wraps a react flow lib.""" + + library = "reactflow" + + def _get_custom_code(self) -> str: + return """import 'reactflow/dist/style.css'; + """ + +class ReactFlow(ReactFlowLib): + + tag = "ReactFlow" + + nodes: rx.Var[List[Dict[str, Any]]] + + edges: rx.Var[List[Dict[str, Any]]] + + fit_view: rx.Var[bool] + + nodes_draggable: rx.Var[bool] + + nodes_connectable: rx.Var[bool] + + nodes_focusable: rx.Var[bool] + + on_nodes_change: rx.EventHandler[lambda e0: [e0]] + + on_connect: rx.EventHandler[lambda e0: [e0]] + + +class Background(ReactFlowLib): + + tag = "Background" + + color: rx.Var[str] + + gap: rx.Var[int] + + size: rx.Var[int] + + variant: rx.Var[str] + +class Controls(ReactFlowLib): + + tag = "Controls" + +react_flow = ReactFlow.create +background = Background.create +controls = Controls.create + +initial_nodes = [ + { + 'id': '1', + 'type': 'input', + 'data': {'label': '150'}, + 'position': {'x': 250, 'y': 25}, + }, + { + 'id': '2', + 'data': {'label': '25'}, + 'position': {'x': 100, 'y': 125}, + }, + { + 'id': '3', + 'type': 'output', + 'data': {'label': '5'}, + 'position': {'x': 250, 'y': 250}, + }, +] + +initial_edges = [ + {'id': 'e1-2', 'source': '1', 'target': '2', 'label': '*', 'animated': True}, + {'id': 'e2-3', 'source': '2', 'target': '3', 'label': '+', 'animated': True}, +] + + +class ReactFlowState(rx.State): + """The app state.""" + nodes: List[Dict[str, Any]] = initial_nodes + edges: List[Dict[str, Any]] = initial_edges + + @rx.event + def add_random_node(self): + new_node_id = f'{len(self.nodes) + 1}' + node_type = random.choice(['default']) + # Label is random number + label = new_node_id + x = random.randint(0, 250) + y = random.randint(0, 250) + + new_node = { + 'id': new_node_id, + 'type': node_type, + 'data': {'label': label}, + 'position': {'x': x, 'y': y}, + 'draggable': True, + } + self.nodes.append(new_node) + + @rx.event + def clear_graph(self): + self.nodes = [] # Clear the nodes list + self.edges = [] # Clear the edges list + + @rx.event + def on_connect(self, new_edge): + # Iterate over the existing edges + for i, edge in enumerate(self.edges): + # If we find an edge with the same ID as the new edge + if edge["id"] == f"e{new_edge['source']}-{new_edge['target']}": + # Delete the existing edge + del self.edges[i] + break + + # Add the new edge + self.edges.append({ + "id": f"e{new_edge['source']}-{new_edge['target']}", + "source": new_edge["source"], + "target": new_edge["target"], + "label": random.choice(["+", "-", "*", "/"]), + "animated": True, + }) + + @rx.event + def on_nodes_change(self, node_changes: List[Dict[str, Any]]): + # Receives a list of Nodes in case of events like dragging + map_id_to_new_position = defaultdict(dict) + + # Loop over the changes and store the new position + for change in node_changes: + if change["type"] == "position" and change.get("dragging") == True: + map_id_to_new_position[change["id"]] = change["position"] + + # Loop over the nodes and update the position + for i, node in enumerate(self.nodes): + if node["id"] in map_id_to_new_position: + new_position = map_id_to_new_position[node["id"]] + self.nodes[i]["position"] = new_position +``` + +Here is an example of the app running: + +```python eval +rx.vstack( + react_flow( + background(), + controls(), + nodes_draggable=True, + nodes_connectable=True, + on_connect=lambda e0: ReactFlowState.on_connect(e0), + on_nodes_change=lambda e0: ReactFlowState.on_nodes_change(e0), + nodes=ReactFlowState.nodes, + edges=ReactFlowState.edges, + fit_view=True, + ), + rx.hstack( + rx.button("Clear graph", on_click=ReactFlowState.clear_graph, width="50%"), + rx.button("Add node", on_click=ReactFlowState.add_random_node, width="50%"), + width="100%", + ), + height="30em", + width="100%", + ) +``` diff --git a/docs/wrapping-react/imports-and-styles.md b/docs/wrapping-react/imports-and-styles.md new file mode 100644 index 00000000000..eb3648cf85c --- /dev/null +++ b/docs/wrapping-react/imports-and-styles.md @@ -0,0 +1,50 @@ +# Styles and Imports + +When wrapping a React component, you may need to define styles and imports that are specific to the component. This is done by defining the `add_styles` and `add_imports` methods in your component class. + +### Imports + +Sometimes, the component you are wrapping will need to import other components or libraries. This is done by defining the `add_imports` method in your component class. +That method should return a dictionary of imports, where the keys are the names of the packages to import and the values are the names of the components or libraries to import. + +Values can be either a string or a list of strings. If the import needs to be aliased, you can use the `ImportVar` object to specify the alias and whether the import should be installed as a dependency. + +```python +from reflex.utils.imports import ImportVar + +class ComponentWithImports(MyBaseComponent): + def add_imports(self): + """Add imports to the component.""" + return { + # If you only have one import, you can use a string. + "my-package1": "my-import1", + # If you have multiple imports, you can pass a list. + "my-package2": ["my-import2"], + # If you need to control the import in a more detailed way, you can use an ImportVar object. + "my-package3": ImportVar(tag="my-import3", alias="my-alias", install=False, is_default=False), + # To import a CSS file, pass the full path to the file, and use an empty string as the key. + "": "my-package-with-css/styles.css", + } +``` + +```md alert info +# The tag and library of the component will be automatically added to the imports. They do not need to be added again in `add_imports`. +``` + +### Styles + +Styles are any CSS styles that need to be included in the component. The style will be added inline to the component, so you can use any CSS styles that are valid in React. + +```python +class StyledComponent(MyBaseComponent): + """MyComponent.""" + + def add_style(self) -> dict[str, Any] | None: + """Add styles to the component.""" + + return rx.Style({ + "backgroundColor": "red", + "color": "white", + "padding": "10px", + }) +``` diff --git a/docs/wrapping-react/library-and-tags.md b/docs/wrapping-react/library-and-tags.md new file mode 100644 index 00000000000..140937e1d14 --- /dev/null +++ b/docs/wrapping-react/library-and-tags.md @@ -0,0 +1,166 @@ +--- +title: Library and Tags +--- + + +# Find The Component + +There are two ways to find a component to wrap: + +1. Write the component yourself locally. +2. Find a well-maintained React library on [npm](https://www.npmjs.com/) that contains the component you need. + +In both cases, the process of wrapping the component is the same except for the `library` field. + +# Wrapping the Component + +To start wrapping your React component, the first step is to create a new component in your Reflex app. This is done by creating a new class that inherits from `rx.Component` or `rx.NoSSRComponent`. + +See the [API Reference](https://reflex.dev/docs/api-reference/component/) for more details on the `rx.Component` class. + +This is when we will define the most important attributes of the component: + +1. **library**: The name of the npm package that contains the component. +2. **tag**: The name of the component to import from the package. +3. **alias**: (Optional) The name of the alias to use for the component. This is useful if multiple component from different package have a name in common. If `alias` is not specified, `tag` will be used. +4. **lib_dependencies**: Any additional libraries needed to use the component. +5. **is_default**: (Optional) If the component is a default export from the module, set this to `True`. Default is `False`. + +Optionally, you can override the default component creation behavior by implementing the `create` class method. Most components won't need this when props are straightforward conversions from Python to JavaScript. However, this is useful when you need to add custom initialization logic, transform props, or handle special cases when the component is created. + +```md alert warning +# When setting the `library` attribute, it is recommended to included a pinned version of the package. Doing so, the package will only change when you intentionally update the version, avoid unexpected breaking changes. +``` + +```python +class MyBaseComponent(rx.Component): + """MyBaseComponent.""" + + # The name of the npm package. + library = "my-library@x.y.z" + + # The name of the component to use from the package. + tag = "MyComponent" + + # Any additional libraries needed to use the component. + lib_dependencies: list[str] = ["package-deps@x.y.z"] + + # The name of the alias to use for the component. + alias = "MyComponentAlias" + + # If the component is a default export from the module, set this to True. + is_default = True/False + + @classmethod + def create(cls, *children, **props): + """Create an instance of MyBaseComponent. + + Args: + *children: The children of the component. + **props: The props of the component. + + Returns: + The component instance. + """ + # Your custom creation logic here + return super().create(*children, **props) + +``` + +# Wrapping a Dynamic Component + +When wrapping some libraries, you may want to use dynamic imports. This is because they may not be compatible with Server-Side Rendering (SSR). + +To handle this in Reflex, subclass `NoSSRComponent` when defining your component. It works the same as `rx.Component`, but it will automatically add the correct custom code for a dynamic import. + +Often times when you see an import something like this: + +```javascript +import dynamic from "next/dynamic"; + +const MyLibraryComponent = dynamic(() => import("./MyLibraryComponent"), { + ssr: false, +}); +``` + +You can wrap it in Reflex like this: + +```python +from reflex.components.component import NoSSRComponent + +class MyLibraryComponent(NoSSRComponent): + """A component that wraps a lib needing dynamic import.""" + + library = "my-library@x.y.z" + + tag="MyLibraryComponent" +``` + +It may not always be clear when a library requires dynamic imports. A few things to keep in mind are if the component is very client side heavy i.e. the view and structure depends on things that are fetched at run time, or if it uses `window` or `document` objects directly it will need to be wrapped as a `NoSSRComponent`. + +Some examples are: + +1. Video and Audio Players +2. Maps +3. Drawing Canvas +4. 3D Graphics +5. QR Scanners +6. Reactflow + +The reason for this is that it does not make sense for your server to render these components as the server does not have access to your camera, it cannot draw on your canvas or render a video from a file. + +In addition, if in the component documentation it mentions nextJS compatibility or server side rendering compatibility, it is a good sign that it requires dynamic imports. + +# Advanced - Parsing a state Var with a JS Function + +When wrapping a component, you may need to parse a state var by applying a JS function to it. + +## Define the parsing function + +First you need to define the parsing function by writing it in `add_custom_code`. + +```python + +def add_custom_code(self) -> list[str]: + """Add custom code to the component.""" + # Define the parsing function + return [ + """ + function myParsingFunction(inputProp) { + // Your parsing logic here + return parsedProp; + }""" + ] +``` + +## Apply the parsing function to your props + +Then, you can apply the parsing function to your props in the `create` method. + +```python +from reflex.vars.base import Var +from reflex.vars.function import FunctionStringVar + + ... + @classmethod + def create(cls, *children, **props): + """Create an instance of MyBaseComponent. + + Args: + *children: The children of the component. + **props: The props of the component. + + Returns: + The component instance. + """ + # Apply the parsing function to the props + if (prop_to_parse := props.get("propsToParse")) is not None: + if isinstance(prop_to_parse, Var): + props["propsToParse"] = FunctionStringVar.create("myParsingFunction").call(prop_to_parse) + else: + # This is not a state Var, so you can parse the value directly in python + parsed_prop = python_parsing_function(prop_to_parse) + props["propsToParse"] = parsed_prop + return super().create(*children, **props) + ... +``` diff --git a/docs/wrapping-react/local-packages.md b/docs/wrapping-react/local-packages.md new file mode 100644 index 00000000000..d7555c10517 --- /dev/null +++ b/docs/wrapping-react/local-packages.md @@ -0,0 +1,171 @@ +--- +title: Wrapping Local Packages +--- + +```python exec +import reflex as rx +``` + +# Assets + +If a wrapped component depends on assets such as images, scripts, or +stylesheets, these can be kept adjacent to the component code and +included in the final build using the `rx.asset` function. + +`rx.asset` returns a relative path that references the asset in the compiled +output. The target files are copied into a subdirectory of `assets/external` +based on the module where they are initially used. This allows third-party +components to have external assets with the same name without conflicting +with each other. + +For example, if there is an SVG file named `wave.svg` in the same directory as +this component, it can be rendered using `rx.image` and `rx.asset`. + +```python +class Hello(rx.Component): + @classmethod + def create(cls, *children, **props) -> rx.Component: + props.setdefault("align", "center") + return rx.hstack( + rx.image(src=rx.asset("wave.svg", shared=True), width="50px", height="50px"), + rx.heading("Hello ", *children), + **props + ) +``` + +# Local Components + +You can also wrap components that you have written yourself. For local components (when the code source is directly in the project), we recommend putting it beside the files that is wrapping it. + +If there is a file `hello.jsx` in the same directory as the component with this content: + +```javascript +// /path/to/components/hello.jsx +import React from "react"; + +export function Hello({ name, onGreet }) { + return ( +
+

Hello, {name}!

+ +
+ ); +} +``` + +The python app can use the `rx.asset` helper to copy the component source into +the generated frontend, after which the `library` path in the `rx.Component` may +be specified by prefixing `$/public` to that path returned by `rx.asset`. + +```python +import reflex as rx + +hello_path = rx.asset("./hello.jsx", shared=True) +hello_css_path = rx.asset("./hello.css", shared=True) + +class Hello(rx.Component): + # Use an absolute path starting with $/public + library = f"$/public{hello_path}" + + # Define everything else as normal. + tag = "Hello" + + name: rx.Var[str] = rx.Var.create("World") + on_greet: rx.EventHandler[rx.event.passthrough_event_spec(str)] + + # Include any related CSS files with rx.asset to ensure they are copied. + def add_imports(self): + return {"": f"$/public/{hello_css_path}"} +``` + +## Considerations + +When wrapping local components, keep the following in mind: + +1. **File Extensions**: Ensure that the file extensions are correct (e.g., `.jsx` for React components and `.tsx` for TypeScript components). +2. **Asset Management**: Use `rx.asset` with `shared=True` to manage any assets (e.g., images, styles) that the component depends on. +3. **Event Handling**: Define any event handlers (e.g., `on_greet`) as part of the component's API and pass those to the component _from the Reflex app_. Do not attempt to hook into Reflex's event system directly from Javascript. + +## Use Case + +Local components are useful when shimming small pieces of functionality that are +simpler or more performant when implemented directly in Javascript, such as: + +- Spammy events: keys, touch, mouse, scroll -- these are often better processed on the client side. +- Using canvas, graphics or WebGPU +- Working with other Web APIs like storage, screen capture, audio/midi +- Integrating with complex third-party libraries + - For application-specific use, it may be easier to wrap a local component that + provides the needed subset of the library's functionality in a simpler API for use in Reflex. + +# Local Packages + +If the component is part of a local package, available on Github, or +downloadable via a web URL, it can also be wrapped in Reflex. Specify the path +or URL after an `@` following the package name. + +Any local paths are relative to the `.web` folder, so you can use `../` prefix +to reference the Reflex project root. + +Some examples of valid specifiers for a package called +[`@masenf/hello-react`](https://github.com/masenf/hello-react) are: + +- GitHub: `@masenf/hello-react@github:masenf/hello-react` +- URL: `@masenf/hello-react@https://github.com/masenf/hello-react/archive/refs/heads/main.tar.gz` +- Local Archive: `@masenf/hello-react@../hello-react.tgz` +- Local Directory: `@masenf/hello-react@../hello-react` + +It is important that the package name matches the name in `package.json` so +Reflex can generate the correct import statement in the generated javascript +code. + +These package specifiers can be used for `library` or `lib_dependencies`. + +```python demo exec toggle +class GithubComponent(rx.Component): + library = "@masenf/hello-react@github:masenf/hello-react" + tag = "Counter" + + def add_imports(self): + return { + "": ["@masenf/hello-react/dist/style.css"] + } + +def github_component_example(): + return GithubComponent.create() +``` + +Although more complicated, this approach is useful when the local components +have additional dependencies or build steps required to prepare the component +for use. + +Some important notes regarding this approach: + +- The repo or archive must contain a `package.json` file. +- `prepare` or `build` scripts will NOT be executed. The distribution archive, + directory, or repo must already contain the built javascript files (this is common). + +````md alert +# Ensure CSS files are exported in `package.json` + +In addition to exporting the module containing the component, any CSS files +intended to be imported by the wrapped component must also be listed in the +`exports` key of `package.json`. + +```json +{ + // ..., + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.umd.cjs" + }, + "./dist/style.css": { + "import": "./dist/style.css", + "require": "./dist/style.css" + } + } + // ... +} +``` +```` diff --git a/docs/wrapping-react/more-wrapping-examples.md b/docs/wrapping-react/more-wrapping-examples.md new file mode 100644 index 00000000000..e1dff462482 --- /dev/null +++ b/docs/wrapping-react/more-wrapping-examples.md @@ -0,0 +1,444 @@ +# More React Libraries + +## AG Charts + +Here we wrap the AG Charts library from the NPM package [ag-charts-react](https://www.npmjs.com/package/ag-charts-react). + +In the react code below we can see the first `2` lines are importing React and ReactDOM, and this can be ignored when wrapping your component. + +We import the `AgCharts` component from the `ag-charts-react` library on line 5. In Reflex this is wrapped by `library = "ag-charts-react"` and `tag = "AgCharts"`. + +Line `7` defines a functional React component, which on line `26` returns `AgCharts` which is similar in the Reflex code to using the `chart` component. + +Line `9` uses the `useState` hook to create a state variable `chartOptions` and its setter function `setChartOptions` (equivalent to the event handler `set_chart_options` in reflex). The initial state variable is of type dict and has two key value pairs `data` and `series`. + +When we see `useState` in React code, it correlates to state variables in your State. As you can see in our Reflex code we have a state variable `chart_options` which is a dictionary, like in our React code. + +Moving to line `26` we see that the `AgCharts` has a prop `options`. In order to use this in Reflex we must wrap this prop. We do this with `options: rx.Var[dict]` in the `AgCharts` component. + +Lines `31` and `32` are rendering the component inside the root element. This can be ignored when we are wrapping a component as it is done in Reflex by creating an `index` function and adding it to the app. + +---md tabs + +--tab React Code + +```javascript +1 | import React, \{ useState } from 'react'; +2 | import ReactDOM from 'react-dom/client'; +3 | +4 | // React Chart Component +5 | import \{ AgCharts } from 'ag-charts-react'; +6 | +7 | const ChartExample = () => { +8 | // Chart Options: Control & configure the chart +9 | const [chartOptions, setChartOptions] = useState({ +10| // Data: Data to be displayed in the chart +11| data: [ +12| \{ month: 'Jan', avgTemp: 2.3, iceCreamSales: 162000 }, +13| \{ month: 'Mar', avgTemp: 6.3, iceCreamSales: 302000 }, +14| \{ month: 'May', avgTemp: 16.2, iceCreamSales: 800000 }, +15| \{ month: 'Jul', avgTemp: 22.8, iceCreamSales: 1254000 }, +16| \{ month: 'Sep', avgTemp: 14.5, iceCreamSales: 950000 }, +17| \{ month: 'Nov', avgTemp: 8.9, iceCreamSales: 200000 }, +18| ], +19| // Series: Defines which chart type and data to use +20| series: [\{ type: 'bar', xKey: 'month', yKey: 'iceCreamSales' }], +21| }); +22| +23| // React Chart Component +24| return ( +25| // AgCharts component with options passed as prop +26| +27| ); +28| } +29| +30| // Render component inside root element +31| const root = ReactDOM.createRoot(document.getElementById('root')); +32| root.render(); +``` + +-- +--tab Reflex Code + +```python +import reflex as rx + +class AgCharts(rx.Component): + """ A simple line chart component using AG Charts """ + + library = "ag-charts-react" + + tag = "AgCharts" + + options: rx.Var[dict] + + +chart = AgCharts.create + + +class State(rx.State): + """The app state.""" + chart_options: dict = { + "data": [ + \{"month":"Jan", "avgTemp":2.3, "iceCreamSales":162000}, + \{"month":"Mar", "avgTemp":6.3, "iceCreamSales":302000}, + \{"month":"May", "avgTemp":16.2, "iceCreamSales":800000}, + \{"month":"Jul", "avgTemp":22.8, "iceCreamSales":1254000}, + \{"month":"Sep", "avgTemp":14.5, "iceCreamSales":950000}, + \{"month":"Nov", "avgTemp":8.9, "iceCreamSales":200000} + ], + "series": [\{"type":"bar", "xKey":"month", "yKey":"iceCreamSales"}] + } + +def index() -> rx.Component: + return chart( + options=State.chart_options, + ) + +app = rx.App() +app.add_page(index) +``` + +-- + +--- + +## React Leaflet + + +In this example we are wrapping the React Leaflet library from the NPM package [react-leaflet](https://www.npmjs.com/package/react-leaflet). + +On line `1` we import the `dynamic` function from Next.js and on line `21` we set `ssr: false`. Lines `4` and `6` use the `dynamic` function to import the `MapContainer` and `TileLayer` components from the `react-leaflet` library. This is used to dynamically import the `MapContainer` and `TileLayer` components from the `react-leaflet` library. This is done in Reflex by using the `NoSSRComponent` class when defining the component. There is more information of when this is needed on the `Dynamic Imports` section of this [page](/docs/wrapping-react/library-and-tags). + +It mentions in the documentation that it is necessary to include the Leaflet CSS file, which is added on line `2` in the React code below. This can be done in Reflex by using the `add_imports` method in the `MapContainer` component. We can add a relative path from within the React library or a full URL to the CSS file. + +Line `4` defines a functional React component, which on line `8` returns the `MapContainer` which is done in the Reflex code using the `map_container` component. + +The `MapContainer` component has props `center`, `zoom`, `scrollWheelZoom`, which we wrap in the `MapContainer` component in the Reflex code. We ignore the `style` prop as it is a reserved name in Reflex. We can use the `rename_props` method to change the name of the prop, as we will see in the React PDF Renderer example, but in this case we just ignore it and add the `width` and `height` props as css in Reflex. + +The `TileLayer` component has a prop `url` which we wrap in the `TileLayer` component in the Reflex code. + +Lines `24` and `25` defines and exports a React functional component named `Home` which returns the `MapComponent` component. This can be ignored in the Reflex code when wrapping the component as we return the `map_container` component in the `index` function. + +---md tabs + +--tab React Code + +```javascript +1 | import dynamic from "next/dynamic"; +2 | import "leaflet/dist/leaflet.css"; +3 | +4 | const MapComponent = dynamic( +5 | () => { +6 | return import("react-leaflet").then((\{ MapContainer, TileLayer }) => { +7 | return () => ( +8 | +14| +17| +18| ); +19| }); +20| }, +21| \{ ssr: false } +22| ); +23| +24| export default function Home() { +25| return ; +26| } +``` + +-- +--tab Reflex Code + +```python +import reflex as rx + +class MapContainer(rx.NoSSRComponent): + + library = "react-leaflet" + + tag = "MapContainer" + + center: rx.Var[list] + + zoom: rx.Var[int] + + scroll_wheel_zoom: rx.Var[bool] + + # Can also pass a url like: https://unpkg.com/leaflet/dist/leaflet.css + def add_imports(self): + return \{"": ["leaflet/dist/leaflet.css"]} + + + +class TileLayer(rx.NoSSRComponent): + + library = "react-leaflet" + + tag = "TileLayer" + + url: rx.Var[str] + + +map_container = MapContainer.create +tile_layer = TileLayer.create + +def index() -> rx.Component: + return map_container( + tile_layer(url="https://\{s}.tile.openstreetmap.org/\{z}/\{x}/\{y}.png"), + center=[51.505, -0.09], + zoom=13, + #scroll_wheel_zoom=True + width="100%", + height="50vh", + ) + + +app = rx.App() +app.add_page(index) + +``` + +-- + +--- + +## React PDF Renderer + +In this example we are wrapping the React renderer for creating PDF files on the browser and server from the NPM package [@react-pdf/renderer](https://www.npmjs.com/package/@react-pdf/renderer). + +This example is similar to the previous examples, and again Dynamic Imports are required for this library. This is done in Reflex by using the `NoSSRComponent` class when defining the component. There is more information on why this is needed on the `Dynamic Imports` section of this [page](/docs/wrapping-react/library-and-tags). + +The main difference with this example is that the `style` prop, used on lines `20`, `21` and `24` in React code, is a reserved name in Reflex so can not be wrapped. A different name must be used when wrapping this prop and then this name must be changed back to the original with the `rename_props` method. In this example we name the prop `theme` in our Reflex code and then change it back to `style` with the `rename_props` method in both the `Page` and `View` components. + +```md alert info +# List of reserved names in Reflex + +_The style of the component._ + +`style: Style = Style()` + +_A mapping from event triggers to event chains._ + +`event_triggers: Dict[str, Union[EventChain, Var]] = \{}` + +_The alias for the tag._ + +`alias: Optional[str] = None` + +_Whether the import is default or named._ + +`is_default: Optional[bool] = False` + +_A unique key for the component._ + +`key: Any = None` + +_The id for the component._ + +`id: Any = None` + +_The class name for the component._ + +`class_name: Any = None` + +_Special component props._ + +`special_props: List[Var] = []` + +_Whether the component should take the focus once the page is loaded_ + +`autofocus: bool = False` + +_components that cannot be children_ + +`_invalid_children: List[str] = []` + +_only components that are allowed as children_ + +`_valid_children: List[str] = []` + +_only components that are allowed as parent_ + +`_valid_parents: List[str] = []` + +_props to change the name of_ + +`_rename_props: Dict[str, str] = \{}` + +_custom attribute_ + +`custom_attrs: Dict[str, Union[Var, str]] = \{}` + +_When to memoize this component and its children._ + +`_memoization_mode: MemoizationMode = MemoizationMode()` + +_State class associated with this component instance_ + +`State: Optional[Type[reflex.state.State]] = None` +``` + +---md tabs + +--tab React Code + +```javascript +1 | import ReactDOM from 'react-dom'; +2 | import \{ Document, Page, Text, View, StyleSheet, PDFViewer } from '@react-pdf/renderer'; +3 | +4 | // Create styles +5 | const styles = StyleSheet.create({ +6 | page: { +7 | flexDirection: 'row', +8 | backgroundColor: '#E4E4E4', +9 | }, +10| section: { +11| margin: 10, +12| padding: 10, +13| flexGrow: 1, +14| }, +15| }); +16| +17| // Create Document Component +18| const MyDocument = () => ( +19| +20| +21| +22| Section #1 +23| +24| +25| Section #2 +26| +27| +28| +29| ); +30| +31| const App = () => ( +32| +33| +34| +35| ); +36| +37| ReactDOM.render(, document.getElementById('root')); +``` + +-- +--tab Reflex Code + +```python +import reflex as rx + +class Document(rx.Component): + + library = "@react-pdf/renderer" + + tag = "Document" + + +class Page(rx.Component): + + library = "@react-pdf/renderer" + + tag = "Page" + + size: rx.Var[str] + # here we are wrapping style prop but as style is a reserved name in Reflex we must name it something else and then change this name with rename props method + theme: rx.Var[dict] + + _rename_props: dict[str, str] = { + "theme": "style", + } + + +class Text(rx.Component): + + library = "@react-pdf/renderer" + + tag = "Text" + + +class View(rx.Component): + + library = "@react-pdf/renderer" + + tag = "View" + + # here we are wrapping style prop but as style is a reserved name in Reflex we must name it something else and then change this name with rename props method + theme: rx.Var[dict] + + _rename_props: dict[str, str] = { + "theme": "style", + } + + +class StyleSheet(rx.Component): + + library = "@react-pdf/renderer" + + tag = "StyleSheet" + + page: rx.Var[dict] + + section: rx.Var[dict] + + +class PDFViewer(rx.NoSSRComponent): + + library = "@react-pdf/renderer" + + tag = "PDFViewer" + + +document = Document.create +page = Page.create +text = Text.create +view = View.create +style_sheet = StyleSheet.create +pdf_viewer = PDFViewer.create + + +styles = style_sheet({ + "page": { + "flexDirection": 'row', + "backgroundColor": '#E4E4E4', + }, + "section": { + "margin": 10, + "padding": 10, + "flexGrow": 1, + }, +}) + + +def index() -> rx.Component: + return pdf_viewer( + document( + page( + view( + text("Hello, World!"), + theme=styles.section, + ), + view( + text("Hello, 2!"), + theme=styles.section, + ), + size="A4", theme=styles.page), + ), + width="100%", + height="80vh", + ) + +app = rx.App() +app.add_page(index) +``` + +-- + +--- diff --git a/docs/wrapping-react/overview.md b/docs/wrapping-react/overview.md new file mode 100644 index 00000000000..7e05df852bc --- /dev/null +++ b/docs/wrapping-react/overview.md @@ -0,0 +1,148 @@ +```python exec +import reflex as rx +from typing import Any +``` + +# Wrapping React + +One of Reflex's most powerful features is the ability to wrap React components and take advantage of the vast ecosystem of React libraries. + +If you want a specific component for your app but Reflex doesn't provide it, there's a good chance it's available as a React component. Search for it on [npm](https://www.npmjs.com/), and if it's there, you can use it in your Reflex app. You can also create your own local React components and wrap them in Reflex. + +Once you wrap your component, you [publish it](/docs/custom-components/overview) to the Reflex library so that others can use it. + +## Simple Example + +Simple components that don't have any interaction can be wrapped with just a few lines of code. + +Below we show how to wrap the [Spline](https://github.com/splinetool/react-spline) library can be used to create 3D scenes and animations. + +```python demo exec +import reflex as rx + +class Spline(rx.Component): + """Spline component.""" + + # The name of the npm package. + library = "@splinetool/react-spline" + + # Any additional libraries needed to use the component. + lib_dependencies: list[str] = ["@splinetool/runtime@1.5.5"] + + # The name of the component to use from the package. + tag = "Spline" + + # Spline is a default export from the module. + is_default = True + + # Any props that the component takes. + scene: rx.Var[str] + +# Convenience function to create the Spline component. +spline = Spline.create + +# Use the Spline component in your app. +def index(): + return spline(scene="https://prod.spline.design/joLpOOYbGL-10EJ4/scene.splinecode") +``` + +## ColorPicker Example + +Similar to the Spline example we start with defining the library and tag. In this case the library is `react-colorful` and the tag is `HexColorPicker`. + +We also have a var `color` which is the current color of the color picker. + +Since this component has interaction we must specify any event triggers that the component takes. The color picker has a single trigger `on_change` to specify when the color changes. This trigger takes in a single argument `color` which is the new color. + +```python exec +from reflex.components.component import NoSSRComponent + +class ColorPicker(NoSSRComponent): + library = "react-colorful" + tag = "HexColorPicker" + color: rx.Var[str] + on_change: rx.EventHandler[lambda color: [color]] + +color_picker = ColorPicker.create + +ColorPickerState = rx._x.client_state(default="#db114b", var_name="color") +``` + +```python eval +rx.box( + ColorPickerState, + rx.vstack( + rx.heading(ColorPickerState.value, color="white"), + color_picker( + on_change=ColorPickerState.set_value + ), + ), + background_color=ColorPickerState.value, + padding="5em", + border_radius="12px", + margin_bottom="1em", +) +``` + +```python +from reflex.components.component import NoSSRComponent + +class ColorPicker(NoSSRComponent): + library = "react-colorful" + tag = "HexColorPicker" + color: rx.Var[str] + on_change: rx.EventHandler[lambda color: [color]] + +color_picker = ColorPicker.create + +class ColorPickerState(rx.State): + color: str = "#db114b" + +def index(): + return rx.box( + rx.vstack( + rx.heading(ColorPickerState.color, color="white"), + color_picker( + on_change=ColorPickerState.set_color + ), + ), + background_color=ColorPickerState.color, + padding="5em", + border_radius="1em", + ) +``` + +## What Not To Wrap + +There are some libraries on npm that are not do not expose React components and therefore are very hard to wrap with Reflex. + +A library like [spline](https://www.npmjs.com/package/@splinetool/runtime) below is going to be difficult to wrap with Reflex because it does not expose a React component. + +```javascript +import \{ Application } from '@splinetool/runtime'; + +// make sure you have a canvas in the body +const canvas = document.getElementById('canvas3d'); + +// start the application and load the scene +const spline = new Application(canvas); +spline.load('https://prod.spline.design/6Wq1Q7YGyM-iab9i/scene.splinecode'); +``` + +You should look out for JSX, a syntax extension to JavaScript, which has angle brackets `(

Hello, world!

)`. If you see JSX, it's likely that the library is a React component and can be wrapped with Reflex. + +If the library does not expose a react component you need to try and find a JS React wrapper for the library, such as [react-spline](https://www.npmjs.com/package/@splinetool/react-spline). + +```javascript +import Spline from "@splinetool/react-spline"; + +export default function App() { + return ( +
+ +
+ ); +} +``` + +In the next page, we will go step by step through a more complex example of wrapping a React component. diff --git a/docs/wrapping-react/props.md b/docs/wrapping-react/props.md new file mode 100644 index 00000000000..207fa0f1fd1 --- /dev/null +++ b/docs/wrapping-react/props.md @@ -0,0 +1,205 @@ +--- +title: Props - Wrapping React +--- + +# Props + +When wrapping a React component, you want to define the props that will be accepted by the component. +This is done by defining the props and annotating them with a `rx.Var`. + +Broadly, there are three kinds of props you can encounter when wrapping a React component: + +1. **Simple Props**: These are props that are passed directly to the component. They can be of any type, including strings, numbers, booleans, and even lists or dictionaries. +2. **Callback Props**: These are props that expect to receive a function. That function will usually be called by the component as a callback. (This is different from event handlers.) +3. **Component Props**: These are props that expect to receive a components themselves. They can be used to create more complex components by composing them together. +4. **Event Handlers**: These are props that expect to receive a function that will be called when an event occurs. They are defined as `rx.EventHandler` with a signature function to define the spec of the event. + +## Simple Props + +Simple props are the most common type of props you will encounter when wrapping a React component. They are passed directly to the component and can be of any type (but most commonly strings, numbers, booleans, and structures). + +For custom types, you can use `TypedDict` to define the structure of the custom types. However, if you need the attributes to be automatically converted to camelCase once compiled in JS, you can use `rx.PropsBase` instead of `TypedDict`. + +```python +class CustomReactType(TypedDict): + """Custom React type.""" + + # Define the structure of the custom type to match the Javascript structure. + attribute1: str + attribute2: bool + attribute3: int + + +class CustomReactType2(rx.PropsBase): + """Custom React type.""" + + # Define the structure of the custom type to match the Javascript structure. + attr_foo: str # will be attrFoo in JS + attr_bar: bool # will be attrBar in JS + attr_baz: int # will be attrBaz in JS + +class SimplePropsComponent(MyBaseComponent): + """MyComponent.""" + + # Type the props according the component documentation. + + # props annotated as `string` in javascript + prop1: rx.Var[str] + + # props annotated as `number` in javascript + prop2: rx.Var[int] + + # props annotated as `boolean` in javascript + prop3: rx.Var[bool] + + # props annotated as `string[]` in javascript + prop4: rx.Var[list[str]] + + # props annotated as `CustomReactType` in javascript + props5: rx.Var[CustomReactType] + + # props annotated as `CustomReactType2` in javascript + props6: rx.Var[CustomReactType2] + + # Sometimes a props will accept multiple types. You can use `|` to specify the types. + # props annotated as `string | boolean` in javascript + props7: rx.Var[str | bool] +``` + +## Callback Props + +Callback props are used to handle events or to pass data back to the parent component. They are defined as `rx.Var` with a type of `FunctionVar` or `Callable`. + +```python +from typing import Callable +from reflex.vars.function import FunctionVar + +class CallbackPropsComponent(MyBaseComponent): + """MyComponent.""" + + # A callback prop that takes a single argument. + callback_props: rx.Var[Callable] +``` + +## Component Props + +Some components will occasionally accept other components as props, usually annotated as `ReactNode`. In Reflex, these are defined as `rx.Component`. + +```python +class ComponentPropsComponent(MyBaseComponent): + """MyComponent.""" + + # A prop that takes a component as an argument. + component_props: rx.Var[rx.Component] +``` + +## Event Handlers + +Event handlers are props that expect to receive a function that will be called when an event occurs. They are defined as `rx.EventHandler` with a signature function to define the spec of the event. + +```python +from reflex.vars.event_handler import EventHandler +from reflex.vars.function import FunctionVar +from reflex.vars.object import ObjectVar + +class InputEventType(TypedDict): + """Input event type.""" + + # Define the structure of the input event. + foo: str + bar: int + +class OutputEventType(TypedDict): + """Output event type.""" + + # Define the structure of the output event. + baz: str + qux: int + + +def custom_spec1(event: ObjectVar[InputEventType]) -> tuple[str, int]: + """Custom event spec using ObjectVar with custom type as input and tuple as output.""" + return ( + event.foo.to(str), + event.bar.to(int), + ) + +def custom_spec2(event: ObjectVar[dict]) -> tuple[Var[OutputEventType]]: + """Custom event spec using ObjectVar with dict as input and custom type as output.""" + return Var.create( + { + "baz": event["foo"], + "qux": event["bar"], + }, + ).to(OutputEventType) + +class EventHandlerComponent(MyBaseComponent): + """MyComponent.""" + + # An event handler that take no argument. + on_event: rx.EventHandler[rx.event.no_args_event_spec] + + # An event handler that takes a single string argument. + on_event_with_arg: rx.EventHandler[rx.event.passthrough_event_spec(str)] + + # An event handler specialized for input events, accessing event.target.value from the event. + on_input_change: rx.EventHandler[rx.event.input_event] + + # An event handler specialized for key events, accessing event.key from the event and provided modifiers (ctrl, alt, shift, meta). + on_key_down: rx.EventHandler[rx.event.key_event] + + # An event handler that takes a custom spec. (Event handler must expect a tuple of two values [str and int]) + on_custom_event: rx.EventHandler[custom_spec1] + + # Another event handler that takes a custom spec. (Event handler must expect a tuple of one value, being a OutputEventType) + on_custom_event2: rx.EventHandler[custom_spec2] +``` + +```md alert info +# Custom event specs have a few use case where they are particularly useful. If the event returns non-serializable data, you can filter them out so the event can be sent to the backend. You can also use them to transform the data before sending it to the backend. +``` + +### Emulating Event Handler Behavior Outside a Component + +In some instances, you may need to replicate the special behavior applied to +event handlers from outside of a component context. For example if the component +to be wrapped requires event callbacks passed in a dictionary, this can be +achieved by directly instantiating an `EventChain`. + +A real-world example of this is the `onEvents` prop of +[`echarts-for-react`](https://www.npmjs.com/package/echarts-for-react) library, +which, unlike a normal event handler, expects a mapping of event handlers like: + +```javascript + +``` + +To achieve this in Reflex, you can create an explicit `EventChain` for each +event handler: + +```python +@classmethod +def create(cls, *children, **props): + on_events = props.pop("on_events", {}) + + event_chains = {} + for event_name, handler in on_events.items(): + # Convert the EventHandler/EventSpec/lambda to an EventChain + event_chains[event_name] = rx.EventChain.create( + handler, + args_spec=rx.event.no_args_event_spec, + key=event_name, + ) + if on_events: + props["on_events"] = event_chains + + # Create the component instance + return super().create(*children, **props) +``` diff --git a/docs/wrapping-react/serializers.md b/docs/wrapping-react/serializers.md new file mode 100644 index 00000000000..4bb1abcffd0 --- /dev/null +++ b/docs/wrapping-react/serializers.md @@ -0,0 +1,44 @@ +--- +title: Serializers +--- + +# Serializers + +Vars can be any type that can be serialized to JSON. This includes primitive types like strings, numbers, and booleans, as well as more complex types like lists, dictionaries, and dataframes. + +In case you need to serialize a more complex type, you can use the `serializer` decorator to convert the type to a primitive type that can be stored in the state. Just define a method that takes the complex type as an argument and returns a primitive type. We use type annotations to determine the type that you want to serialize. + +For example, the Plotly component serializes a plotly figure into a JSON string that can be stored in the state. + +```python +import json +import reflex as rx +from plotly.graph_objects import Figure +from plotly.io import to_json + +# Use the serializer decorator to convert the figure to a JSON string. +# Specify the type of the argument as an annotation. +@rx.serializer +def serialize_figure(figure: Figure) -> list: + # Use Plotly's to_json method to convert the figure to a JSON string. + return json.loads(to_json(figure))["data"] +``` + +We can then define a var of this type as a prop in our component. + +```python +import reflex as rx +from plotly.graph_objects import Figure + +class Plotly(rx.Component): + """Display a plotly graph.""" + library = "react-plotly.js@2.6.0" + lib_dependencies: List[str] = ["plotly.js@2.22.0"] + + tag = "Plot" + + is_default = True + + # Since a serialize is defined now, we can use the Figure type directly. + data: rx.Var[Figure] +``` diff --git a/reflex/.templates/apps/blank/code/__init__.py b/docs/wrapping-react/step-by-step.md similarity index 100% rename from reflex/.templates/apps/blank/code/__init__.py rename to docs/wrapping-react/step-by-step.md diff --git a/docs/zh/zh_cn/README.md b/docs/zh/zh_cn/README.md deleted file mode 100644 index e3da05f034f..00000000000 --- a/docs/zh/zh_cn/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -### **✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![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) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/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.10+): - -```bash -pip install reflex -``` - -## 🥳 创建您的第一个应用程序 - -安装 Reflex 的同时也会安装 `reflex` 命令行工具. - -通过创建一个新项目来测试是否安装成功(请把 my_app_name 替代为您的项目名字): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -这段命令会在新文件夹初始化一个应用程序模板. - -您可以在开发者模式下运行这个应用程序: - -```bash -reflex run -``` - -您可以看到您的应用程序运行在 http://localhost:3000. - -现在您可以在以下位置修改代码 `my_app_name/my_app_name.py`,Reflex 拥有快速刷新(fast refresh),所以您可以在保存代码后马上看到更改. - -## 🫧 范例 - -让我们来看一个例子: 创建一个使用 [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node) 进行图像生成的图形界面.为了保持范例简单,我们只使用 OpenAI API,但是您可以将其替换成本地端的 ML 模型. - -  - -
-DALL·E的前端界面, 展示了图片生成的进程 -
- -  - -这是这个范例的完整代码,只需要一个 Python 文件就可以完成! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## 让我们分解以上步骤. - -
-解释 DALL-E app 的前端和后端部分的区别。 -
- -### **Reflex UI** - -让我们从 UI 开始. - -```python -def index(): - return rx.center( - ... - ) -``` - -这个 `index` 函数定义了应用程序的前端. - -我们用不同的组件比如 `center`, `vstack`, `input`, 和 `button` 来创建前端, 组件之间可以相互嵌入,来创建复杂的布局. -并且您可以使用关键字参数来使用 CSS 的全部功能. - -Reflex 拥有 [60+ 个内置组件](https://reflex.dev/docs/library) 来帮助您开始创建应用程序. 我们正在积极添加组件, 但是您也可以容易的 [创建自己的组件](https://reflex.dev/docs/wrapping-react/overview/). - -### **State** - -Reflex 用 State 来渲染您的 UI. - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -State 定义了所有可能会发生变化的变量(称为 vars)以及能够改变这些变量的函数. - -在这个范例中,State 由 `prompt` 和 `image_url` 组成.此外,State 还包含有两个布尔值 `processing` 和 `complete`,用于指示何时显示循环进度指示器和图像. - -### **Event Handlers** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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 将在事件处理器结束时进行更新. - -### **Routing** - -最后,定义我们的应用程序. - -```python -app = rx.App() -``` - -我们添加从应用程序根目录到 index 组件的路由.我们还添加了一个在页面预览或浏览器标签中显示的标题. - -```python -app.add_page(index, title="DALL-E") -``` - -您可以通过增加更多页面来创建一个多页面的应用. - -## 📑 资源 - -
- -📑 [文档](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [日志](https://reflex.dev/blog)   |   📱 [组件库](https://reflex.dev/docs/library)   |   🖼️ [模板](https://reflex.dev/templates/)   |   🛸 [部署](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Reflex 的状态 - -Reflex 于 2022 年 12 月以 Pynecone 的名称推出. - -从 2025 年开始,[Reflex Cloud](https://cloud.reflex.dev)已经推出,为 Reflex 应用提供最佳的托管体验。我们将继续开发并实现更多功能。 - -Reflex 每周都有新功能和发布新版本! 确保您按下 :star: 收藏和 :eyes: 关注 这个 仓库来确保知道最新信息. - -## 贡献 - -我们欢迎任何大小的贡献,以下是几个好的方法来加入 Reflex 社群. - -- **加入我们的 Discord**: 我们的 [Discord](https://discord.gg/T5WSbC2YtQ) 是帮助您加入 Reflex 项目和讨论或贡献最棒的地方. -- **GitHub Discussions**: 一个来讨论您想要添加的功能或是需要澄清的事情的好地方. -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)是报告错误的绝佳地方,另外您可以试着解决一些现有 issue 并提交 PR. - -我们正在积极寻找贡献者,无关您的技能或经验水平. 若要贡献,请查看[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## 感谢我们所有的贡献者: - - - - - -## 授权 - -Reflex 是一个开源项目,使用 [Apache License 2.0](/LICENSE) 授权. diff --git a/docs/zh/zh_tw/README.md b/docs/zh/zh_tw/README.md deleted file mode 100644 index 606e483ee09..00000000000 --- a/docs/zh/zh_tw/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -**✨ 使用 Python 建立高效且可自訂的網頁應用程式,幾秒鐘內即可部署。✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![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) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫。 - -主要特色: - -- **純 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 如何在底層運作。 - -## ⚙️ 安裝 - -開啟一個終端機並且執行 (需要 Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 建立你的第一個應用程式 - -安裝 Reflex 的同時也會安裝 `reflex` 命令行工具。 - -通過創建一個新專案來測試是否安裝成功。(把 my_app_name 作為新專案名稱): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -此命令會初始化一個應用程式模板在你的新資料夾中。 - -你可以在開發者模式運行這個應用程式: - -```bash -reflex run -``` - -你可以看到你的應用程式運行在 http://localhost:3000。 - -現在在以下位置修改原始碼 `my_app_name/my_app_name.py`,Reflex 擁有快速刷新功能,存儲程式碼後便可立即看到改變。 - -## 🫧 範例應用程式 - -讓我們來看一個例子: 建立一個使用 DALL·E 的圖形使用者介面,為了保持範例簡單,我們只呼叫 OpenAI API,而這部份可以置換掉,改為執行成本地端的 ML 模型。 - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -下方為該應用之完整程式碼,這一切都只需要一個 Python 檔案就能作到! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """應用程式狀態""" - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """透過提示詞取得圖片""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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", 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, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# 把狀態跟頁面添加到應用程式。 -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## 讓我們來拆解一下。 - -
-解釋 DALL-E app 的前端和後端部分的區別。 -
- -### **Reflex 使用者介面** - -讓我們從使用介面開始。 - -```python -def index(): - return rx.center( - ... - ) -``` - -這個 `index` 函式定義了應用程式的前端. - -我們用不同的元件像是 `center`, `vstack`, `input`, 和 `button` 來建立前端,元件之間可互相套入以建立出複雜的版面配置。並且您可使用關鍵字引數 _keyword args_ 運行 CSS 全部功能來設計這些元件們的樣式。 - -Reflex 擁有 [60+ 內建元件](https://reflex.dev/docs/library) 來幫助你開始建立應用程式。我們正積極添加元件,你也可以簡單地 [創建自己所屬的元件](https://reflex.dev/docs/wrapping-react/overview/)。 - -### **應用程式狀態** - -Reflex 使用應用程式狀態中的函式來渲染你的 UI。 - -```python -class State(rx.State): - """應用程式狀態""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -應用程式狀態定義了應用程式中所有可以更改的變數及變更他們的函式 (稱為 vars)。 - -這裡的狀態由 `prompt` 和 `image_url`組成, 以及布林變數 `processing` 和 `complete` 來指示何時顯示進度條及圖片。 - -### **事件處理程序** - -```python -def get_image(self): - """透過提示詞取得圖片""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - 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 -``` - -在應用程式狀態中,我們定義稱之為事件處理程序的函式來改變其 vars. 事件處理程序是我們用來改變 Reflex 應用程式狀態的方法。 - -當使用者動作被響應時,對應的事件處理程序就會被呼叫。點擊按鈕或是文字框輸入都是使用者動作,它們被稱之為事件。 - -我們的 DALL·E. 應用程式有一個事件處理程序 `get_image`,它透過 Open AI API 取得圖片。在事件處理程序中使用 `yield` 將讓使用者介面中途更新,若不使用的話,使用介面只能在事件處理程序結束時才更新。 - -### **路由** - -最後,我們定義我們的應用程式 app。 - -```python -app = rx.App() -``` - -添加從應用程式根目錄(root of the app) 到 index 元件的路由。 我們也添加了一個標題將會顯示在 預覽/瀏覽 分頁。 - -```python -app.add_page(index, title="DALL-E") -``` - -你可以添加更多頁面至路由藉此來建立多頁面應用程式(multi-page app) - -## 📑 資源 - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ 產品狀態 - -Reflex 在 2022 年 12 月以 Pynecone 的名字推出。 - -自 2025 年起,[Reflex Cloud](https://cloud.reflex.dev) 已推出,為 Reflex 應用程式提供最佳的託管體驗。我們將繼續開發並實施更多功能。 - -Reflex 每周都有新功能和釋出新版本! 確保你按下 :star: 和 :eyes: watch 這個 repository 來確保知道最新資訊。 - -## 貢獻 - -我們歡迎任何大小的貢獻,以下是一些加入 Reflex 社群的好方法。 - -- **加入我們的 Discord**: 我們的 [Discord](https://discord.gg/T5WSbC2YtQ) 是獲取 Reflex 專案幫助和討論如何貢獻的最佳地方。 -- **GitHub Discussions**: 這是一個討論您想新增的功能或對於一些困惑/需要澄清事項的好方法。 -- **GitHub Issues**: 在 [Issues](https://github.com/reflex-dev/reflex/issues) 頁面報告錯誤是一個絕佳的方式。此外,您也可以嘗試解決現有 Issue 並提交 PR。 - -我們積極尋找貢獻者,不論您的技能水平或經驗如何。要貢獻,請查看 [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## 感謝所有貢獻者: - - - - - -## 授權 - -Reflex 是一個開源專案且使用 [Apache License 2.0](/LICENSE) 授權。 diff --git a/packages/hatch-reflex-pyi/README.md b/packages/hatch-reflex-pyi/README.md new file mode 100644 index 00000000000..1083758b200 --- /dev/null +++ b/packages/hatch-reflex-pyi/README.md @@ -0,0 +1,3 @@ +# hatch-reflex-pyi + +Hatch build hook that generates .pyi stubs for Reflex component packages. diff --git a/packages/hatch-reflex-pyi/pyproject.toml b/packages/hatch-reflex-pyi/pyproject.toml new file mode 100644 index 00000000000..a2754ad8a0c --- /dev/null +++ b/packages/hatch-reflex-pyi/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "hatch-reflex-pyi" +dynamic = ["version"] +description = "Hatch build hook that generates .pyi stubs for Reflex component packages." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [ + "hatchling", +] + +[project.entry-points.hatch] +reflex-pyi = "hatch_reflex_pyi.hooks" + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "hatch-reflex-pyi-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/packages/hatch-reflex-pyi/src/hatch_reflex_pyi/__init__.py b/packages/hatch-reflex-pyi/src/hatch_reflex_pyi/__init__.py new file mode 100644 index 00000000000..e32a5b34796 --- /dev/null +++ b/packages/hatch-reflex-pyi/src/hatch_reflex_pyi/__init__.py @@ -0,0 +1 @@ +"""Hatch build hook that generates .pyi stubs for Reflex component packages.""" diff --git a/packages/hatch-reflex-pyi/src/hatch_reflex_pyi/hooks.py b/packages/hatch-reflex-pyi/src/hatch_reflex_pyi/hooks.py new file mode 100644 index 00000000000..d244ef08210 --- /dev/null +++ b/packages/hatch-reflex-pyi/src/hatch_reflex_pyi/hooks.py @@ -0,0 +1,15 @@ +"""Hatch plugin registration for reflex-pyi build hook.""" + +from hatchling.plugin import hookimpl + +from hatch_reflex_pyi.plugin import ReflexPyiBuildHook + + +@hookimpl +def hatch_register_build_hook(): + """Register the reflex-pyi build hook. + + Returns: + ReflexPyiBuildHook: The build hook class to be registered." + """ + return ReflexPyiBuildHook diff --git a/packages/hatch-reflex-pyi/src/hatch_reflex_pyi/plugin.py b/packages/hatch-reflex-pyi/src/hatch_reflex_pyi/plugin.py new file mode 100644 index 00000000000..c8b03ec4016 --- /dev/null +++ b/packages/hatch-reflex-pyi/src/hatch_reflex_pyi/plugin.py @@ -0,0 +1,75 @@ +"""Hatch build hook that generates .pyi stub files for Reflex component packages.""" + +from __future__ import annotations + +import pathlib +import subprocess +import sys +from typing import Any + +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + + +class ReflexPyiBuildHook(BuildHookInterface): + """Build hook that generates .pyi stubs for component packages.""" + + PLUGIN_NAME = "reflex-pyi" + + def _src_dir(self) -> pathlib.Path | None: + """Find the source directory under src/. + + Returns: + The source directory path, or None if not found. + """ + src = pathlib.Path(self.root) / "src" + if not src.is_dir(): + return None + children = [ + d for d in src.iterdir() if d.is_dir() and not d.name.startswith(".") + ] + return children[0] if len(children) == 1 else None + + def _marker(self) -> pathlib.Path: + """Get the marker file path. + + Returns: + The marker file path. + """ + return ( + pathlib.Path(self.directory) + / f".{self.metadata.name}-{self.metadata.version}.pyi_generated" + ) + + def initialize(self, version: str, build_data: dict[str, Any]) -> None: + """Generate .pyi stubs before the build. + + Args: + version: The version being built. + build_data: Additional build data. + """ + if self._marker().exists(): + return + + src_dir = self._src_dir() + if src_dir is None: + return + + try: + from reflex_core.utils.pyi_generator import PyiGenerator # noqa: F401 + except ImportError: + # reflex-core is not installed — skip pyi generation. + # Pre-generated .pyi files in the sdist will be used. + return + + for file in src_dir.rglob("*.pyi"): + file.unlink(missing_ok=True) + + # Run from src/ so _path_to_module_name produces valid import names + # (e.g. "reflex_components_core.core.banner" instead of + # "packages.reflex-components-core.src.reflex_components_core.core.banner"). + subprocess.run( + [sys.executable, "-m", "reflex_core.utils.pyi_generator", src_dir.name], + cwd=src_dir.parent, + check=True, + ) + self._marker().touch() diff --git a/packages/reflex-components-code/README.md b/packages/reflex-components-code/README.md new file mode 100644 index 00000000000..78a2013b19c --- /dev/null +++ b/packages/reflex-components-code/README.md @@ -0,0 +1,3 @@ +# reflex-components-code + +Reflex code display components. diff --git a/packages/reflex-components-code/pyproject.toml b/packages/reflex-components-code/pyproject.toml new file mode 100644 index 00000000000..454ac263659 --- /dev/null +++ b/packages/reflex-components-code/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "reflex-components-code" +dynamic = ["version"] +description = "Reflex code display components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [ + "reflex-components-core", + "reflex-components-lucide", + "reflex-components-radix", +] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-code-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = [ + "ruff", + "reflex-core", + "reflex-components-core", + "reflex-components-lucide", + "reflex-components-radix", + "reflex-components-sonner", +] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/packages/reflex-components-code/src/reflex_components_code/__init__.py b/packages/reflex-components-code/src/reflex_components_code/__init__.py new file mode 100644 index 00000000000..e3e7851bee9 --- /dev/null +++ b/packages/reflex-components-code/src/reflex_components_code/__init__.py @@ -0,0 +1 @@ +"""Reflex code display components.""" diff --git a/reflex/components/datadisplay/code.py b/packages/reflex-components-code/src/reflex_components_code/code.py similarity index 94% rename from reflex/components/datadisplay/code.py rename to packages/reflex-components-code/src/reflex_components_code/code.py index c80172cbdb9..86f0ace171e 100644 --- a/reflex/components/datadisplay/code.py +++ b/packages/reflex-components-code/src/reflex_components_code/code.py @@ -5,18 +5,17 @@ import dataclasses from typing import ClassVar, Literal -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.cond import color_mode_cond -from reflex.components.lucide.icon import Icon -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.components.button import Button -from reflex.components.radix.themes.layout.box import Box -from reflex.constants.colors import Color -from reflex.event import set_clipboard -from reflex.style import Style -from reflex.utils import format -from reflex.utils.imports import ImportVar -from reflex.vars.base import LiteralVar, Var, VarData +from reflex_components_core.core.cond import color_mode_cond +from reflex_components_core.core.markdown_component_map import MarkdownComponentMap +from reflex_components_radix.themes.components.button import Button +from reflex_components_radix.themes.layout.box import Box +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.constants.colors import Color +from reflex_core.event import set_clipboard +from reflex_core.style import Style +from reflex_core.utils import format +from reflex_core.utils.imports import ImportVar +from reflex_core.vars.base import LiteralVar, Var, VarData LiteralCodeLanguage = Literal[ "abap", @@ -443,6 +442,8 @@ def create( Returns: The text component. """ + from reflex_components_lucide.icon import Icon + # This component handles style in a special prop. custom_style = props.pop("custom_style", {}) can_copy = props.pop("can_copy", False) diff --git a/reflex/components/datadisplay/shiki_code_block.py b/packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py similarity index 96% rename from reflex/components/datadisplay/shiki_code_block.py rename to packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py index 30984edcfd0..a299ea66b7e 100644 --- a/reflex/components/datadisplay/shiki_code_block.py +++ b/packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py @@ -8,21 +8,20 @@ from dataclasses import dataclass from typing import Any, Literal -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.colors import color -from reflex.components.core.cond import color_mode_cond -from reflex.components.el.elements.forms import Button -from reflex.components.lucide.icon import Icon -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.props import NoExtrasAllowedProps -from reflex.components.radix.themes.layout.box import Box -from reflex.event import run_script, set_clipboard -from reflex.style import Style -from reflex.utils.exceptions import VarTypeError -from reflex.utils.imports import ImportVar -from reflex.vars.base import LiteralVar, Var -from reflex.vars.function import FunctionStringVar -from reflex.vars.sequence import StringVar, string_replace_operation +from reflex_components_core.core.colors import color +from reflex_components_core.core.cond import color_mode_cond +from reflex_components_core.core.markdown_component_map import MarkdownComponentMap +from reflex_components_core.el.elements.forms import Button +from reflex_components_radix.themes.layout.box import Box +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.components.props import NoExtrasAllowedProps +from reflex_core.event import run_script, set_clipboard +from reflex_core.style import Style +from reflex_core.utils.exceptions import VarTypeError +from reflex_core.utils.imports import ImportVar +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.function import FunctionStringVar +from reflex_core.vars.sequence import StringVar, string_replace_operation def copy_script() -> Any: @@ -751,6 +750,8 @@ def create( Returns: The code block component. """ + from reflex_components_lucide.icon import Icon + use_transformers = props.pop("use_transformers", False) show_line_numbers = props.pop("show_line_numbers", False) language = props.pop("language", None) diff --git a/packages/reflex-components-core/README.md b/packages/reflex-components-core/README.md new file mode 100644 index 00000000000..bedf7d6ed79 --- /dev/null +++ b/packages/reflex-components-core/README.md @@ -0,0 +1,3 @@ +# reflex-components-core + +UI components for Reflex. diff --git a/packages/reflex-components-core/pyproject.toml b/packages/reflex-components-core/pyproject.toml new file mode 100644 index 00000000000..f61e5d74612 --- /dev/null +++ b/packages/reflex-components-core/pyproject.toml @@ -0,0 +1,42 @@ +[project] +name = "reflex-components-core" +dynamic = ["version"] +description = "UI components for Reflex." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [ + "reflex-components-lucide", + "reflex-components-sonner", + "python_multipart", + "starlette", + "typing_extensions", +] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-core-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = [ + "ruff", + "reflex-core", + "reflex-components-lucide", + "reflex-components-sonner", + "python_multipart", + "starlette", + "typing_extensions", +] + + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/packages/reflex-components-core/src/reflex_components_core/__init__.py b/packages/reflex-components-core/src/reflex_components_core/__init__.py new file mode 100644 index 00000000000..b202eddb5ba --- /dev/null +++ b/packages/reflex-components-core/src/reflex_components_core/__init__.py @@ -0,0 +1,20 @@ +"""Reflex base UI components package.""" + +from __future__ import annotations + +from reflex_core.utils import lazy_loader + +_SUBMODULES: set[str] = { + "base", + "core", + "datadisplay", + "el", +} + +_SUBMOD_ATTRS: dict[str, list[str]] = {} + +__getattr__, __dir__, __all__ = lazy_loader.attach( + __name__, + submodules=_SUBMODULES, + submod_attrs=_SUBMOD_ATTRS, +) diff --git a/reflex/components/base/__init__.py b/packages/reflex-components-core/src/reflex_components_core/base/__init__.py similarity index 93% rename from reflex/components/base/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/base/__init__.py index 6e4cfd66b3c..38e538497e1 100644 --- a/reflex/components/base/__init__.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader _SUBMODULES: set[str] = {"app_wrap", "bare"} diff --git a/reflex/components/base/app_wrap.py b/packages/reflex-components-core/src/reflex_components_core/base/app_wrap.py similarity index 71% rename from reflex/components/base/app_wrap.py rename to packages/reflex-components-core/src/reflex_components_core/base/app_wrap.py index c7cfe650544..17a6a86d7f3 100644 --- a/reflex/components/base/app_wrap.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/app_wrap.py @@ -1,8 +1,9 @@ """Top-level component that wraps the entire app.""" -from reflex.components.base.fragment import Fragment -from reflex.components.component import Component -from reflex.vars.base import Var +from reflex_core.components.component import Component +from reflex_core.vars.base import Var + +from reflex_components_core.base.fragment import Fragment class AppWrap(Fragment): diff --git a/reflex/components/base/bare.py b/packages/reflex-components-core/src/reflex_components_core/base/bare.py similarity index 93% rename from reflex/components/base/bare.py rename to packages/reflex-components-core/src/reflex_components_core/base/bare.py index 4182d0666ba..50da3a6b775 100644 --- a/reflex/components/base/bare.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/bare.py @@ -5,16 +5,16 @@ from collections.abc import Iterator, Sequence from typing import Any -from reflex.components.component import BaseComponent, Component, ComponentStyle -from reflex.components.tags import Tag -from reflex.components.tags.tagless import Tagless -from reflex.environment import PerformanceMode, environment -from reflex.utils import console -from reflex.utils.decorator import once -from reflex.utils.imports import ParsedImportDict -from reflex.vars import BooleanVar, ObjectVar, Var -from reflex.vars.base import GLOBAL_CACHE, VarData -from reflex.vars.sequence import LiteralStringVar +from reflex_core.components.component import BaseComponent, Component, ComponentStyle +from reflex_core.components.tags import Tag +from reflex_core.components.tags.tagless import Tagless +from reflex_core.environment import PerformanceMode, environment +from reflex_core.utils import console +from reflex_core.utils.decorator import once +from reflex_core.utils.imports import ParsedImportDict +from reflex_core.vars import BooleanVar, ObjectVar, Var +from reflex_core.vars.base import GLOBAL_CACHE, VarData +from reflex_core.vars.sequence import LiteralStringVar @once diff --git a/reflex/components/base/body.py b/packages/reflex-components-core/src/reflex_components_core/base/body.py similarity index 64% rename from reflex/components/base/body.py rename to packages/reflex-components-core/src/reflex_components_core/base/body.py index 2327aa2d366..51fdc70d7e1 100644 --- a/reflex/components/base/body.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/body.py @@ -1,6 +1,6 @@ """Display the page body.""" -from reflex.components.el import elements +from reflex_components_core.el import elements class Body(elements.Body): diff --git a/reflex/components/base/document.py b/packages/reflex-components-core/src/reflex_components_core/base/document.py similarity index 91% rename from reflex/components/base/document.py rename to packages/reflex-components-core/src/reflex_components_core/base/document.py index 93979c5451b..9157a805a76 100644 --- a/reflex/components/base/document.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/document.py @@ -1,6 +1,6 @@ """Document components.""" -from reflex.components.component import Component +from reflex_core.components.component import Component class ReactRouterLib(Component): diff --git a/reflex/components/base/error_boundary.py b/packages/reflex-components-core/src/reflex_components_core/base/error_boundary.py similarity index 93% rename from reflex/components/base/error_boundary.py rename to packages/reflex-components-core/src/reflex_components_core/base/error_boundary.py index 5b91442cb8c..35432a91810 100644 --- a/reflex/components/base/error_boundary.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/error_boundary.py @@ -2,14 +2,14 @@ from __future__ import annotations -from reflex.components.component import Component, field -from reflex.components.datadisplay.logo import svg_logo -from reflex.components.el import a, button, div, h2, hr, p, pre, svg -from reflex.event import EventHandler, set_clipboard -from reflex.state import FrontendEventExceptionState -from reflex.vars.base import Var -from reflex.vars.function import ArgsFunctionOperation -from reflex.vars.object import ObjectVar +from reflex_core.components.component import Component, field +from reflex_core.event import EventHandler, set_clipboard +from reflex_core.vars.base import Var +from reflex_core.vars.function import ArgsFunctionOperation +from reflex_core.vars.object import ObjectVar + +from reflex_components_core.datadisplay.logo import svg_logo +from reflex_components_core.el import a, button, div, h2, hr, p, pre, svg def on_error_spec( @@ -58,6 +58,8 @@ def create(cls, *children, **props): Returns: The ErrorBoundary component. """ + from reflex.state import FrontendEventExceptionState + if "on_error" not in props: props["on_error"] = FrontendEventExceptionState.handle_frontend_exception if "fallback_render" not in props: diff --git a/reflex/components/base/fragment.py b/packages/reflex-components-core/src/reflex_components_core/base/fragment.py similarity index 84% rename from reflex/components/base/fragment.py rename to packages/reflex-components-core/src/reflex_components_core/base/fragment.py index 84ec86145e0..0d6697a776f 100644 --- a/reflex/components/base/fragment.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/fragment.py @@ -1,6 +1,6 @@ """React fragments to enable bare returns of component trees from functions.""" -from reflex.components.component import Component +from reflex_core.components.component import Component class Fragment(Component): diff --git a/reflex/components/base/link.py b/packages/reflex-components-core/src/reflex_components_core/base/link.py similarity index 86% rename from reflex/components/base/link.py rename to packages/reflex-components-core/src/reflex_components_core/base/link.py index 7af286e49a1..bdbc454ad9c 100644 --- a/reflex/components/base/link.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/link.py @@ -1,8 +1,9 @@ """Display the title of the current page.""" -from reflex.components.component import field -from reflex.components.el.elements.base import BaseHTML -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_core.el.elements.base import BaseHTML class RawLink(BaseHTML): diff --git a/reflex/components/base/meta.py b/packages/reflex-components-core/src/reflex_components_core/base/meta.py similarity index 78% rename from reflex/components/base/meta.py rename to packages/reflex-components-core/src/reflex_components_core/base/meta.py index a8f81da18ea..42ce2990089 100644 --- a/reflex/components/base/meta.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/meta.py @@ -2,11 +2,14 @@ from __future__ import annotations -from reflex.components.base.bare import Bare -from reflex.components.component import field -from reflex.components.el import elements -from reflex.components.el.elements.metadata import Meta as Meta # for compatibility -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_core.base.bare import Bare +from reflex_components_core.el import elements +from reflex_components_core.el.elements.metadata import ( + Meta as Meta, +) # for compatibility class Title(elements.Title): diff --git a/reflex/components/base/script.py b/packages/reflex-components-core/src/reflex_components_core/base/script.py similarity index 93% rename from reflex/components/base/script.py rename to packages/reflex-components-core/src/reflex_components_core/base/script.py index 507e6db324e..1282b982d7b 100644 --- a/reflex/components/base/script.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/script.py @@ -2,9 +2,10 @@ from __future__ import annotations -from reflex.components import el as elements -from reflex.components.core.helmet import helmet -from reflex.utils import console +from reflex_core.utils import console + +from reflex_components_core import el as elements +from reflex_components_core.core.helmet import helmet class Script(elements.Script): diff --git a/reflex/components/base/strict_mode.py b/packages/reflex-components-core/src/reflex_components_core/base/strict_mode.py similarity index 78% rename from reflex/components/base/strict_mode.py rename to packages/reflex-components-core/src/reflex_components_core/base/strict_mode.py index 46b01ad872c..2ce993cdc53 100644 --- a/reflex/components/base/strict_mode.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/strict_mode.py @@ -1,6 +1,6 @@ """Module for the StrictMode component.""" -from reflex.components.component import Component +from reflex_core.components.component import Component class StrictMode(Component): diff --git a/reflex/components/core/__init__.py b/packages/reflex-components-core/src/reflex_components_core/core/__init__.py similarity index 97% rename from reflex/components/core/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/core/__init__.py index c9babf294a1..e31071804c4 100644 --- a/reflex/components/core/__init__.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader _SUBMODULES: set[str] = {"layout"} diff --git a/packages/reflex-components-core/src/reflex_components_core/core/_upload.py b/packages/reflex-components-core/src/reflex_components_core/core/_upload.py new file mode 100644 index 00000000000..b096a03cd8a --- /dev/null +++ b/packages/reflex-components-core/src/reflex_components_core/core/_upload.py @@ -0,0 +1,719 @@ +"""Backend upload helpers and routes for Reflex apps.""" + +from __future__ import annotations + +import asyncio +import contextlib +import dataclasses +from collections import deque +from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable +from pathlib import Path +from typing import TYPE_CHECKING, Any, BinaryIO, cast + +from python_multipart.multipart import MultipartParser, parse_options_header +from reflex_core import constants +from reflex_core.utils import exceptions +from starlette.datastructures import Headers +from starlette.datastructures import UploadFile as StarletteUploadFile +from starlette.exceptions import HTTPException +from starlette.formparsers import MultiPartException, _user_safe_decode +from starlette.requests import ClientDisconnect, Request +from starlette.responses import JSONResponse, Response, StreamingResponse +from typing_extensions import Self + +if TYPE_CHECKING: + from reflex_core.event import EventHandler + from reflex_core.utils.types import Receive, Scope, Send + + from reflex.app import App + from reflex.state import BaseState + + +@dataclasses.dataclass(frozen=True) +class UploadFile(StarletteUploadFile): + """A file uploaded to the server. + + Args: + file: The standard Python file object (non-async). + filename: The original file name. + size: The size of the file in bytes. + headers: The headers of the request. + """ + + file: BinaryIO + + path: Path | None = dataclasses.field(default=None) + + size: int | None = dataclasses.field(default=None) + + headers: Headers = dataclasses.field(default_factory=Headers) + + @property + def filename(self) -> str | None: + """Get the name of the uploaded file. + + Returns: + The name of the uploaded file. + """ + return self.name + + @property + def name(self) -> str | None: + """Get the name of the uploaded file. + + Returns: + The name of the uploaded file. + """ + if self.path: + return self.path.name + return None + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class UploadChunk: + """A chunk of uploaded file data.""" + + filename: str + offset: int + content_type: str + data: bytes + + +class UploadChunkIterator(AsyncIterator[UploadChunk]): + """An async iterator over uploaded file chunks.""" + + __slots__ = ( + "_chunks", + "_closed", + "_condition", + "_consumer_task", + "_error", + "_maxsize", + ) + + def __init__(self, *, maxsize: int = 8): + """Initialize the iterator. + + Args: + maxsize: Maximum number of chunks to buffer before blocking producers. + """ + self._maxsize = maxsize + self._chunks: deque[UploadChunk] = deque() + self._condition = asyncio.Condition() + self._closed = False + self._error: Exception | None = None + self._consumer_task: asyncio.Task[Any] | None = None + + def __aiter__(self) -> Self: + """Return the iterator itself. + + Returns: + The upload chunk iterator. + """ + return self + + async def __anext__(self) -> UploadChunk: + """Yield the next available upload chunk. + + Returns: + The next upload chunk. + + Raises: + _error: Any error forwarded from the upload producer. + StopAsyncIteration: When all chunks have been consumed. + """ + async with self._condition: + while not self._chunks and not self._closed: + await self._condition.wait() + + if self._chunks: + chunk = self._chunks.popleft() + self._condition.notify_all() + return chunk + + if self._error is not None: + raise self._error + raise StopAsyncIteration + + def set_consumer_task(self, task: asyncio.Task[Any]) -> None: + """Track the task consuming this iterator. + + Args: + task: The background task consuming upload chunks. + """ + self._consumer_task = task + task.add_done_callback(self._wake_waiters) + + async def push(self, chunk: UploadChunk) -> None: + """Push a new chunk into the iterator. + + Args: + chunk: The chunk to push. + + Raises: + RuntimeError: If the iterator is already closed or the consumer exited early. + """ + async with self._condition: + while len(self._chunks) >= self._maxsize and not self._closed: + self._raise_if_consumer_finished() + await self._condition.wait() + + if self._closed: + msg = "Upload chunk iterator is closed." + raise RuntimeError(msg) + + self._raise_if_consumer_finished() + self._chunks.append(chunk) + self._condition.notify_all() + + async def finish(self) -> None: + """Mark the iterator as complete.""" + async with self._condition: + if self._closed: + return + self._closed = True + self._condition.notify_all() + + async def fail(self, error: Exception) -> None: + """Mark the iterator as failed. + + Args: + error: The error to raise from the iterator. + """ + async with self._condition: + if self._closed: + return + self._closed = True + self._error = error + self._condition.notify_all() + + def _raise_if_consumer_finished(self) -> None: + """Raise if the consumer task exited before draining the iterator. + + Raises: + RuntimeError: If the consumer task completed before draining the iterator. + """ + if self._consumer_task is None or not self._consumer_task.done(): + return + + try: + task_exc = self._consumer_task.exception() + except asyncio.CancelledError as err: + task_exc = err + + msg = "Upload handler returned before consuming all upload chunks." + if task_exc is not None: + raise RuntimeError(msg) from task_exc + raise RuntimeError(msg) + + def _wake_waiters(self, task: asyncio.Task[Any]) -> None: + """Wake any producers or consumers blocked on the iterator condition. + + Args: + task: The completed consumer task. + """ + task.get_loop().create_task(self._notify_waiters()) + + async def _notify_waiters(self) -> None: + """Notify tasks waiting on the iterator condition.""" + async with self._condition: + self._condition.notify_all() + + +@dataclasses.dataclass(kw_only=True, slots=True) +class _UploadChunkPart: + """Track the current multipart file part for upload streaming.""" + + content_disposition: bytes | None = None + field_name: str = "" + filename: str | None = None + content_type: str = "" + item_headers: list[tuple[bytes, bytes]] = dataclasses.field(default_factory=list) + offset: int = 0 + bytes_emitted: int = 0 + is_upload_chunk: bool = False + + +@dataclasses.dataclass(kw_only=True, slots=True) +class _UploadChunkMultipartParser: + """Streaming multipart parser for streamed upload files.""" + + headers: Headers + stream: AsyncGenerator[bytes, None] + chunk_iter: UploadChunkIterator + _charset: str = "" + _current_partial_header_name: bytes = b"" + _current_partial_header_value: bytes = b"" + _current_part: _UploadChunkPart = dataclasses.field( + default_factory=_UploadChunkPart + ) + _chunks_to_emit: deque[UploadChunk] = dataclasses.field(default_factory=deque) + _seen_upload_chunk: bool = False + _part_count: int = 0 + _emitted_chunk_count: int = 0 + _emitted_bytes: int = 0 + _stream_chunk_count: int = 0 + + def on_part_begin(self) -> None: + """Reset parser state for a new multipart part.""" + self._current_part = _UploadChunkPart() + + def on_part_data(self, data: bytes, start: int, end: int) -> None: + """Record streamed chunk data for the current part.""" + if ( + not self._current_part.is_upload_chunk + or self._current_part.filename is None + ): + return + + message_bytes = data[start:end] + self._chunks_to_emit.append( + UploadChunk( + filename=self._current_part.filename, + offset=self._current_part.offset + self._current_part.bytes_emitted, + content_type=self._current_part.content_type, + data=message_bytes, + ) + ) + self._current_part.bytes_emitted += len(message_bytes) + self._emitted_chunk_count += 1 + self._emitted_bytes += len(message_bytes) + + def on_part_end(self) -> None: + """Emit a zero-byte chunk for empty file parts.""" + if ( + self._current_part.is_upload_chunk + and self._current_part.filename is not None + and self._current_part.bytes_emitted == 0 + ): + self._chunks_to_emit.append( + UploadChunk( + filename=self._current_part.filename, + offset=self._current_part.offset, + content_type=self._current_part.content_type, + data=b"", + ) + ) + self._emitted_chunk_count += 1 + + def on_header_field(self, data: bytes, start: int, end: int) -> None: + """Accumulate multipart header field bytes.""" + self._current_partial_header_name += data[start:end] + + def on_header_value(self, data: bytes, start: int, end: int) -> None: + """Accumulate multipart header value bytes.""" + self._current_partial_header_value += data[start:end] + + def on_header_end(self) -> None: + """Store the completed multipart header.""" + field = self._current_partial_header_name.lower() + if field == b"content-disposition": + self._current_part.content_disposition = self._current_partial_header_value + self._current_part.item_headers.append(( + field, + self._current_partial_header_value, + )) + self._current_partial_header_name = b"" + self._current_partial_header_value = b"" + + def on_headers_finished(self) -> None: + """Parse upload metadata from multipart headers.""" + disposition, options = parse_options_header( + self._current_part.content_disposition + ) + if disposition != b"form-data": + msg = "Invalid upload chunk disposition." + raise MultiPartException(msg) + + try: + field_name = _user_safe_decode(options[b"name"], self._charset) + except KeyError as err: + msg = 'The Content-Disposition header field "name" must be provided.' + raise MultiPartException(msg) from err + + try: + filename = _user_safe_decode(options[b"filename"], self._charset) + except KeyError: + # Ignore non-file form fields entirely. + return + filename = Path(filename.lstrip("/")).name + + content_type = "" + for header_name, header_value in self._current_part.item_headers: + if header_name == b"content-type": + content_type = _user_safe_decode(header_value, self._charset) + break + + self._current_part.field_name = field_name + self._current_part.filename = filename + self._current_part.content_type = content_type + self._current_part.offset = 0 + self._current_part.bytes_emitted = 0 + self._current_part.is_upload_chunk = True + self._seen_upload_chunk = True + self._part_count += 1 + + def on_end(self) -> None: + """Finalize parser callbacks.""" + + async def _flush_emitted_chunks(self) -> None: + """Push parsed upload chunks into the handler iterator.""" + while self._chunks_to_emit: + await self.chunk_iter.push(self._chunks_to_emit.popleft()) + + async def parse(self) -> None: + """Parse the incoming request stream and push chunks to the iterator. + + Raises: + MultiPartException: If the request is not valid multipart upload data. + RuntimeError: If the upload handler exits before consuming all chunks. + """ + _, params = parse_options_header(self.headers["Content-Type"]) + charset = params.get(b"charset", "utf-8") + if isinstance(charset, bytes): + charset = charset.decode("latin-1") + self._charset = charset + + try: + boundary = params[b"boundary"] + except KeyError as err: + msg = "Missing boundary in multipart." + raise MultiPartException(msg) from err + + callbacks = { + "on_part_begin": self.on_part_begin, + "on_part_data": self.on_part_data, + "on_part_end": self.on_part_end, + "on_header_field": self.on_header_field, + "on_header_value": self.on_header_value, + "on_header_end": self.on_header_end, + "on_headers_finished": self.on_headers_finished, + "on_end": self.on_end, + } + parser = MultipartParser(boundary, cast(Any, callbacks)) + + async for chunk in self.stream: + self._stream_chunk_count += 1 + parser.write(chunk) + await self._flush_emitted_chunks() + + parser.finalize() + await self._flush_emitted_chunks() + + +class _UploadStreamingResponse(StreamingResponse): + """Streaming response that always releases upload form resources.""" + + _on_finish: Callable[[], Awaitable[None]] + + def __init__( + self, + *args: Any, + on_finish: Callable[[], Awaitable[None]], + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self._on_finish = on_finish + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + try: + await super().__call__(scope, receive, send) + finally: + await self._on_finish() + + +def _require_upload_headers(request: Request) -> tuple[str, str]: + """Extract the required upload headers from a request. + + Args: + request: The incoming request. + + Returns: + The client token and event handler name. + + Raises: + HTTPException: If the upload headers are missing. + """ + token = request.headers.get("reflex-client-token") + handler = request.headers.get("reflex-event-handler") + + if not token or not handler: + raise HTTPException( + status_code=400, + detail="Missing reflex-client-token or reflex-event-handler header.", + ) + + return token, handler + + +async def _get_upload_runtime_handler( + app: App, + token: str, + handler_name: str, +) -> tuple[BaseState, EventHandler]: + """Resolve the runtime state and event handler for an upload request. + + Args: + app: The Reflex app. + token: The client token. + handler_name: The fully qualified event handler name. + + Returns: + The root state instance and resolved event handler. + """ + from reflex.state import _substate_key + + substate_token = _substate_key(token, handler_name.rpartition(".")[0]) + state = await app.state_manager.get_state(substate_token) + _current_state, event_handler = state._get_event_handler(handler_name) + return state, event_handler + + +def _seed_upload_router_data(state: BaseState, token: str) -> None: + """Ensure upload-launched handlers have the client token in router state. + + Background upload handlers use ``StateProxy`` which derives its mutable-state + token from ``self.router.session.client_token``. Upload requests do not flow + through the normal websocket event pipeline, so we seed the token here. + + Args: + state: The root state instance. + token: The client token from the upload request. + """ + from reflex.state import RouterData + + router_data = dict(state.router_data) + if router_data.get(constants.RouteVar.CLIENT_TOKEN) == token: + return + + router_data[constants.RouteVar.CLIENT_TOKEN] = token + state.router_data = router_data + state.router = RouterData.from_router_data(router_data) + + +async def _upload_buffered_file( + request: Request, + app: App, + *, + token: str, + handler_name: str, + handler_upload_param: tuple[str, Any], +) -> Response: + """Handle buffered uploads on the standard upload endpoint. + + Returns: + A streaming response for the buffered upload. + """ + from reflex_core.event import Event + from reflex_core.utils.exceptions import UploadValueError + + try: + form_data = await request.form() + except ClientDisconnect: + return Response() + + form_data_closed = False + + async def _close_form_data() -> None: + """Close the parsed form data exactly once.""" + nonlocal form_data_closed + if form_data_closed: + return + form_data_closed = True + await form_data.close() + + def _create_upload_event() -> Event: + """Create an upload event using the live Starlette temp files. + + Returns: + The upload event backed by the parsed files. + """ + files = form_data.getlist("files") + file_uploads = [] + for file in files: + if not isinstance(file, StarletteUploadFile): + raise UploadValueError( + "Uploaded file is not an UploadFile." + str(file) + ) + file_uploads.append( + UploadFile( + file=file.file, + path=Path(file.filename.lstrip("/")) if file.filename else None, + size=file.size, + headers=file.headers, + ) + ) + + return Event( + token=token, + name=handler_name, + payload={handler_upload_param[0]: file_uploads}, + ) + + event: Event | None = None + try: + event = _create_upload_event() + finally: + if event is None: + await _close_form_data() + + if event is None: + msg = "Upload event was not created." + raise RuntimeError(msg) + + async def _ndjson_updates(): + """Process the upload event, generating ndjson updates. + + Yields: + Each state update as newline-delimited JSON. + """ + async with app.state_manager.modify_state_with_links( + event.substate_token, event=event + ) as state: + async for update in state._process(event): + update = await app._postprocess(state, event, update) + yield update.json() + "\n" + + return _UploadStreamingResponse( + _ndjson_updates(), + media_type="application/x-ndjson", + on_finish=_close_form_data, + ) + + +def _background_upload_accepted_response() -> StreamingResponse: + """Return a minimal ndjson response for background upload dispatch.""" + from reflex.state import StateUpdate + + def _accepted_updates(): + yield StateUpdate(final=True).json() + "\n" + + return StreamingResponse( + _accepted_updates(), + media_type="application/x-ndjson", + status_code=202, + ) + + +async def _upload_chunk_file( + request: Request, + app: App, + *, + token: str, + handler_name: str, + handler_upload_param: tuple[str, Any], + acknowledge_on_upload_endpoint: bool, +) -> Response: + """Handle a streaming upload request. + + Returns: + The streaming upload response. + """ + from reflex_core.event import Event + + chunk_iter = UploadChunkIterator(maxsize=8) + event = Event( + token=token, + name=handler_name, + payload={handler_upload_param[0]: chunk_iter}, + ) + + async with app.state_manager.modify_state_with_links( + event.substate_token, + event=event, + ) as state: + _seed_upload_router_data(state, token) + task = app._process_background(state, event) + + if task is None: + msg = f"@rx.event(background=True) is required for upload_files_chunk handler `{handler_name}`." + return JSONResponse({"detail": msg}, status_code=400) + + chunk_iter.set_consumer_task(task) + + parser = _UploadChunkMultipartParser( + headers=request.headers, + stream=request.stream(), + chunk_iter=chunk_iter, + ) + + try: + await parser.parse() + except ClientDisconnect: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + return Response() + except (MultiPartException, RuntimeError, ValueError) as err: + await chunk_iter.fail(err) + return JSONResponse({"detail": str(err)}, status_code=400) + + try: + await chunk_iter.finish() + except RuntimeError as err: + return JSONResponse({"detail": str(err)}, status_code=400) + + if acknowledge_on_upload_endpoint: + return _background_upload_accepted_response() + return Response(status_code=202) + + +def upload(app: App): + """Upload files, dispatching to buffered or streaming handling. + + Args: + app: The app to upload the file for. + + Returns: + The upload function. + """ + + async def upload_file(request: Request): + """Upload a file. + + Args: + request: The Starlette request object. + + Returns: + The upload response. + + Raises: + UploadValueError: If the handler does not have a supported annotation. + UploadTypeError: If a non-streaming upload is wired to a background task. + HTTPException: when the request does not include token / handler headers. + """ + from reflex_core.event import ( + resolve_upload_chunk_handler_param, + resolve_upload_handler_param, + ) + + token, handler_name = _require_upload_headers(request) + _state, event_handler = await _get_upload_runtime_handler( + app, token, handler_name + ) + + if event_handler.is_background: + try: + handler_upload_param = resolve_upload_chunk_handler_param(event_handler) + except exceptions.UploadValueError: + pass + else: + return await _upload_chunk_file( + request, + app, + token=token, + handler_name=handler_name, + handler_upload_param=handler_upload_param, + acknowledge_on_upload_endpoint=True, + ) + + handler_upload_param = resolve_upload_handler_param(event_handler) + return await _upload_buffered_file( + request, + app, + token=token, + handler_name=handler_name, + handler_upload_param=handler_upload_param, + ) + + return upload_file diff --git a/reflex/components/core/auto_scroll.py b/packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.py similarity index 93% rename from reflex/components/core/auto_scroll.py rename to packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.py index a4eb6f56725..e6bbd9b9f15 100644 --- a/reflex/components/core/auto_scroll.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.py @@ -4,10 +4,11 @@ import dataclasses -from reflex.components.el.elements.typography import Div -from reflex.constants.compiler import MemoizationDisposition, MemoizationMode -from reflex.utils.imports import ImportDict -from reflex.vars.base import Var, get_unique_variable_name +from reflex_core.constants.compiler import MemoizationDisposition, MemoizationMode +from reflex_core.utils.imports import ImportDict +from reflex_core.vars.base import Var, get_unique_variable_name + +from reflex_components_core.el.elements.typography import Div class AutoScroll(Div): diff --git a/reflex/components/core/banner.py b/packages/reflex-components-core/src/reflex_components_core/core/banner.py similarity index 57% rename from reflex/components/core/banner.py rename to packages/reflex-components-core/src/reflex_components_core/core/banner.py index 8160faf38c5..bd5cc3d9dfd 100644 --- a/reflex/components/core/banner.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/banner.py @@ -2,29 +2,24 @@ from __future__ import annotations -from reflex import constants -from reflex.components.base.fragment import Fragment -from reflex.components.component import Component -from reflex.components.core.cond import cond -from reflex.components.el.elements.typography import Div -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.components.dialog import ( - DialogContent, - DialogRoot, - DialogTitle, -) -from reflex.components.radix.themes.layout.flex import Flex -from reflex.components.radix.themes.typography.text import Text -from reflex.components.sonner.toast import ToastProps, toast_ref -from reflex.constants import Dirs, Hooks, Imports -from reflex.constants.compiler import CompileVars -from reflex.environment import environment -from reflex.utils.imports import ImportVar -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var -from reflex.vars.function import FunctionStringVar -from reflex.vars.number import BooleanVar -from reflex.vars.sequence import LiteralArrayVar +from reflex_components_lucide.icon import Icon +from reflex_components_sonner.toast import ToastProps, toast_ref +from reflex_core import constants +from reflex_core.components.component import Component +from reflex_core.constants import Dirs, Hooks, Imports +from reflex_core.constants.compiler import CompileVars +from reflex_core.environment import environment +from reflex_core.utils.imports import ImportVar +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.function import FunctionStringVar +from reflex_core.vars.number import BooleanVar +from reflex_core.vars.sequence import LiteralArrayVar + +from reflex_components_core import el +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core.cond import cond +from reflex_components_core.el.elements.typography import Div connect_error_var_data: VarData = VarData( imports=Imports.EVENTS, @@ -210,13 +205,14 @@ def create(cls, comp: Component | None = None) -> Component: The connection banner component. """ if not comp: - comp = Flex.create( - Text.create( + comp = el.div( + el.span( *default_connection_error(), color="black", - size="4", + font_size="1.125rem", ), - justify="center", + display="flex", + justify_content="center", background_color="crimson", width="100vw", padding="5px", @@ -240,14 +236,12 @@ def create(cls, comp: Component | None = None) -> Component: The connection banner component. """ if not comp: - comp = Text.create(*default_connection_error()) + comp = el.span(*default_connection_error()) return cond( has_too_many_connection_errors, - DialogRoot.create( - DialogContent.create( - DialogTitle.create("Connection Error"), - comp, - ), + el.dialog( + el.h2("Connection Error"), + comp, open=has_too_many_connection_errors, z_index=9999, ), @@ -330,7 +324,7 @@ def create(cls, **props) -> Component: Returns: The backend disabled component. """ - import reflex as rx + from reflex_components_core.core.colors import color is_backend_disabled = Var( "backendDisabled", @@ -348,118 +342,128 @@ def create(cls, **props) -> Component: ), ) + warning_icon = el.svg( + el.path( + d="M6.90816 1.34341C7.61776 1.10786 8.38256 1.10786 9.09216 1.34341C9.7989 1.57799 10.3538 2.13435 10.9112 2.91605C11.4668 3.69515 12.0807 4.78145 12.872 6.18175L12.9031 6.23672C13.6946 7.63721 14.3085 8.72348 14.6911 9.60441C15.0755 10.4896 15.267 11.2539 15.1142 11.9881C14.9604 12.7275 14.5811 13.3997 14.0287 13.9079C13.4776 14.4147 12.7273 14.6286 11.7826 14.7313C10.8432 14.8334 9.6143 14.8334 8.0327 14.8334H7.9677C6.38604 14.8334 5.15719 14.8334 4.21778 14.7313C3.27301 14.6286 2.52269 14.4147 1.97164 13.9079C1.41924 13.3997 1.03995 12.7275 0.88613 11.9881C0.733363 11.2539 0.92483 10.4896 1.30926 9.60441C1.69184 8.72348 2.30573 7.63721 3.09722 6.23671L3.12828 6.18175C3.91964 4.78146 4.53355 3.69515 5.08914 2.91605C5.64658 2.13435 6.20146 1.57799 6.90816 1.34341ZM7.3335 11.3334C7.3335 10.9652 7.63063 10.6667 7.99716 10.6667H8.00316C8.3697 10.6667 8.66683 10.9652 8.66683 11.3334C8.66683 11.7016 8.3697 12.0001 8.00316 12.0001H7.99716C7.63063 12.0001 7.3335 11.7016 7.3335 11.3334ZM7.3335 8.66675C7.3335 9.03495 7.63196 9.33341 8.00016 9.33341C8.36836 9.33341 8.66683 9.03495 8.66683 8.66675V6.00008C8.66683 5.63189 8.36836 5.33341 8.00016 5.33341C7.63196 5.33341 7.3335 5.63189 7.3335 6.00008V8.66675Z", + fill_rule="evenodd", + clip_rule="evenodd", + fill=color("amber", 11), + ), + width="16", + height="16", + viewBox="0 0 16 16", + fill="none", + xmlns="http://www.w3.org/2000/svg", + margin_top="0.125rem", + flex_shrink="0", + ) + + info_message = el.div( + el.span( + "If you are the owner of this app, visit ", + el.a( + "Reflex Cloud", + color=color("amber", 11), + text_decoration="underline", + _hover={ + "color": color("amber", 11), + "text_decoration_color": color("amber", 11), + }, + text_decoration_color=color("amber", 10), + href="https://cloud.reflex.dev/", + font_weight="600", + target="_blank", + ), + " for more information on how to resume your app.", + font_size="0.875rem", + font_weight="500", + line_height="1.25rem", + letter_spacing="-0.01094rem", + color=color("amber", 11), + ), + display="flex", + align_items="start", + gap="0.625rem", + border_radius="0.75rem", + border_width="1px", + border_color=color("amber", 5), + background_color=color("amber", 3), + padding="0.625rem", + ) + # Prepend warning icon into info_message children + info_message.children.insert(0, warning_icon) + + resume_button = el.a( + el.button( + "Resume app", + color="rgba(252, 252, 253, 1)", + font_size="0.875rem", + font_weight="600", + line_height="1.25rem", + letter_spacing="-0.01094rem", + height="2.5rem", + padding="0rem 0.75rem", + width="100%", + border_radius="0.75rem", + background=f"linear-gradient(180deg, {color('violet', 9)} 0%, {color('violet', 10)} 100%)", + _hover={ + "background": f"linear-gradient(180deg, {color('violet', 10)} 0%, {color('violet', 10)} 100%)", + }, + ), + width="100%", + text_decoration="none", + href="https://cloud.reflex.dev/", + target="_blank", + ) + + card = el.div( + el.div( + el.div( + "This app is paused", + font_size="1.5rem", + font_weight="600", + line_height="1.25rem", + letter_spacing="-0.0375rem", + ), + info_message, + resume_button, + display="flex", + flex_direction="column", + gap="1rem", + ), + font_family='"Instrument Sans", "Helvetica", "Arial", sans-serif', + position="fixed", + top="50%", + left="50%", + transform="translate(-50%, -50%)", + width="60ch", + max_width="90vw", + border_radius="0.75rem", + border_width="1px", + border_color=color("slate", 4), + padding="1.5rem", + background_color=color("slate", 1), + box_shadow="0px 2px 5px 0px light-dark(rgba(28, 32, 36, 0.03), rgba(0, 0, 0, 0.00))", + ) + return super().create( - rx.cond( + cond( is_backend_disabled, - rx.box( - rx.el.link( + el.div( + el.link( rel="preconnect", href="https://fonts.googleapis.com", ), - rx.el.link( + el.link( rel="preconnect", href="https://fonts.gstatic.com", crossorigin="", ), - rx.el.link( + el.link( href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,500;0,600&display=swap", rel="stylesheet", ), - rx.box( - rx.vstack( - rx.text( - "This app is paused", - font_size="1.5rem", - font_weight="600", - line_height="1.25rem", - letter_spacing="-0.0375rem", - ), - rx.hstack( - rx.el.svg( - rx.el.path( - d="M6.90816 1.34341C7.61776 1.10786 8.38256 1.10786 9.09216 1.34341C9.7989 1.57799 10.3538 2.13435 10.9112 2.91605C11.4668 3.69515 12.0807 4.78145 12.872 6.18175L12.9031 6.23672C13.6946 7.63721 14.3085 8.72348 14.6911 9.60441C15.0755 10.4896 15.267 11.2539 15.1142 11.9881C14.9604 12.7275 14.5811 13.3997 14.0287 13.9079C13.4776 14.4147 12.7273 14.6286 11.7826 14.7313C10.8432 14.8334 9.6143 14.8334 8.0327 14.8334H7.9677C6.38604 14.8334 5.15719 14.8334 4.21778 14.7313C3.27301 14.6286 2.52269 14.4147 1.97164 13.9079C1.41924 13.3997 1.03995 12.7275 0.88613 11.9881C0.733363 11.2539 0.92483 10.4896 1.30926 9.60441C1.69184 8.72348 2.30573 7.63721 3.09722 6.23671L3.12828 6.18175C3.91964 4.78146 4.53355 3.69515 5.08914 2.91605C5.64658 2.13435 6.20146 1.57799 6.90816 1.34341ZM7.3335 11.3334C7.3335 10.9652 7.63063 10.6667 7.99716 10.6667H8.00316C8.3697 10.6667 8.66683 10.9652 8.66683 11.3334C8.66683 11.7016 8.3697 12.0001 8.00316 12.0001H7.99716C7.63063 12.0001 7.3335 11.7016 7.3335 11.3334ZM7.3335 8.66675C7.3335 9.03495 7.63196 9.33341 8.00016 9.33341C8.36836 9.33341 8.66683 9.03495 8.66683 8.66675V6.00008C8.66683 5.63189 8.36836 5.33341 8.00016 5.33341C7.63196 5.33341 7.3335 5.63189 7.3335 6.00008V8.66675Z", - fill_rule="evenodd", - clip_rule="evenodd", - fill=rx.color("amber", 11), - ), - width="16", - height="16", - viewBox="0 0 16 16", - fill="none", - xmlns="http://www.w3.org/2000/svg", - margin_top="0.125rem", - flex_shrink="0", - ), - rx.text( - "If you are the owner of this app, visit ", - rx.link( - "Reflex Cloud", - color=rx.color("amber", 11), - underline="always", - _hover={ - "color": rx.color("amber", 11), - "text_decoration_color": rx.color( - "amber", 11 - ), - }, - text_decoration_color=rx.color("amber", 10), - href="https://cloud.reflex.dev/", - font_weight="600", - is_external=True, - ), - " for more information on how to resume your app.", - font_size="0.875rem", - font_weight="500", - line_height="1.25rem", - letter_spacing="-0.01094rem", - color=rx.color("amber", 11), - ), - align="start", - gap="0.625rem", - border_radius="0.75rem", - border_width="1px", - border_color=rx.color("amber", 5), - background_color=rx.color("amber", 3), - padding="0.625rem", - ), - rx.link( - rx.el.button( - "Resume app", - color="rgba(252, 252, 253, 1)", - font_size="0.875rem", - font_weight="600", - line_height="1.25rem", - letter_spacing="-0.01094rem", - height="2.5rem", - padding="0rem 0.75rem", - width="100%", - border_radius="0.75rem", - background=f"linear-gradient(180deg, {rx.color('violet', 9)} 0%, {rx.color('violet', 10)} 100%)", - _hover={ - "background": f"linear-gradient(180deg, {rx.color('violet', 10)} 0%, {rx.color('violet', 10)} 100%)", - }, - ), - width="100%", - underline="none", - href="https://cloud.reflex.dev/", - is_external=True, - ), - gap="1rem", - ), - font_family='"Instrument Sans", "Helvetica", "Arial", sans-serif', - position="fixed", - top="50%", - left="50%", - transform="translate(-50%, -50%)", - width="60ch", - max_width="90vw", - border_radius="0.75rem", - border_width="1px", - border_color=rx.color("slate", 4), - padding="1.5rem", - background_color=rx.color("slate", 1), - box_shadow="0px 2px 5px 0px light-dark(rgba(28, 32, 36, 0.03), rgba(0, 0, 0, 0.00))", - ), + card, position="fixed", z_index=9999, backdrop_filter="grayscale(1) blur(5px)", diff --git a/packages/reflex-components-core/src/reflex_components_core/core/breakpoints.py b/packages/reflex-components-core/src/reflex_components_core/core/breakpoints.py new file mode 100644 index 00000000000..f7779aaab09 --- /dev/null +++ b/packages/reflex-components-core/src/reflex_components_core/core/breakpoints.py @@ -0,0 +1,3 @@ +"""Re-export from reflex_core.""" + +from reflex_core.breakpoints import * diff --git a/reflex/components/core/clipboard.py b/packages/reflex-components-core/src/reflex_components_core/core/clipboard.py similarity index 86% rename from reflex/components/core/clipboard.py rename to packages/reflex-components-core/src/reflex_components_core/core/clipboard.py index 0001a459f57..1a64a2ba13b 100644 --- a/reflex/components/core/clipboard.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/clipboard.py @@ -4,15 +4,16 @@ from collections.abc import Sequence -from reflex.components.base.fragment import Fragment -from reflex.components.component import field -from reflex.components.tags.tag import Tag -from reflex.constants.compiler import Hooks -from reflex.event import EventChain, EventHandler, passthrough_event_spec -from reflex.utils.format import format_prop, wrap -from reflex.utils.imports import ImportVar -from reflex.vars import get_unique_variable_name -from reflex.vars.base import Var, VarData +from reflex_core.components.component import field +from reflex_core.components.tags.tag import Tag +from reflex_core.constants.compiler import Hooks +from reflex_core.event import EventChain, EventHandler, passthrough_event_spec +from reflex_core.utils.format import format_prop, wrap +from reflex_core.utils.imports import ImportVar +from reflex_core.vars import get_unique_variable_name +from reflex_core.vars.base import Var, VarData + +from reflex_components_core.base.fragment import Fragment class Clipboard(Fragment): diff --git a/reflex/components/core/colors.py b/packages/reflex-components-core/src/reflex_components_core/core/colors.py similarity index 90% rename from reflex/components/core/colors.py rename to packages/reflex-components-core/src/reflex_components_core/core/colors.py index 5556559eedd..7e2fca69726 100644 --- a/reflex/components/core/colors.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/colors.py @@ -1,7 +1,7 @@ """The colors used in Reflex are a wrapper around https://www.radix-ui.com/colors.""" -from reflex.constants.base import REFLEX_VAR_OPENING_TAG -from reflex.constants.colors import ( +from reflex_core.constants.base import REFLEX_VAR_OPENING_TAG +from reflex_core.constants.colors import ( COLORS, MAX_SHADE_VALUE, MIN_SHADE_VALUE, @@ -9,7 +9,7 @@ ColorType, ShadeType, ) -from reflex.vars.base import Var +from reflex_core.vars.base import Var def color( diff --git a/reflex/components/core/cond.py b/packages/reflex-components-core/src/reflex_components_core/core/cond.py similarity index 90% rename from reflex/components/core/cond.py rename to packages/reflex-components-core/src/reflex_components_core/core/cond.py index 209e414a5af..9d8deb53fde 100644 --- a/reflex/components/core/cond.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/cond.py @@ -4,16 +4,17 @@ from typing import Any, overload -from reflex.components.base.fragment import Fragment -from reflex.components.component import BaseComponent, Component, field -from reflex.components.tags import CondTag, Tag -from reflex.constants import Dirs -from reflex.style import LIGHT_COLOR_MODE, resolved_color_mode -from reflex.utils import types -from reflex.utils.imports import ImportDict, ImportVar -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var -from reflex.vars.number import ternary_operation +from reflex_core.components.component import BaseComponent, Component, field +from reflex_core.components.tags import CondTag, Tag +from reflex_core.constants import Dirs +from reflex_core.style import LIGHT_COLOR_MODE, resolved_color_mode +from reflex_core.utils import types +from reflex_core.utils.imports import ImportDict, ImportVar +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.number import ternary_operation + +from reflex_components_core.base.fragment import Fragment _IS_TRUE_IMPORT: ImportDict = { f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")], diff --git a/reflex/components/core/debounce.py b/packages/reflex-components-core/src/reflex_components_core/core/debounce.py similarity index 95% rename from reflex/components/core/debounce.py rename to packages/reflex-components-core/src/reflex_components_core/core/debounce.py index 4b8d6a1aa88..fc814e64c32 100644 --- a/reflex/components/core/debounce.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/debounce.py @@ -4,11 +4,11 @@ from typing import Any -from reflex.components.component import Component, field -from reflex.constants import EventTriggers -from reflex.event import EventHandler, no_args_event_spec -from reflex.vars import VarData -from reflex.vars.base import Var +from reflex_core.components.component import Component, field +from reflex_core.constants import EventTriggers +from reflex_core.event import EventHandler, no_args_event_spec +from reflex_core.vars import VarData +from reflex_core.vars.base import Var DEFAULT_DEBOUNCE_TIMEOUT = 300 diff --git a/reflex/components/core/foreach.py b/packages/reflex-components-core/src/reflex_components_core/core/foreach.py similarity index 90% rename from reflex/components/core/foreach.py rename to packages/reflex-components-core/src/reflex_components_core/core/foreach.py index 38f921e5cf8..6e36051577d 100644 --- a/reflex/components/core/foreach.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/foreach.py @@ -8,16 +8,16 @@ from hashlib import md5 from typing import Any -from reflex.components.base.fragment import Fragment -from reflex.components.component import Component, field -from reflex.components.core.cond import cond -from reflex.components.tags import IterTag -from reflex.constants import MemoizationMode -from reflex.constants.state import FIELD_MARKER -from reflex.state import ComponentState -from reflex.utils import types -from reflex.utils.exceptions import UntypedVarError -from reflex.vars.base import LiteralVar, Var +from reflex_core.components.component import Component, field +from reflex_core.components.tags import IterTag +from reflex_core.constants import MemoizationMode +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.utils import types +from reflex_core.utils.exceptions import UntypedVarError +from reflex_core.vars.base import LiteralVar, Var + +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core.cond import cond class ForeachVarError(TypeError): @@ -64,7 +64,9 @@ def create( # noqa: DAR401 with_traceback # noqa: DAR402 UntypedVarError """ - from reflex.vars import ArrayVar, ObjectVar, StringVar + from reflex_core.vars import ArrayVar, ObjectVar, StringVar + + from reflex.state import ComponentState iterable = ( LiteralVar.create(iterable).guess_type() diff --git a/reflex/components/core/helmet.py b/packages/reflex-components-core/src/reflex_components_core/core/helmet.py similarity index 75% rename from reflex/components/core/helmet.py rename to packages/reflex-components-core/src/reflex_components_core/core/helmet.py index e4d1730759e..823003808f5 100644 --- a/reflex/components/core/helmet.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/helmet.py @@ -1,6 +1,6 @@ """Helmet component module.""" -from reflex.components.component import Component +from reflex_core.components.component import Component class Helmet(Component): diff --git a/reflex/components/core/html.py b/packages/reflex-components-core/src/reflex_components_core/core/html.py similarity index 88% rename from reflex/components/core/html.py rename to packages/reflex-components-core/src/reflex_components_core/core/html.py index 33a18fbcbc4..2e989cc1782 100644 --- a/reflex/components/core/html.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/html.py @@ -1,8 +1,9 @@ """A html component.""" -from reflex.components.component import field -from reflex.components.el.elements.typography import Div -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_core.el.elements.typography import Div class Html(Div): diff --git a/reflex/components/core/layout/__init__.py b/packages/reflex-components-core/src/reflex_components_core/core/layout/__init__.py similarity index 100% rename from reflex/components/core/layout/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/core/layout/__init__.py diff --git a/packages/reflex-components-core/src/reflex_components_core/core/markdown_component_map.py b/packages/reflex-components-core/src/reflex_components_core/core/markdown_component_map.py new file mode 100644 index 00000000000..ca1b99d90b0 --- /dev/null +++ b/packages/reflex-components-core/src/reflex_components_core/core/markdown_component_map.py @@ -0,0 +1,77 @@ +"""Markdown component map mixin.""" + +from __future__ import annotations + +import dataclasses +from collections.abc import Sequence + +from reflex_core.vars.base import Var, VarData +from reflex_core.vars.function import ArgsFunctionOperation, DestructuredArg + +# Special vars used in the component map. +_CHILDREN = Var(_js_expr="children", _var_type=str) +_PROPS_SPREAD = Var(_js_expr="...props") + + +@dataclasses.dataclass() +class MarkdownComponentMap: + """Mixin class for handling custom component maps in Markdown components.""" + + _explicit_return: bool = dataclasses.field(default=False) + + @classmethod + def get_component_map_custom_code(cls) -> Var: + """Get the custom code for the component map. + + Returns: + The custom code for the component map. + """ + return Var("") + + @classmethod + def create_map_fn_var( + cls, + fn_body: Var | None = None, + fn_args: Sequence[str] | None = None, + explicit_return: bool | None = None, + var_data: VarData | None = None, + ) -> Var: + """Create a function Var for the component map. + + Args: + fn_body: The formatted component as a string. + fn_args: The function arguments. + explicit_return: Whether to use explicit return syntax. + var_data: The var data for the function. + + Returns: + The function Var for the component map. + """ + fn_args = fn_args or cls.get_fn_args() + fn_body = fn_body if fn_body is not None else cls.get_fn_body() + explicit_return = explicit_return or cls._explicit_return + + return ArgsFunctionOperation.create( + args_names=(DestructuredArg(fields=tuple(fn_args)),), + return_expr=fn_body, + explicit_return=explicit_return, + _var_data=var_data, + ) + + @classmethod + def get_fn_args(cls) -> Sequence[str]: + """Get the function arguments for the component map. + + Returns: + The function arguments as a list of strings. + """ + return ["node", _CHILDREN._js_expr, _PROPS_SPREAD._js_expr] + + @classmethod + def get_fn_body(cls) -> Var: + """Get the function body for the component map. + + Returns: + The function body as a string. + """ + return Var(_js_expr="undefined", _var_type=None) diff --git a/reflex/components/core/match.py b/packages/reflex-components-core/src/reflex_components_core/core/match.py similarity index 94% rename from reflex/components/core/match.py rename to packages/reflex-components-core/src/reflex_components_core/core/match.py index f567cb79d37..b7285ebb0b3 100644 --- a/reflex/components/core/match.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/match.py @@ -3,16 +3,22 @@ import textwrap from typing import Any, cast -from reflex.components.base import Fragment -from reflex.components.component import BaseComponent, Component, MemoizationLeaf, field -from reflex.components.tags import Tag -from reflex.components.tags.match_tag import MatchTag -from reflex.style import Style -from reflex.utils import format -from reflex.utils.exceptions import MatchTypeError -from reflex.utils.imports import ImportDict -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var +from reflex_core.components.component import ( + BaseComponent, + Component, + MemoizationLeaf, + field, +) +from reflex_core.components.tags import Tag +from reflex_core.components.tags.match_tag import MatchTag +from reflex_core.style import Style +from reflex_core.utils import format +from reflex_core.utils.exceptions import MatchTypeError +from reflex_core.utils.imports import ImportDict +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var + +from reflex_components_core.base import Fragment class Match(MemoizationLeaf): diff --git a/reflex/components/core/responsive.py b/packages/reflex-components-core/src/reflex_components_core/core/responsive.py similarity index 80% rename from reflex/components/core/responsive.py rename to packages/reflex-components-core/src/reflex_components_core/core/responsive.py index e1c7f0cb305..bb8e16e4bfb 100644 --- a/reflex/components/core/responsive.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/responsive.py @@ -1,6 +1,6 @@ """Responsive components.""" -from reflex.components.radix.themes.layout.box import Box +from reflex_components_core.el.elements.typography import Div # Add responsive styles shortcuts. @@ -14,7 +14,7 @@ def mobile_only(*children, **props): Returns: The component. """ - return Box.create(*children, **props, display=["block", "none", "none", "none"]) + return Div.create(*children, **props, display=["block", "none", "none", "none"]) def tablet_only(*children, **props): @@ -27,7 +27,7 @@ def tablet_only(*children, **props): Returns: The component. """ - return Box.create(*children, **props, display=["none", "block", "block", "none"]) + return Div.create(*children, **props, display=["none", "block", "block", "none"]) def desktop_only(*children, **props): @@ -40,7 +40,7 @@ def desktop_only(*children, **props): Returns: The component. """ - return Box.create(*children, **props, display=["none", "none", "none", "block"]) + return Div.create(*children, **props, display=["none", "none", "none", "block"]) def tablet_and_desktop(*children, **props): @@ -53,7 +53,7 @@ def tablet_and_desktop(*children, **props): Returns: The component. """ - return Box.create(*children, **props, display=["none", "block", "block", "block"]) + return Div.create(*children, **props, display=["none", "block", "block", "block"]) def mobile_and_tablet(*children, **props): @@ -66,4 +66,4 @@ def mobile_and_tablet(*children, **props): Returns: The component. """ - return Box.create(*children, **props, display=["block", "block", "block", "none"]) + return Div.create(*children, **props, display=["block", "block", "block", "none"]) diff --git a/reflex/components/core/sticky.py b/packages/reflex-components-core/src/reflex_components_core/core/sticky.py similarity index 87% rename from reflex/components/core/sticky.py rename to packages/reflex-components-core/src/reflex_components_core/core/sticky.py index 4373e0e2fc6..864064f4330 100644 --- a/reflex/components/core/sticky.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/sticky.py @@ -1,13 +1,13 @@ """Components for displaying the Reflex sticky logo.""" -from reflex.components.component import ComponentNamespace -from reflex.components.core.colors import color -from reflex.components.core.cond import color_mode_cond -from reflex.components.core.responsive import desktop_only -from reflex.components.el.elements.inline import A -from reflex.components.el.elements.media import Path, Rect, Svg -from reflex.components.radix.themes.typography.text import Text -from reflex.style import Style +from reflex_core.components.component import ComponentNamespace +from reflex_core.style import Style + +from reflex_components_core.core.colors import color +from reflex_components_core.core.cond import color_mode_cond +from reflex_components_core.core.responsive import desktop_only +from reflex_components_core.el.elements.inline import A, Span +from reflex_components_core.el.elements.media import Path, Rect, Svg class StickyLogo(Svg): @@ -41,7 +41,7 @@ def add_style(self): }) -class StickyLabel(Text): +class StickyLabel(Span): """A label that displays the Reflex sticky.""" @classmethod diff --git a/reflex/components/core/upload.py b/packages/reflex-components-core/src/reflex_components_core/core/upload.py similarity index 93% rename from reflex/components/core/upload.py rename to packages/reflex-components-core/src/reflex_components_core/core/upload.py index a74da9e3d8b..ebe5019d526 100644 --- a/reflex/components/core/upload.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/upload.py @@ -6,23 +6,18 @@ from pathlib import Path from typing import Any, ClassVar -from reflex._upload import UploadChunkIterator, UploadFile -from reflex.components.base.fragment import Fragment -from reflex.components.component import ( +from reflex_components_sonner.toast import toast +from reflex_core.components.component import ( Component, ComponentNamespace, MemoizationLeaf, StatefulComponent, field, ) -from reflex.components.core.cond import cond -from reflex.components.el.elements.forms import Input -from reflex.components.radix.themes.layout.box import Box -from reflex.components.sonner.toast import toast -from reflex.constants import Dirs -from reflex.constants.compiler import Hooks, Imports -from reflex.environment import environment -from reflex.event import ( +from reflex_core.constants import Dirs +from reflex_core.constants.compiler import Hooks, Imports +from reflex_core.environment import environment +from reflex_core.event import ( CallableEventSpec, EventChain, EventHandler, @@ -34,14 +29,20 @@ run_script, upload_files, ) -from reflex.style import Style -from reflex.utils import format -from reflex.utils.imports import ImportVar -from reflex.vars import VarData -from reflex.vars.base import Var, get_unique_variable_name -from reflex.vars.function import FunctionVar -from reflex.vars.object import ObjectVar -from reflex.vars.sequence import ArrayVar, LiteralStringVar +from reflex_core.style import Style +from reflex_core.utils import format +from reflex_core.utils.imports import ImportVar +from reflex_core.vars import VarData +from reflex_core.vars.base import Var, get_unique_variable_name +from reflex_core.vars.function import FunctionVar +from reflex_core.vars.object import ObjectVar +from reflex_core.vars.sequence import ArrayVar, LiteralStringVar + +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core._upload import UploadChunkIterator, UploadFile +from reflex_components_core.core.cond import cond +from reflex_components_core.el.elements.forms import Input +from reflex_components_core.el.elements.typography import Div DEFAULT_UPLOAD_ID: str = "default" @@ -411,7 +412,7 @@ def create(cls, *children, **props) -> Component: ] # The dropzone to use. - zone = Box.create( + zone = Div.create( upload, *children, **{k: v for k, v in props.items() if k not in supported_props}, diff --git a/reflex/components/core/window_events.py b/packages/reflex-components-core/src/reflex_components_core/core/window_events.py similarity index 83% rename from reflex/components/core/window_events.py rename to packages/reflex-components-core/src/reflex_components_core/core/window_events.py index 3e2e980ef1f..7ba473f6cfa 100644 --- a/reflex/components/core/window_events.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/window_events.py @@ -4,13 +4,13 @@ from typing import Any, cast -import reflex as rx -from reflex.components.base.fragment import Fragment -from reflex.components.component import StatefulComponent, field -from reflex.constants.compiler import Hooks -from reflex.event import key_event, no_args_event_spec -from reflex.vars.base import Var, VarData -from reflex.vars.object import ObjectVar +from reflex_core.components.component import StatefulComponent, field +from reflex_core.constants.compiler import Hooks +from reflex_core.event import EventHandler, key_event, no_args_event_spec +from reflex_core.vars.base import Var, VarData +from reflex_core.vars.object import ObjectVar + +from reflex_components_core.base.fragment import Fragment def _on_resize_spec() -> tuple[Var[int], Var[int]]: @@ -55,31 +55,31 @@ def _on_storage_spec(e: ObjectVar) -> tuple[Var[str], Var[str], Var[str], Var[st class WindowEventListener(Fragment): """A component that listens for window events.""" - on_resize: rx.EventHandler[_on_resize_spec] = field( + on_resize: EventHandler[_on_resize_spec] = field( doc="Triggered when the browser window is resized. Receives the new width and height in pixels." ) - on_scroll: rx.EventHandler[_on_scroll_spec] = field( + on_scroll: EventHandler[_on_scroll_spec] = field( doc="Triggered when the user scrolls the page. Receives the current horizontal and vertical scroll positions." ) - on_focus: rx.EventHandler[no_args_event_spec] = field( + on_focus: EventHandler[no_args_event_spec] = field( doc="Triggered when the browser tab or window gains focus (e.g. user switches back to the tab)." ) - on_blur: rx.EventHandler[no_args_event_spec] = field( + on_blur: EventHandler[no_args_event_spec] = field( doc="Triggered when the browser tab or window loses focus (e.g. user switches to another tab)." ) - on_visibility_change: rx.EventHandler[_on_visibility_change_spec] = field( + on_visibility_change: EventHandler[_on_visibility_change_spec] = field( doc="Triggered when the page becomes visible or hidden (e.g. tab switch or minimize). Receives a boolean indicating whether the document is hidden." ) - on_before_unload: rx.EventHandler[no_args_event_spec] = field( + on_before_unload: EventHandler[no_args_event_spec] = field( doc="Triggered just before the user navigates away from or closes the page. Useful for cleanup or prompting unsaved-changes warnings." ) - on_key_down: rx.EventHandler[key_event] = field( + on_key_down: EventHandler[key_event] = field( doc="Triggered when a key is pressed anywhere on the page. Receives the key name and active modifier keys (shift, ctrl, alt, meta)." ) - on_popstate: rx.EventHandler[no_args_event_spec] = field( + on_popstate: EventHandler[no_args_event_spec] = field( doc="Triggered when the user navigates back or forward via the browser history buttons." ) - on_storage: rx.EventHandler[_on_storage_spec] = field( + on_storage: EventHandler[_on_storage_spec] = field( doc="Triggered when localStorage or sessionStorage is modified in another tab. Receives the key, old value, new value, and the URL of the document that changed the storage." ) diff --git a/reflex/components/datadisplay/__init__.py b/packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.py similarity index 90% rename from reflex/components/datadisplay/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.py index 1aff69676cc..33e01746acf 100644 --- a/reflex/components/datadisplay/__init__.py +++ b/packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader _SUBMOD_ATTRS: dict[str, list[str]] = { "code": [ diff --git a/reflex/components/datadisplay/logo.py b/packages/reflex-components-core/src/reflex_components_core/datadisplay/logo.py similarity index 75% rename from reflex/components/datadisplay/logo.py rename to packages/reflex-components-core/src/reflex_components_core/datadisplay/logo.py index 235ed91c2e2..fb1721875bd 100644 --- a/reflex/components/datadisplay/logo.py +++ b/packages/reflex-components-core/src/reflex_components_core/datadisplay/logo.py @@ -1,11 +1,14 @@ """A Reflex logo component.""" -import reflex as rx +from reflex_core.vars import Var -SVG_COLOR = rx.color_mode_cond("#110F1F", "white") +from reflex_components_core import el +from reflex_components_core.core import color_mode_cond +SVG_COLOR = color_mode_cond("#110F1F", "white") -def svg_logo(color: str | rx.Var[str] = SVG_COLOR, **props): + +def svg_logo(color: str | Var[str] = SVG_COLOR, **props): """A Reflex logo SVG. Args: @@ -17,7 +20,7 @@ def svg_logo(color: str | rx.Var[str] = SVG_COLOR, **props): """ def logo_path(d: str): - return rx.el.path(d=d) + return el.path(d=d) paths = [ "M0 11.5999V0.399902H8.96V4.8799H6.72V2.6399H2.24V4.8799H6.72V7.1199H2.24V11.5999H0ZM6.72 11.5999V7.1199H8.96V11.5999H6.72Z", @@ -28,9 +31,9 @@ def logo_path(d: str): "M47.04 4.8799V0.399902H49.28V4.8799H47.04ZM53.76 4.8799V0.399902H56V4.8799H53.76ZM49.28 7.1199V4.8799H53.76V7.1199H49.28ZM47.04 11.5999V7.1199H49.28V11.5999H47.04ZM53.76 11.5999V7.1199H56V11.5999H53.76Z", ] - return rx.el.svg( + return el.svg( *[logo_path(d) for d in paths], - rx.el.title("Reflex"), + el.title("Reflex"), aria_label="Reflex", role="img", width=props.pop("width", "56"), @@ -50,18 +53,20 @@ def logo(**props): Returns: The logo component. """ - return rx.center( - rx.link( - rx.hstack( + return el.div( + el.a( + el.div( "Built with ", svg_logo(), - text_align="center", - align="center", + display="flex", + align_items="center", + gap="0.5em", padding="1em", ), href="https://reflex.dev", - size="3", ), + display="flex", + justify_content="center", width=props.pop("width", "100%"), **props, ) diff --git a/reflex/components/el/__init__.py b/packages/reflex-components-core/src/reflex_components_core/el/__init__.py similarity index 77% rename from reflex/components/el/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/el/__init__.py index 88acb2a313b..49e9cb5bccc 100644 --- a/reflex/components/el/__init__.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/__init__.py @@ -2,18 +2,18 @@ from __future__ import annotations -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader from . import elements _SUBMODULES: set[str] = {"elements"} _SUBMOD_ATTRS: dict[str, list[str]] = { - # rx.el.a is replaced by React Router's Link. + # el.a is replaced by React Router's Link. f"elements.{k}": [attr for attr in attrs if attr != "a"] for k, attrs in elements._MAPPING.items() } _EXTRA_MAPPINGS: dict[str, str] = { - "a": "reflex.components.react_router.link", + "a": "reflex_components_core.react_router.link", } __getattr__, __dir__, __all__ = lazy_loader.attach( diff --git a/reflex/components/el/element.py b/packages/reflex-components-core/src/reflex_components_core/el/element.py similarity index 90% rename from reflex/components/el/element.py rename to packages/reflex-components-core/src/reflex_components_core/el/element.py index 7232ee38f17..9903284446b 100644 --- a/reflex/components/el/element.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/element.py @@ -2,7 +2,7 @@ from typing import ClassVar -from reflex.components.component import Component +from reflex_core.components.component import Component class Element(Component): diff --git a/reflex/components/el/elements/__init__.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/__init__.py similarity index 96% rename from reflex/components/el/elements/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/__init__.py index eb7c894a032..2b4494bcb35 100644 --- a/reflex/components/el/elements/__init__.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader _MAPPING = { "forms": [ @@ -142,7 +142,7 @@ EXCLUDE = ["del_", "Del", "image", "style"] for v in _MAPPING.values(): - from reflex.utils.format import to_camel_case + from reflex_core.utils.format import to_camel_case v.extend([ to_camel_case(mod)[0].upper() + to_camel_case(mod)[1:] diff --git a/reflex/components/el/elements/base.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/base.py similarity index 95% rename from reflex/components/el/elements/base.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/base.py index 70e9cff1beb..296a7030e83 100644 --- a/reflex/components/el/elements/base.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/base.py @@ -2,9 +2,10 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.el.element import Element -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_core.el.element import Element AutoCapitalize = Literal["off", "none", "on", "sentences", "words", "characters"] ContentEditable = Literal["inherit", "plaintext-only"] | bool diff --git a/reflex/components/el/elements/forms.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py similarity index 98% rename from reflex/components/el/elements/forms.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py index ce9b7de03cf..0c4ab4feeb3 100644 --- a/reflex/components/el/elements/forms.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py @@ -6,11 +6,10 @@ from hashlib import md5 from typing import Any, ClassVar, Literal -from reflex.components.component import field -from reflex.components.el.element import Element -from reflex.components.tags.tag import Tag -from reflex.constants import Dirs, EventTriggers -from reflex.event import ( +from reflex_core.components.component import field +from reflex_core.components.tags.tag import Tag +from reflex_core.constants import Dirs, EventTriggers +from reflex_core.event import ( FORM_DATA, EventChain, EventHandler, @@ -23,10 +22,12 @@ on_submit_string_event, prevent_default, ) -from reflex.utils.imports import ImportDict -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var -from reflex.vars.number import ternary_operation +from reflex_core.utils.imports import ImportDict +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.number import ternary_operation + +from reflex_components_core.el.element import Element from .base import BaseHTML diff --git a/reflex/components/el/elements/inline.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/inline.py similarity index 97% rename from reflex/components/el/elements/inline.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/inline.py index c9e8fa6bbc8..c3b43aad317 100644 --- a/reflex/components/el/elements/inline.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/inline.py @@ -2,8 +2,8 @@ from typing import ClassVar, Literal -from reflex.components.component import field -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var from .base import BaseHTML diff --git a/reflex/components/el/elements/media.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/media.py similarity index 99% rename from reflex/components/el/elements/media.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/media.py index a53076e124c..2e978426e81 100644 --- a/reflex/components/el/elements/media.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/media.py @@ -2,11 +2,11 @@ from typing import Any, Literal -from reflex import Component, ComponentNamespace -from reflex.components.component import field -from reflex.components.el.elements.inline import ReferrerPolicy -from reflex.constants.colors import Color -from reflex.vars.base import Var +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.constants.colors import Color +from reflex_core.vars.base import Var + +from reflex_components_core.el.elements.inline import ReferrerPolicy from .base import BaseHTML diff --git a/reflex/components/el/elements/metadata.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py similarity index 89% rename from reflex/components/el/elements/metadata.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py index 1b7c2089f1d..39d487fb8cb 100644 --- a/reflex/components/el/elements/metadata.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py @@ -1,10 +1,11 @@ """Metadata classes.""" -from reflex.components.component import field -from reflex.components.el.element import Element -from reflex.components.el.elements.inline import ReferrerPolicy -from reflex.components.el.elements.media import CrossOrigin -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_core.el.element import Element +from reflex_components_core.el.elements.inline import ReferrerPolicy +from reflex_components_core.el.elements.media import CrossOrigin from .base import BaseHTML diff --git a/reflex/components/el/elements/other.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/other.py similarity index 94% rename from reflex/components/el/elements/other.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/other.py index b73e75c2d63..1b9a5c28a31 100644 --- a/reflex/components/el/elements/other.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/other.py @@ -1,7 +1,7 @@ """Other classes.""" -from reflex.components.component import field -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var from .base import BaseHTML diff --git a/reflex/components/el/elements/scripts.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.py similarity index 84% rename from reflex/components/el/elements/scripts.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.py index 2435a749c98..d6cdb7966c8 100644 --- a/reflex/components/el/elements/scripts.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.py @@ -1,9 +1,10 @@ """Scripts classes.""" -from reflex.components.component import field -from reflex.components.el.elements.inline import ReferrerPolicy -from reflex.components.el.elements.media import CrossOrigin -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_core.el.elements.inline import ReferrerPolicy +from reflex_components_core.el.elements.media import CrossOrigin from .base import BaseHTML diff --git a/reflex/components/el/elements/sectioning.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/sectioning.py similarity index 100% rename from reflex/components/el/elements/sectioning.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/sectioning.py diff --git a/reflex/components/el/elements/tables.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/tables.py similarity index 96% rename from reflex/components/el/elements/tables.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/tables.py index d48a787812d..6ef588830af 100644 --- a/reflex/components/el/elements/tables.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/tables.py @@ -2,8 +2,8 @@ from typing import Literal -from reflex.components.component import field -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var from .base import BaseHTML diff --git a/reflex/components/el/elements/typography.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/typography.py similarity index 96% rename from reflex/components/el/elements/typography.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/typography.py index 50ee10b1f5d..85d3807d97d 100644 --- a/reflex/components/el/elements/typography.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/typography.py @@ -2,8 +2,8 @@ from typing import ClassVar, Literal -from reflex.components.component import field -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var from .base import BaseHTML diff --git a/reflex/components/react_router/__init__.py b/packages/reflex-components-core/src/reflex_components_core/react_router/__init__.py similarity index 100% rename from reflex/components/react_router/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/react_router/__init__.py diff --git a/reflex/components/react_router/dom.py b/packages/reflex-components-core/src/reflex_components_core/react_router/dom.py similarity index 94% rename from reflex/components/react_router/dom.py rename to packages/reflex-components-core/src/reflex_components_core/react_router/dom.py index 5654c829a89..a5eb70b4290 100644 --- a/reflex/components/react_router/dom.py +++ b/packages/reflex-components-core/src/reflex_components_core/react_router/dom.py @@ -4,9 +4,10 @@ from typing import ClassVar, Literal, TypedDict -from reflex.components.component import field -from reflex.components.el.elements.inline import A -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_core.el.elements.inline import A LiteralLinkDiscover = Literal["none", "render"] diff --git a/packages/reflex-components-dataeditor/README.md b/packages/reflex-components-dataeditor/README.md new file mode 100644 index 00000000000..6683e72a292 --- /dev/null +++ b/packages/reflex-components-dataeditor/README.md @@ -0,0 +1,3 @@ +# reflex-components-dataeditor + +Reflex dataeditor components. diff --git a/packages/reflex-components-dataeditor/pyproject.toml b/packages/reflex-components-dataeditor/pyproject.toml new file mode 100644 index 00000000000..a0420e0c9ee --- /dev/null +++ b/packages/reflex-components-dataeditor/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "reflex-components-dataeditor" +dynamic = ["version"] +description = "Reflex dataeditor components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = ["reflex-components-core"] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-dataeditor-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = [ + "ruff", + "reflex-core", + "reflex-components-core", + "reflex-components-lucide", + "reflex-components-sonner", +] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/__init__.py b/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/__init__.py new file mode 100644 index 00000000000..86d12c5542c --- /dev/null +++ b/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/__init__.py @@ -0,0 +1 @@ +"""Reflex DataEditor component.""" diff --git a/reflex/components/datadisplay/dataeditor.py b/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.py similarity index 96% rename from reflex/components/datadisplay/dataeditor.py rename to packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.py index cae0e7a9080..569263eef84 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.py @@ -7,16 +7,16 @@ from enum import Enum from typing import Any, Literal, TypedDict -from reflex.components.component import Component, NoSSRComponent, field -from reflex.components.literals import LiteralRowMarker -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.utils import console, format, types -from reflex.utils.imports import ImportDict, ImportVar -from reflex.utils.serializers import serializer -from reflex.vars import get_unique_variable_name -from reflex.vars.base import Var -from reflex.vars.function import FunctionStringVar -from reflex.vars.sequence import ArrayVar +from reflex_core.components.component import Component, NoSSRComponent, field +from reflex_core.components.literals import LiteralRowMarker +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.utils import console, format, types +from reflex_core.utils.imports import ImportDict, ImportVar +from reflex_core.utils.serializers import serializer +from reflex_core.vars import get_unique_variable_name +from reflex_core.vars.base import Var +from reflex_core.vars.function import FunctionStringVar +from reflex_core.vars.sequence import ArrayVar # TODO: Fix the serialization issue for custom types. @@ -502,7 +502,7 @@ def create(cls, *children, **props) -> Component: Raises: ValueError: invalid input. """ - from reflex.components.el import Div + from reflex_components_core.el import Div columns = props.get("columns", []) data = props.get("data", []) @@ -564,7 +564,7 @@ def _get_app_wrap_components() -> dict[tuple[int, str], Component]: Returns: The app wrap components. """ - from reflex.components.el import Div + from reflex_components_core.el import Div class Portal(Div): def get_ref(self): diff --git a/packages/reflex-components-gridjs/README.md b/packages/reflex-components-gridjs/README.md new file mode 100644 index 00000000000..f476e663587 --- /dev/null +++ b/packages/reflex-components-gridjs/README.md @@ -0,0 +1,3 @@ +# reflex-components-gridjs + +Reflex gridjs components. diff --git a/packages/reflex-components-gridjs/pyproject.toml b/packages/reflex-components-gridjs/pyproject.toml new file mode 100644 index 00000000000..631d694dbd3 --- /dev/null +++ b/packages/reflex-components-gridjs/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "reflex-components-gridjs" +dynamic = ["version"] +description = "Reflex gridjs components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-gridjs-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = ["ruff", "reflex-core"] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/reflex/components/gridjs/__init__.py b/packages/reflex-components-gridjs/src/reflex_components_gridjs/__init__.py similarity index 100% rename from reflex/components/gridjs/__init__.py rename to packages/reflex-components-gridjs/src/reflex_components_gridjs/__init__.py diff --git a/reflex/components/gridjs/datatable.py b/packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py similarity index 92% rename from reflex/components/gridjs/datatable.py rename to packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py index 4ffc8db52fb..9725e8d94f1 100644 --- a/reflex/components/gridjs/datatable.py +++ b/packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py @@ -5,12 +5,12 @@ from collections.abc import Sequence from typing import Any -from reflex.components.component import NoSSRComponent, field -from reflex.components.tags import Tag -from reflex.utils import types -from reflex.utils.imports import ImportDict -from reflex.utils.serializers import serialize -from reflex.vars.base import LiteralVar, Var, is_computed_var +from reflex_core.components.component import NoSSRComponent, field +from reflex_core.components.tags import Tag +from reflex_core.utils import types +from reflex_core.utils.imports import ImportDict +from reflex_core.utils.serializers import serialize +from reflex_core.vars.base import LiteralVar, Var, is_computed_var class Gridjs(NoSSRComponent): diff --git a/packages/reflex-components-lucide/README.md b/packages/reflex-components-lucide/README.md new file mode 100644 index 00000000000..f889e94ec50 --- /dev/null +++ b/packages/reflex-components-lucide/README.md @@ -0,0 +1,3 @@ +# reflex-components-lucide + +Reflex lucide components. diff --git a/packages/reflex-components-lucide/pyproject.toml b/packages/reflex-components-lucide/pyproject.toml new file mode 100644 index 00000000000..27b6c6821d0 --- /dev/null +++ b/packages/reflex-components-lucide/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "reflex-components-lucide" +dynamic = ["version"] +description = "Reflex lucide components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-lucide-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = ["ruff", "reflex-core"] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/reflex/components/lucide/__init__.py b/packages/reflex-components-lucide/src/reflex_components_lucide/__init__.py similarity index 100% rename from reflex/components/lucide/__init__.py rename to packages/reflex-components-lucide/src/reflex_components_lucide/__init__.py diff --git a/reflex/components/lucide/icon.py b/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py similarity index 99% rename from reflex/components/lucide/icon.py rename to packages/reflex-components-lucide/src/reflex_components_lucide/icon.py index 1eaea18bca6..beb3eb13b86 100644 --- a/reflex/components/lucide/icon.py +++ b/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py @@ -1,10 +1,10 @@ """Lucide Icon component.""" -from reflex.components.component import Component, field -from reflex.utils import console, format -from reflex.utils.imports import ImportVar -from reflex.vars.base import LiteralVar, Var -from reflex.vars.sequence import LiteralStringVar, StringVar +from reflex_core.components.component import Component, field +from reflex_core.utils import console, format +from reflex_core.utils.imports import ImportVar +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.sequence import LiteralStringVar, StringVar LUCIDE_LIBRARY = "lucide-react@0.577.0" diff --git a/packages/reflex-components-markdown/README.md b/packages/reflex-components-markdown/README.md new file mode 100644 index 00000000000..51648cfb68d --- /dev/null +++ b/packages/reflex-components-markdown/README.md @@ -0,0 +1,3 @@ +# reflex-components-markdown + +Reflex markdown components. diff --git a/packages/reflex-components-markdown/pyproject.toml b/packages/reflex-components-markdown/pyproject.toml new file mode 100644 index 00000000000..b85165c2bf4 --- /dev/null +++ b/packages/reflex-components-markdown/pyproject.toml @@ -0,0 +1,37 @@ +[project] +name = "reflex-components-markdown" +dynamic = ["version"] +description = "Reflex markdown components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [ + "reflex-components-code", + "reflex-components-core", + "reflex-components-radix", +] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-markdown-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = [ + "ruff", + "reflex-core", + "reflex-components-core", + "reflex-components-lucide", + "reflex-components-sonner", +] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/reflex/components/markdown/__init__.py b/packages/reflex-components-markdown/src/reflex_components_markdown/__init__.py similarity index 100% rename from reflex/components/markdown/__init__.py rename to packages/reflex-components-markdown/src/reflex_components_markdown/__init__.py diff --git a/reflex/components/markdown/markdown.py b/packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py similarity index 84% rename from reflex/components/markdown/markdown.py rename to packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py index 8da316bb621..6e90c93390a 100644 --- a/reflex/components/markdown/markdown.py +++ b/packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py @@ -2,7 +2,6 @@ from __future__ import annotations -import dataclasses import textwrap from collections.abc import Callable, Sequence from functools import lru_cache @@ -10,21 +9,21 @@ from types import SimpleNamespace from typing import Any -from reflex.components.component import ( +from reflex_components_core.core.markdown_component_map import MarkdownComponentMap +from reflex_components_core.el.elements.typography import Div +from reflex_core.components.component import ( BaseComponent, Component, ComponentNamespace, CustomComponent, field, ) -from reflex.components.el.elements.typography import Div -from reflex.components.tags.tag import Tag -from reflex.utils import console -from reflex.utils.imports import ImportDict, ImportTypes, ImportVar -from reflex.vars.base import LiteralVar, Var, VarData -from reflex.vars.function import ArgsFunctionOperation, DestructuredArg -from reflex.vars.number import ternary_operation -from reflex.vars.sequence import LiteralArrayVar +from reflex_core.components.tags.tag import Tag +from reflex_core.utils import console +from reflex_core.utils.imports import ImportDict, ImportTypes, ImportVar +from reflex_core.vars.base import LiteralVar, Var, VarData +from reflex_core.vars.number import ternary_operation +from reflex_core.vars.sequence import LiteralArrayVar # Special vars used in the component map. _CHILDREN = Var(_js_expr="children", _var_type=str) @@ -88,79 +87,79 @@ def create( def _h1(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h1", size="6", margin_y="0.5em") def _h2(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h2", size="5", margin_y="0.5em") def _h3(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h3", size="4", margin_y="0.5em") def _h4(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h4", size="3", margin_y="0.5em") def _h5(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h5", size="2", margin_y="0.5em") def _h6(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h6", size="1", margin_y="0.5em") def _p(value: object): - from reflex.components.radix.themes.typography.text import Text + from reflex_components_radix.themes.typography.text import Text return Text.create(value, margin_y="1em") def _ul(value: object): - from reflex.components.radix.themes.layout.list import UnorderedList + from reflex_components_radix.themes.layout.list import UnorderedList return UnorderedList.create(value, margin_y="1em") def _ol(value: object): - from reflex.components.radix.themes.layout.list import OrderedList + from reflex_components_radix.themes.layout.list import OrderedList return OrderedList.create(value, margin_y="1em") def _li(value: object): - from reflex.components.radix.themes.layout.list import ListItem + from reflex_components_radix.themes.layout.list import ListItem return ListItem.create(value, margin_y="0.5em") def _a(value: object): - from reflex.components.radix.themes.typography.link import Link + from reflex_components_radix.themes.typography.link import Link return Link.create(value) def _code(value: object): - from reflex.components.radix.themes.typography.code import Code + from reflex_components_radix.themes.typography.code import Code return Code.create(value) def _codeblock(value: object, **props): - from reflex.components.datadisplay.code import CodeBlock + from reflex_components_code.code import CodeBlock return CodeBlock.create(value, margin_y="1em", wrap_long_lines=True, **props) @@ -190,70 +189,6 @@ def get_base_component_map() -> dict[str, Callable]: } -@dataclasses.dataclass() -class MarkdownComponentMap: - """Mixin class for handling custom component maps in Markdown components.""" - - _explicit_return: bool = dataclasses.field(default=False) - - @classmethod - def get_component_map_custom_code(cls) -> Var: - """Get the custom code for the component map. - - Returns: - The custom code for the component map. - """ - return Var("") - - @classmethod - def create_map_fn_var( - cls, - fn_body: Var | None = None, - fn_args: Sequence[str] | None = None, - explicit_return: bool | None = None, - var_data: VarData | None = None, - ) -> Var: - """Create a function Var for the component map. - - Args: - fn_body: The formatted component as a string. - fn_args: The function arguments. - explicit_return: Whether to use explicit return syntax. - var_data: The var data for the function. - - Returns: - The function Var for the component map. - """ - fn_args = fn_args or cls.get_fn_args() - fn_body = fn_body if fn_body is not None else cls.get_fn_body() - explicit_return = explicit_return or cls._explicit_return - - return ArgsFunctionOperation.create( - args_names=(DestructuredArg(fields=tuple(fn_args)),), - return_expr=fn_body, - explicit_return=explicit_return, - _var_data=var_data, - ) - - @classmethod - def get_fn_args(cls) -> Sequence[str]: - """Get the function arguments for the component map. - - Returns: - The function arguments as a list of strings. - """ - return ["node", _CHILDREN._js_expr, _PROPS_SPREAD._js_expr] - - @classmethod - def get_fn_body(cls) -> Var: - """Get the function body for the component map. - - Returns: - The function body as a string. - """ - return Var(_js_expr="undefined", _var_type=None) - - class Markdown(Component): """A markdown component.""" @@ -514,7 +449,7 @@ def _get_component_map_name(self) -> str: def _get_custom_code(self) -> str | None: hooks = {} - from reflex.compiler.templates import _render_hooks + from reflex_core.compiler.templates import _render_hooks for component_factory in self.component_map.values(): comp = component_factory(_MOCK_ARG) diff --git a/packages/reflex-components-moment/README.md b/packages/reflex-components-moment/README.md new file mode 100644 index 00000000000..ca7092de983 --- /dev/null +++ b/packages/reflex-components-moment/README.md @@ -0,0 +1,3 @@ +# reflex-components-moment + +Reflex moment components. diff --git a/packages/reflex-components-moment/pyproject.toml b/packages/reflex-components-moment/pyproject.toml new file mode 100644 index 00000000000..70f3cf1a6f4 --- /dev/null +++ b/packages/reflex-components-moment/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "reflex-components-moment" +dynamic = ["version"] +description = "Reflex moment components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-moment-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = ["ruff", "reflex-core"] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/reflex/components/moment/__init__.py b/packages/reflex-components-moment/src/reflex_components_moment/__init__.py similarity index 100% rename from reflex/components/moment/__init__.py rename to packages/reflex-components-moment/src/reflex_components_moment/__init__.py diff --git a/reflex/components/moment/moment.py b/packages/reflex-components-moment/src/reflex_components_moment/moment.py similarity index 95% rename from reflex/components/moment/moment.py rename to packages/reflex-components-moment/src/reflex_components_moment/moment.py index a7c40bd074d..979a9ffc83e 100644 --- a/reflex/components/moment/moment.py +++ b/packages/reflex-components-moment/src/reflex_components_moment/moment.py @@ -5,10 +5,10 @@ import dataclasses from datetime import date, datetime, time, timedelta -from reflex.components.component import NoSSRComponent, field -from reflex.event import EventHandler, passthrough_event_spec -from reflex.utils.imports import ImportDict -from reflex.vars.base import LiteralVar, Var +from reflex_core.components.component import NoSSRComponent, field +from reflex_core.event import EventHandler, passthrough_event_spec +from reflex_core.utils.imports import ImportDict +from reflex_core.vars.base import LiteralVar, Var @dataclasses.dataclass(frozen=True) diff --git a/packages/reflex-components-plotly/README.md b/packages/reflex-components-plotly/README.md new file mode 100644 index 00000000000..ebd0b1284a8 --- /dev/null +++ b/packages/reflex-components-plotly/README.md @@ -0,0 +1,3 @@ +# reflex-components-plotly + +Reflex plotly components. diff --git a/packages/reflex-components-plotly/pyproject.toml b/packages/reflex-components-plotly/pyproject.toml new file mode 100644 index 00000000000..aac8d08152d --- /dev/null +++ b/packages/reflex-components-plotly/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "reflex-components-plotly" +dynamic = ["version"] +description = "Reflex plotly components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = ["reflex-components-core"] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-plotly-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = [ + "ruff", + "reflex-core", + "reflex-components-core", + "reflex-components-lucide", + "reflex-components-sonner", +] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/reflex/components/plotly/__init__.py b/packages/reflex-components-plotly/src/reflex_components_plotly/__init__.py similarity index 90% rename from reflex/components/plotly/__init__.py rename to packages/reflex-components-plotly/src/reflex_components_plotly/__init__.py index 8743b31b288..e852a08c82e 100644 --- a/reflex/components/plotly/__init__.py +++ b/packages/reflex-components-plotly/src/reflex_components_plotly/__init__.py @@ -1,6 +1,6 @@ """Plotly components.""" -from reflex.components.component import ComponentNamespace +from reflex_core.components.component import ComponentNamespace from .plotly import ( Plotly, diff --git a/reflex/components/plotly/plotly.py b/packages/reflex-components-plotly/src/reflex_components_plotly/plotly.py similarity index 97% rename from reflex/components/plotly/plotly.py rename to packages/reflex-components-plotly/src/reflex_components_plotly/plotly.py index 3f66a1eeffc..38dcd233bb6 100644 --- a/reflex/components/plotly/plotly.py +++ b/packages/reflex-components-plotly/src/reflex_components_plotly/plotly.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING, Any, TypedDict, TypeVar -from reflex.components.component import Component, NoSSRComponent, field -from reflex.components.core.cond import color_mode_cond -from reflex.event import EventHandler, no_args_event_spec -from reflex.utils import console -from reflex.utils.imports import ImportDict, ImportVar -from reflex.vars.base import LiteralVar, Var +from reflex_components_core.core.cond import color_mode_cond +from reflex_core.components.component import Component, NoSSRComponent, field +from reflex_core.event import EventHandler, no_args_event_spec +from reflex_core.utils import console +from reflex_core.utils.imports import ImportDict, ImportVar +from reflex_core.vars.base import LiteralVar, Var try: from plotly.graph_objs import Figure diff --git a/packages/reflex-components-radix/README.md b/packages/reflex-components-radix/README.md new file mode 100644 index 00000000000..f5a060a25e3 --- /dev/null +++ b/packages/reflex-components-radix/README.md @@ -0,0 +1,3 @@ +# reflex-components-radix + +Reflex radix components. diff --git a/packages/reflex-components-radix/pyproject.toml b/packages/reflex-components-radix/pyproject.toml new file mode 100644 index 00000000000..9634cc731ef --- /dev/null +++ b/packages/reflex-components-radix/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "reflex-components-radix" +dynamic = ["version"] +description = "Reflex radix components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [ + "reflex-components-core", + "reflex-components-lucide", +] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-radix-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = [ + "ruff", + "reflex-core", + "reflex-components-core", + "reflex-components-lucide", + "reflex-components-sonner", +] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/reflex/components/radix/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/__init__.py similarity index 62% rename from reflex/components/radix/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/__init__.py index 6b1673b8887..e2e8541abae 100644 --- a/reflex/components/radix/__init__.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/__init__.py @@ -2,13 +2,15 @@ from __future__ import annotations -from reflex import RADIX_MAPPING -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader + +from reflex_components_radix.mappings import RADIX_MAPPING _SUBMODULES: set[str] = {"themes", "primitives"} _SUBMOD_ATTRS: dict[str, list[str]] = { - "".join(k.split("components.radix.")[-1]): v for k, v in RADIX_MAPPING.items() + "".join(k.split("reflex_components_radix.")[-1]): v + for k, v in RADIX_MAPPING.items() } __getattr__, __dir__, __all__ = lazy_loader.attach( __name__, diff --git a/packages/reflex-components-radix/src/reflex_components_radix/mappings.py b/packages/reflex-components-radix/src/reflex_components_radix/mappings.py new file mode 100644 index 00000000000..141dc6ac59c --- /dev/null +++ b/packages/reflex-components-radix/src/reflex_components_radix/mappings.py @@ -0,0 +1,131 @@ +"""Radix component mappings for lazy loading.""" + +RADIX_THEMES_MAPPING: dict[str, list[str]] = { + "reflex_components_radix.themes.base": ["color_mode", "theme", "theme_panel"], + "reflex_components_radix.themes.color_mode": ["color_mode"], +} +RADIX_THEMES_COMPONENTS_MAPPING: dict[str, list[str]] = { + **{ + f"reflex_components_radix.themes.components.{mod}": [mod] + for mod in [ + "alert_dialog", + "aspect_ratio", + "avatar", + "badge", + "button", + "callout", + "card", + "checkbox", + "context_menu", + "data_list", + "dialog", + "hover_card", + "icon_button", + "input", + "inset", + "popover", + "scroll_area", + "select", + "skeleton", + "slider", + "spinner", + "switch", + "table", + "tabs", + "text_area", + "tooltip", + "segmented_control", + "radio_cards", + "checkbox_cards", + "checkbox_group", + ] + }, + "reflex_components_radix.themes.components.text_field": ["text_field", "input"], + "reflex_components_radix.themes.components.radio_group": ["radio", "radio_group"], + "reflex_components_radix.themes.components.dropdown_menu": [ + "menu", + "dropdown_menu", + ], + "reflex_components_radix.themes.components.separator": ["divider", "separator"], + "reflex_components_radix.themes.components.progress": ["progress"], +} + +RADIX_THEMES_LAYOUT_MAPPING: dict[str, list[str]] = { + "reflex_components_radix.themes.layout.box": [ + "box", + ], + "reflex_components_radix.themes.layout.center": [ + "center", + ], + "reflex_components_radix.themes.layout.container": [ + "container", + ], + "reflex_components_radix.themes.layout.flex": [ + "flex", + ], + "reflex_components_radix.themes.layout.grid": [ + "grid", + ], + "reflex_components_radix.themes.layout.section": [ + "section", + ], + "reflex_components_radix.themes.layout.spacer": [ + "spacer", + ], + "reflex_components_radix.themes.layout.stack": [ + "stack", + "hstack", + "vstack", + ], + "reflex_components_radix.themes.layout.list": [ + "list", + "list_item", + "ordered_list", + "unordered_list", + ], +} + +RADIX_THEMES_TYPOGRAPHY_MAPPING: dict[str, list[str]] = { + "reflex_components_radix.themes.typography.blockquote": [ + "blockquote", + ], + "reflex_components_radix.themes.typography.code": [ + "code", + ], + "reflex_components_radix.themes.typography.heading": [ + "heading", + ], + "reflex_components_radix.themes.typography.link": [ + "link", + ], + "reflex_components_radix.themes.typography.text": [ + "text", + ], +} + +RADIX_PRIMITIVES_MAPPING: dict[str, list[str]] = { + "reflex_components_radix.primitives.accordion": [ + "accordion", + ], + "reflex_components_radix.primitives.drawer": [ + "drawer", + ], + "reflex_components_radix.primitives.form": [ + "form", + ], + "reflex_components_radix.primitives.progress": [ + "progress", + ], +} + +RADIX_PRIMITIVES_SHORTCUT_MAPPING: dict[str, list[str]] = { + k: v for k, v in RADIX_PRIMITIVES_MAPPING.items() if "progress" not in k +} + +RADIX_MAPPING: dict[str, list[str]] = { + **RADIX_THEMES_MAPPING, + **RADIX_THEMES_COMPONENTS_MAPPING, + **RADIX_THEMES_TYPOGRAPHY_MAPPING, + **RADIX_THEMES_LAYOUT_MAPPING, + **RADIX_PRIMITIVES_SHORTCUT_MAPPING, +} diff --git a/reflex/components/radix/primitives/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.py similarity index 64% rename from reflex/components/radix/primitives/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.py index 7b6cd7b607d..799113be297 100644 --- a/reflex/components/radix/primitives/__init__.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations -from reflex import RADIX_PRIMITIVES_MAPPING -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader + +from reflex_components_radix.mappings import RADIX_PRIMITIVES_MAPPING _SUBMOD_ATTRS: dict[str, list[str]] = { - "".join(k.split("components.radix.primitives.")[-1]): v + "".join(k.split("reflex_components_radix.primitives.")[-1]): v for k, v in RADIX_PRIMITIVES_MAPPING.items() } | {"dialog": ["dialog"]} diff --git a/reflex/components/radix/primitives/accordion.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.py similarity index 96% rename from reflex/components/radix/primitives/accordion.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.py index 0579dffee7e..9eb2eb208f3 100644 --- a/reflex/components/radix/primitives/accordion.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.py @@ -5,17 +5,18 @@ from collections.abc import Sequence from typing import Any, ClassVar, Literal -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.colors import color -from reflex.components.core.cond import cond -from reflex.components.lucide.icon import Icon -from reflex.components.radix.primitives.base import RadixPrimitiveComponent -from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler -from reflex.style import Style -from reflex.vars import get_uuid_string_var -from reflex.vars.base import LiteralVar, Var +from reflex_components_core.core.colors import color +from reflex_components_core.core.cond import cond +from reflex_components_lucide.icon import Icon +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler +from reflex_core.style import Style +from reflex_core.vars import get_uuid_string_var +from reflex_core.vars.base import LiteralVar, Var + +from reflex_components_radix.primitives.base import RadixPrimitiveComponent +from reflex_components_radix.themes.base import LiteralAccentColor, LiteralRadius LiteralAccordionType = Literal["single", "multiple"] LiteralAccordionDir = Literal["ltr", "rtl"] diff --git a/reflex/components/radix/primitives/base.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py similarity index 86% rename from reflex/components/radix/primitives/base.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py index 21a6815e557..57c8725cfc6 100644 --- a/reflex/components/radix/primitives/base.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py @@ -2,10 +2,10 @@ from typing import Any -from reflex.components.component import Component, field -from reflex.components.tags.tag import Tag -from reflex.utils import format -from reflex.vars.base import Var +from reflex_core.components.component import Component, field +from reflex_core.components.tags.tag import Tag +from reflex_core.utils import format +from reflex_core.vars.base import Var class RadixPrimitiveComponent(Component): @@ -49,7 +49,7 @@ def create(cls, *children: Any, **props: Any) -> Component: Returns: The new RadixPrimitiveTriggerComponent instance. """ - from reflex.components.el.elements.typography import Div + from reflex_components_core.el.elements.typography import Div for child in children: if "on_click" in getattr(child, "event_triggers", {}): diff --git a/reflex/components/radix/primitives/dialog.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/dialog.py similarity index 94% rename from reflex/components/radix/primitives/dialog.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/dialog.py index c559346ea0a..69098177118 100644 --- a/reflex/components/radix/primitives/dialog.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/dialog.py @@ -2,11 +2,11 @@ from typing import Any, ClassVar -from reflex.components.component import ComponentNamespace, field -from reflex.components.el import elements -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var +from reflex_components_core.el import elements +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.vars.base import Var from .base import RadixPrimitiveComponent, RadixPrimitiveTriggerComponent diff --git a/reflex/components/radix/primitives/drawer.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/drawer.py similarity index 95% rename from reflex/components/radix/primitives/drawer.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/drawer.py index 35a7950ab53..96564aef7d3 100644 --- a/reflex/components/radix/primitives/drawer.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/drawer.py @@ -7,13 +7,14 @@ from collections.abc import Sequence from typing import Any, Literal -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.radix.primitives.base import RadixPrimitiveComponent -from reflex.components.radix.themes.base import Theme -from reflex.components.radix.themes.layout.flex import Flex -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.primitives.base import RadixPrimitiveComponent +from reflex_components_radix.themes.base import Theme +from reflex_components_radix.themes.layout.flex import Flex class DrawerComponent(RadixPrimitiveComponent): diff --git a/reflex/components/radix/primitives/form.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/form.py similarity index 92% rename from reflex/components/radix/primitives/form.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/form.py index 3ef2bb8dc30..7433e96d1bb 100644 --- a/reflex/components/radix/primitives/form.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/form.py @@ -4,12 +4,13 @@ from typing import Any, Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.debounce import DebounceInput -from reflex.components.el.elements.forms import Form as HTMLForm -from reflex.components.radix.themes.components.text_field import TextFieldRoot -from reflex.event import EventHandler, no_args_event_spec -from reflex.vars.base import Var +from reflex_components_core.core.debounce import DebounceInput +from reflex_components_core.el.elements.forms import Form as HTMLForm +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.event import EventHandler, no_args_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.components.text_field import TextFieldRoot from .base import RadixPrimitiveComponentWithClassName diff --git a/reflex/components/radix/primitives/progress.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/progress.py similarity index 91% rename from reflex/components/radix/primitives/progress.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/progress.py index 6ad5d367bfb..263537969a2 100644 --- a/reflex/components/radix/primitives/progress.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/progress.py @@ -4,12 +4,13 @@ from typing import Any -from reflex.components.component import Component, ComponentNamespace, field -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.vars.base import Var +from reflex_components_core.core.colors import color +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.vars.base import Var + +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 class ProgressComponent(RadixPrimitiveComponentWithClassName): diff --git a/reflex/components/radix/primitives/slider.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.py similarity index 94% rename from reflex/components/radix/primitives/slider.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.py index b04b5f6bb74..187ddf3b505 100644 --- a/reflex/components/radix/primitives/slider.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.py @@ -5,10 +5,11 @@ from collections.abc import Sequence from typing import Any, Literal -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName -from reflex.event import EventHandler, passthrough_event_spec -from reflex.vars.base import Var +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.event import EventHandler, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.primitives.base import RadixPrimitiveComponentWithClassName LiteralSliderOrientation = Literal["horizontal", "vertical"] LiteralSliderDir = Literal["ltr", "rtl"] diff --git a/reflex/components/radix/themes/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/__init__.py similarity index 91% rename from reflex/components/radix/themes/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/__init__.py index 0afd5aa5d5e..c2cf4b11b88 100644 --- a/reflex/components/radix/themes/__init__.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader _SUBMODULES: set[str] = {"components", "layout", "typography"} _SUBMOD_ATTRS: dict[str, list[str]] = { diff --git a/reflex/components/radix/themes/base.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/base.py similarity index 96% rename from reflex/components/radix/themes/base.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/base.py index 22cbc82c3fa..f5a1f886fe4 100644 --- a/reflex/components/radix/themes/base.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/base.py @@ -4,12 +4,11 @@ from typing import Any, ClassVar, Literal -from reflex.components import Component -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.tags import Tag -from reflex.utils.imports import ImportDict, ImportVar -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import Component, field +from reflex_core.components.tags import Tag +from reflex_core.utils.imports import ImportDict, ImportVar +from reflex_core.vars.base import Var LiteralAlign = Literal["start", "center", "end", "baseline", "stretch"] LiteralJustify = Literal["start", "center", "end", "between"] diff --git a/reflex/components/radix/themes/color_mode.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.py similarity index 93% rename from reflex/components/radix/themes/color_mode.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.py index a444df79c5d..8d5195009e8 100644 --- a/reflex/components/radix/themes/color_mode.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.py @@ -19,20 +19,21 @@ from typing import Any, Literal, get_args -from reflex.components.component import BaseComponent, field -from reflex.components.core.cond import Cond, color_mode_cond, cond -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.components.dropdown_menu import dropdown_menu -from reflex.components.radix.themes.components.switch import Switch -from reflex.style import ( +from reflex_components_core.core.cond import Cond, color_mode_cond, cond +from reflex_components_lucide.icon import Icon +from reflex_core.components.component import BaseComponent, field +from reflex_core.style import ( LIGHT_COLOR_MODE, color_mode, resolved_color_mode, set_color_mode, toggle_color_mode, ) -from reflex.vars.base import Var -from reflex.vars.sequence import LiteralArrayVar +from reflex_core.vars.base import Var +from reflex_core.vars.sequence import LiteralArrayVar + +from reflex_components_radix.themes.components.dropdown_menu import dropdown_menu +from reflex_components_radix.themes.components.switch import Switch from .components.icon_button import IconButton diff --git a/reflex/components/radix/themes/components/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/__init__.py similarity index 58% rename from reflex/components/radix/themes/components/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/__init__.py index ffb7a580654..bbb5061ee7a 100644 --- a/reflex/components/radix/themes/components/__init__.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations -from reflex import RADIX_THEMES_COMPONENTS_MAPPING -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader + +from reflex_components_radix.mappings import RADIX_THEMES_COMPONENTS_MAPPING _SUBMOD_ATTRS: dict[str, list[str]] = { - "".join(k.split("components.radix.themes.components.")[-1]): v + "".join(k.split("reflex_components_radix.themes.components.")[-1]): v for k, v in RADIX_THEMES_COMPONENTS_MAPPING.items() } diff --git a/reflex/components/radix/themes/components/alert_dialog.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/alert_dialog.py similarity index 88% rename from reflex/components/radix/themes/components/alert_dialog.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/alert_dialog.py index 17c38511b6c..0cbfcce5913 100644 --- a/reflex/components/radix/themes/components/alert_dialog.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/alert_dialog.py @@ -2,16 +2,17 @@ from typing import Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( RadixThemesComponent, RadixThemesTriggerComponent, ) -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var LiteralContentSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/aspect_ratio.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/aspect_ratio.py similarity index 68% rename from reflex/components/radix/themes/components/aspect_ratio.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/aspect_ratio.py index d71811b6dd2..8a369eaefbb 100644 --- a/reflex/components/radix/themes/components/aspect_ratio.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/aspect_ratio.py @@ -1,8 +1,9 @@ """Interactive components provided by @radix-ui/themes.""" -from reflex.components.component import field -from reflex.components.radix.themes.base import RadixThemesComponent -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import RadixThemesComponent class AspectRatio(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/avatar.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/avatar.py similarity index 83% rename from reflex/components/radix/themes/components/avatar.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/avatar.py index 2f22e7498e0..da7fe956829 100644 --- a/reflex/components/radix/themes/components/avatar.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/avatar.py @@ -2,14 +2,15 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralSize = Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"] diff --git a/reflex/components/radix/themes/components/badge.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/badge.py similarity index 77% rename from reflex/components/radix/themes/components/badge.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/badge.py index a9f51dba97b..a8e2a5205c4 100644 --- a/reflex/components/radix/themes/components/badge.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/badge.py @@ -2,15 +2,16 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, RadixThemesComponent, ) -from reflex.vars.base import Var class Badge(elements.Span, RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/button.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/button.py similarity index 82% rename from reflex/components/radix/themes/components/button.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/button.py index 6caa4362514..79352474d24 100644 --- a/reflex/components/radix/themes/components/button.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/button.py @@ -2,17 +2,18 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, LiteralVariant, RadixLoadingProp, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralButtonSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/callout.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/callout.py similarity index 83% rename from reflex/components/radix/themes/components/callout.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/callout.py index b51f40e6bdc..0d78d2bfd86 100644 --- a/reflex/components/radix/themes/components/callout.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/callout.py @@ -2,13 +2,13 @@ from typing import Literal -import reflex as rx -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.base import fragment +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent CalloutVariant = Literal["soft", "surface", "outline"] @@ -65,11 +65,13 @@ def create(cls, text: str | Var[str], **props) -> Component: Returns: The callout component. """ + from reflex_components_lucide.icon import Icon + return CalloutRoot.create( ( CalloutIcon.create(Icon.create(tag=props["icon"])) if "icon" in props - else rx.fragment() + else fragment() ), CalloutText.create(text), **props, diff --git a/reflex/components/radix/themes/components/card.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/card.py similarity index 70% rename from reflex/components/radix/themes/components/card.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/card.py index 10234dbeb67..88feb783c31 100644 --- a/reflex/components/radix/themes/components/card.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/card.py @@ -2,11 +2,12 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import RadixThemesComponent class Card(elements.Div, RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/checkbox.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.py similarity index 91% rename from reflex/components/radix/themes/components/checkbox.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.py index d9b4dee7d28..7e93a758563 100644 --- a/reflex/components/radix/themes/components/checkbox.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.py @@ -2,17 +2,18 @@ from typing import Literal -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.event import EventHandler, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralSpacing, RadixThemesComponent, ) -from reflex.components.radix.themes.layout.flex import Flex -from reflex.components.radix.themes.typography.text import Text -from reflex.event import EventHandler, passthrough_event_spec -from reflex.vars.base import Var +from reflex_components_radix.themes.layout.flex import Flex +from reflex_components_radix.themes.typography.text import Text LiteralCheckboxSize = Literal["1", "2", "3"] LiteralCheckboxVariant = Literal["classic", "surface", "soft"] diff --git a/reflex/components/radix/themes/components/checkbox_cards.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_cards.py similarity index 86% rename from reflex/components/radix/themes/components/checkbox_cards.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_cards.py index e8bf228bb72..d67af623929 100644 --- a/reflex/components/radix/themes/components/checkbox_cards.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_cards.py @@ -3,10 +3,11 @@ from types import SimpleNamespace from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class CheckboxCardsRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/checkbox_group.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_group.py similarity index 88% rename from reflex/components/radix/themes/components/checkbox_group.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_group.py index e3146157e71..4cfb20a4823 100644 --- a/reflex/components/radix/themes/components/checkbox_group.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_group.py @@ -4,10 +4,11 @@ from types import SimpleNamespace from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class CheckboxGroupRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/context_menu.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/context_menu.py similarity index 97% rename from reflex/components/radix/themes/components/context_menu.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/context_menu.py index 0d53b4d173d..0d026073f6c 100644 --- a/reflex/components/radix/themes/components/context_menu.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/context_menu.py @@ -2,12 +2,13 @@ from typing import ClassVar, Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .checkbox import Checkbox from .radio_group import HighLevelRadioGroup diff --git a/reflex/components/radix/themes/components/data_list.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/data_list.py similarity index 89% rename from reflex/components/radix/themes/components/data_list.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/data_list.py index b8217eda3e1..e82d9be9596 100644 --- a/reflex/components/radix/themes/components/data_list.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/data_list.py @@ -3,10 +3,11 @@ from types import SimpleNamespace from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class DataListRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/dialog.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/dialog.py similarity index 85% rename from reflex/components/radix/themes/components/dialog.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/dialog.py index dfbd3cae21f..5cb49e2d63c 100644 --- a/reflex/components/radix/themes/components/dialog.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/dialog.py @@ -2,16 +2,17 @@ from typing import Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( RadixThemesComponent, RadixThemesTriggerComponent, ) -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var class DialogRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/dropdown_menu.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/dropdown_menu.py similarity index 96% rename from reflex/components/radix/themes/components/dropdown_menu.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/dropdown_menu.py index dd074e8539f..a93a4aa8006 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/dropdown_menu.py @@ -2,16 +2,17 @@ from typing import ClassVar, Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAccentColor, RadixThemesComponent, RadixThemesTriggerComponent, ) -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var LiteralDirType = Literal["ltr", "rtl"] diff --git a/reflex/components/radix/themes/components/hover_card.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/hover_card.py similarity index 88% rename from reflex/components/radix/themes/components/hover_card.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/hover_card.py index 5a779c19b53..a0ffb444e68 100644 --- a/reflex/components/radix/themes/components/hover_card.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/hover_card.py @@ -2,16 +2,17 @@ from typing import Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( RadixThemesComponent, RadixThemesTriggerComponent, ) -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, passthrough_event_spec -from reflex.vars.base import Var class HoverCardRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/icon_button.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/icon_button.py similarity index 88% rename from reflex/components/radix/themes/components/icon_button.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/icon_button.py index 002d0740312..02a7fdad726 100644 --- a/reflex/components/radix/themes/components/icon_button.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/icon_button.py @@ -4,20 +4,21 @@ from typing import Literal -from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.match import Match -from reflex.components.el import elements -from reflex.components.lucide import Icon -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.match import Match +from reflex_components_core.el import elements +from reflex_components_lucide import Icon +from reflex_core.components.component import Component, field +from reflex_core.style import Style +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, LiteralVariant, RadixLoadingProp, RadixThemesComponent, ) -from reflex.style import Style -from reflex.vars.base import Var LiteralButtonSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/inset.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/inset.py similarity index 80% rename from reflex/components/radix/themes/components/inset.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/inset.py index 8d4a45a79e4..9545d09217a 100644 --- a/reflex/components/radix/themes/components/inset.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/inset.py @@ -2,11 +2,12 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import RadixThemesComponent LiteralButtonSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/popover.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/popover.py similarity index 90% rename from reflex/components/radix/themes/components/popover.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/popover.py index 9c9f248b7cd..9ea7b9db628 100644 --- a/reflex/components/radix/themes/components/popover.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/popover.py @@ -2,16 +2,17 @@ from typing import Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( RadixThemesComponent, RadixThemesTriggerComponent, ) -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var class PopoverRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/progress.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/progress.py similarity index 89% rename from reflex/components/radix/themes/components/progress.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/progress.py index 97b0701325a..40157d3134e 100644 --- a/reflex/components/radix/themes/components/progress.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/progress.py @@ -2,11 +2,12 @@ from typing import Literal -from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.style import Style -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import Component, field +from reflex_core.style import Style +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class Progress(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/radio.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio.py similarity index 78% rename from reflex/components/radix/themes/components/radio.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio.py index 87493f758d6..f71519607bf 100644 --- a/reflex/components/radix/themes/components/radio.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio.py @@ -2,10 +2,11 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class Radio(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/radio_cards.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_cards.py similarity index 92% rename from reflex/components/radix/themes/components/radio_cards.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_cards.py index 674446b5dff..e86913cf0a7 100644 --- a/reflex/components/radix/themes/components/radio_cards.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_cards.py @@ -3,11 +3,12 @@ from types import SimpleNamespace from typing import ClassVar, Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.event import EventHandler, passthrough_event_spec -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.event import EventHandler, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class RadioCardsRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/radio_group.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_group.py similarity index 89% rename from reflex/components/radix/themes/components/radio_group.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_group.py index 32167edbad3..537cdc6d114 100644 --- a/reflex/components/radix/themes/components/radio_group.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_group.py @@ -5,20 +5,22 @@ from collections.abc import Sequence from typing import Literal -import reflex as rx -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.cond import cond +from reflex_components_core.core.foreach import foreach +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.event import EventHandler, passthrough_event_spec +from reflex_core.utils import types +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.sequence import StringVar + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralSpacing, RadixThemesComponent, ) -from reflex.components.radix.themes.layout.flex import Flex -from reflex.components.radix.themes.typography.text import Text -from reflex.event import EventHandler, passthrough_event_spec -from reflex.utils import types -from reflex.vars.base import LiteralVar, Var -from reflex.vars.sequence import StringVar +from reflex_components_radix.themes.layout.flex import Flex +from reflex_components_radix.themes.typography.text import Text LiteralFlexDirection = Literal["row", "column", "row-reverse", "column-reverse"] @@ -158,7 +160,8 @@ def create( default_value = props.pop("default_value", "") if not isinstance(items, (list, Var)) or ( - isinstance(items, Var) and not types._issubclass(items._var_type, list) + isinstance(items, Var) + and not types.typehint_issubclass(items._var_type, list) ): items_type = type(items) if not isinstance(items, Var) else items._var_type msg = f"The radio group component takes in a list, got {items_type} instead" @@ -176,7 +179,7 @@ def create( default_value = LiteralVar.create(default_value).to_string() def radio_group_item(value: Var) -> Component: - item_value = rx.cond( + item_value = cond( value.js_type() == "string", value, value.to_string(), @@ -196,7 +199,7 @@ def radio_group_item(value: Var) -> Component: ) children = [ - rx.foreach( + foreach( items, radio_group_item, ) diff --git a/reflex/components/radix/themes/components/scroll_area.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/scroll_area.py similarity index 85% rename from reflex/components/radix/themes/components/scroll_area.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/scroll_area.py index ca1b6a2d1e0..31488ee0ce4 100644 --- a/reflex/components/radix/themes/components/scroll_area.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/scroll_area.py @@ -2,9 +2,10 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.radix.themes.base import RadixThemesComponent -from reflex.vars.base import Var +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import RadixThemesComponent class ScrollArea(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/segmented_control.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py similarity index 89% rename from reflex/components/radix/themes/components/segmented_control.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py index b8cadb1b512..6e1d8049ec4 100644 --- a/reflex/components/radix/themes/components/segmented_control.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py @@ -6,11 +6,12 @@ from types import SimpleNamespace from typing import ClassVar, Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.event import EventHandler -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.event import EventHandler +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent def on_value_change( diff --git a/reflex/components/radix/themes/components/select.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/select.py similarity index 90% rename from reflex/components/radix/themes/components/select.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/select.py index c4592224e80..583433e8010 100644 --- a/reflex/components/radix/themes/components/select.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/select.py @@ -3,17 +3,18 @@ from collections.abc import Sequence from typing import ClassVar, Literal -import reflex as rx -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.foreach import foreach +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, RadixThemesComponent, ) -from reflex.constants.compiler import MemoizationMode -from reflex.event import no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var class SelectRoot(RadixThemesComponent): @@ -56,11 +57,11 @@ class SelectRoot(RadixThemesComponent): # Props to rename _rename_props = {"onChange": "onValueChange"} - on_change: rx.EventHandler[passthrough_event_spec(str)] = field( + on_change: EventHandler[passthrough_event_spec(str)] = field( doc="Fired when the value of the select changes." ) - on_open_change: rx.EventHandler[passthrough_event_spec(bool)] = field( + on_open_change: EventHandler[passthrough_event_spec(bool)] = field( doc="Fired when the select is opened or closed." ) @@ -120,15 +121,15 @@ class SelectContent(RadixThemesComponent): doc="The vertical distance in pixels from the anchor. Only available when position is set to popper." ) - on_close_auto_focus: rx.EventHandler[no_args_event_spec] = field( + on_close_auto_focus: EventHandler[no_args_event_spec] = field( doc="Fired when the select content is closed." ) - on_escape_key_down: rx.EventHandler[no_args_event_spec] = field( + on_escape_key_down: EventHandler[no_args_event_spec] = field( doc="Fired when the escape key is pressed." ) - on_pointer_down_outside: rx.EventHandler[no_args_event_spec] = field( + on_pointer_down_outside: EventHandler[no_args_event_spec] = field( doc="Fired when a pointer down event happens outside the select content." ) @@ -236,9 +237,7 @@ def create(cls, items: list[str] | Var[list[str]], **props) -> Component: label = props.pop("label", None) if isinstance(items, Var): - child = [ - rx.foreach(items, lambda item: SelectItem.create(item, value=item)) - ] + child = [foreach(items, lambda item: SelectItem.create(item, value=item))] else: child = [SelectItem.create(item, value=item) for item in items] diff --git a/reflex/components/radix/themes/components/separator.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/separator.py similarity index 80% rename from reflex/components/radix/themes/components/separator.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/separator.py index fd0d4f9fa2b..76902fe07e5 100644 --- a/reflex/components/radix/themes/components/separator.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/separator.py @@ -2,10 +2,11 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.vars.base import LiteralVar, Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.vars.base import LiteralVar, Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent LiteralSeparatorSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/skeleton.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/skeleton.py similarity index 73% rename from reflex/components/radix/themes/components/skeleton.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/skeleton.py index 4e33ec64a12..6259f3dcd13 100644 --- a/reflex/components/radix/themes/components/skeleton.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/skeleton.py @@ -1,10 +1,11 @@ """Skeleton theme from Radix components.""" -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import RadixLoadingProp, RadixThemesComponent -from reflex.constants.compiler import MemoizationMode -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import RadixLoadingProp, RadixThemesComponent class Skeleton(RadixLoadingProp, RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/slider.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/slider.py similarity index 90% rename from reflex/components/radix/themes/components/slider.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/slider.py index 04836bcdea7..80f2c4e0e5a 100644 --- a/reflex/components/radix/themes/components/slider.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/slider.py @@ -5,12 +5,13 @@ from collections.abc import Sequence from typing import Literal -from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.event import EventHandler, passthrough_event_spec -from reflex.utils.types import typehint_issubclass -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import Component, field +from reflex_core.event import EventHandler, passthrough_event_spec +from reflex_core.utils.types import typehint_issubclass +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent on_value_event_spec = ( passthrough_event_spec(list[float]), diff --git a/reflex/components/radix/themes/components/spinner.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/spinner.py similarity index 63% rename from reflex/components/radix/themes/components/spinner.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/spinner.py index 57d9a497645..8c2714ef947 100644 --- a/reflex/components/radix/themes/components/spinner.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/spinner.py @@ -2,10 +2,11 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import RadixLoadingProp, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import RadixLoadingProp, RadixThemesComponent LiteralSpinnerSize = Literal["1", "2", "3"] diff --git a/reflex/components/radix/themes/components/switch.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/switch.py similarity index 86% rename from reflex/components/radix/themes/components/switch.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/switch.py index 6c465bfbecd..293f35aabf6 100644 --- a/reflex/components/radix/themes/components/switch.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/switch.py @@ -2,11 +2,12 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.event import EventHandler, passthrough_event_spec -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.event import EventHandler, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent LiteralSwitchSize = Literal["1", "2", "3"] diff --git a/reflex/components/radix/themes/components/table.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/table.py similarity index 92% rename from reflex/components/radix/themes/components/table.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/table.py index 13d92d27848..8773f85bc2d 100644 --- a/reflex/components/radix/themes/components/table.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/table.py @@ -2,11 +2,12 @@ from typing import ClassVar, Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import CommonPaddingProps, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import CommonPaddingProps, RadixThemesComponent class TableRoot(elements.Table, RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/tabs.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/tabs.py similarity index 91% rename from reflex/components/radix/themes/components/tabs.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/tabs.py index eb5c136cd71..3c9dd20c641 100644 --- a/reflex/components/radix/themes/components/tabs.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/tabs.py @@ -4,13 +4,14 @@ from typing import Any, ClassVar, Literal -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.colors import color -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, passthrough_event_spec -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.colors import color +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, passthrough_event_spec +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent vertical_orientation_css = "&[data-orientation='vertical']" diff --git a/reflex/components/radix/themes/components/text_area.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.py similarity index 91% rename from reflex/components/radix/themes/components/text_area.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.py index e76dc01be9e..db463668949 100644 --- a/reflex/components/radix/themes/components/text_area.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.py @@ -2,16 +2,17 @@ from typing import Literal -from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.debounce import DebounceInput -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.debounce import DebounceInput +from reflex_components_core.el import elements +from reflex_core.components.component import Component, field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralTextAreaSize = Literal["1", "2", "3"] diff --git a/reflex/components/radix/themes/components/text_field.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_field.py similarity index 89% rename from reflex/components/radix/themes/components/text_field.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_field.py index 8e8d581c36b..5747ed80717 100644 --- a/reflex/components/radix/themes/components/text_field.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_field.py @@ -4,19 +4,20 @@ from typing import Literal -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.debounce import DebounceInput -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.debounce import DebounceInput +from reflex_components_core.el import elements +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.event import EventHandler, input_event, key_event +from reflex_core.utils.types import is_optional +from reflex_core.vars.base import Var +from reflex_core.vars.number import ternary_operation + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, RadixThemesComponent, ) -from reflex.event import EventHandler, input_event, key_event -from reflex.utils.types import is_optional -from reflex.vars.base import Var -from reflex.vars.number import ternary_operation LiteralTextFieldSize = Literal["1", "2", "3"] LiteralTextFieldVariant = Literal["classic", "surface", "soft"] diff --git a/reflex/components/radix/themes/components/tooltip.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/tooltip.py similarity index 92% rename from reflex/components/radix/themes/components/tooltip.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/tooltip.py index 9915f7a5c72..051985dbc19 100644 --- a/reflex/components/radix/themes/components/tooltip.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/tooltip.py @@ -2,12 +2,13 @@ from typing import Literal -from reflex.components.component import Component, field -from reflex.components.radix.themes.base import RadixThemesComponent -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.utils import format -from reflex.vars.base import Var +from reflex_core.components.component import Component, field +from reflex_core.constants.compiler import MemoizationMode +from reflex_core.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex_core.utils import format +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import RadixThemesComponent LiteralSideType = Literal[ "top", diff --git a/reflex/components/radix/themes/layout/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/__init__.py similarity index 58% rename from reflex/components/radix/themes/layout/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/__init__.py index b141146bd16..b2b6a0a02d0 100644 --- a/reflex/components/radix/themes/layout/__init__.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations -from reflex import RADIX_THEMES_LAYOUT_MAPPING -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader + +from reflex_components_radix.mappings import RADIX_THEMES_LAYOUT_MAPPING _SUBMOD_ATTRS: dict[str, list[str]] = { - "".join(k.split("components.radix.themes.layout.")[-1]): v + "".join(k.split("reflex_components_radix.themes.layout.")[-1]): v for k, v in RADIX_THEMES_LAYOUT_MAPPING.items() } diff --git a/reflex/components/radix/themes/layout/base.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/base.py similarity index 80% rename from reflex/components/radix/themes/layout/base.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/base.py index b7f512ea6e2..d1dc3277ffc 100644 --- a/reflex/components/radix/themes/layout/base.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/base.py @@ -4,14 +4,15 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( CommonMarginProps, CommonPaddingProps, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralBoolNumber = Literal["0", "1"] diff --git a/reflex/components/radix/themes/layout/box.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/box.py similarity index 68% rename from reflex/components/radix/themes/layout/box.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/box.py index 3932d2b9e1a..16d8d0fbf33 100644 --- a/reflex/components/radix/themes/layout/box.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/box.py @@ -2,8 +2,9 @@ from __future__ import annotations -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent +from reflex_components_core.el import elements + +from reflex_components_radix.themes.base import RadixThemesComponent class Box(elements.Div, RadixThemesComponent): diff --git a/reflex/components/radix/themes/layout/center.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/center.py similarity index 100% rename from reflex/components/radix/themes/layout/center.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/center.py diff --git a/reflex/components/radix/themes/layout/container.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/container.py similarity index 80% rename from reflex/components/radix/themes/layout/container.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/container.py index e44b583d2c9..c38e893c20e 100644 --- a/reflex/components/radix/themes/layout/container.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/container.py @@ -4,12 +4,13 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent -from reflex.style import STACK_CHILDREN_FULL_WIDTH -from reflex.vars.base import LiteralVar, Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.style import STACK_CHILDREN_FULL_WIDTH +from reflex_core.vars.base import LiteralVar, Var + +from reflex_components_radix.themes.base import RadixThemesComponent LiteralContainerSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/layout/flex.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/flex.py similarity index 86% rename from reflex/components/radix/themes/layout/flex.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/flex.py index 80101d27e47..1f6b45e393c 100644 --- a/reflex/components/radix/themes/layout/flex.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/flex.py @@ -4,16 +4,17 @@ from typing import ClassVar, Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAlign, LiteralJustify, LiteralSpacing, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralFlexDirection = Literal["row", "column", "row-reverse", "column-reverse"] LiteralFlexWrap = Literal["nowrap", "wrap", "wrap-reverse"] diff --git a/reflex/components/radix/themes/layout/grid.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/grid.py similarity index 87% rename from reflex/components/radix/themes/layout/grid.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/grid.py index 186fa3c11a7..2b9388f4bb3 100644 --- a/reflex/components/radix/themes/layout/grid.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/grid.py @@ -4,16 +4,17 @@ from typing import ClassVar, Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAlign, LiteralJustify, LiteralSpacing, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralGridFlow = Literal["row", "column", "dense", "row-dense", "column-dense"] diff --git a/reflex/components/radix/themes/layout/list.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/list.py similarity index 83% rename from reflex/components/radix/themes/layout/list.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/list.py index b39a56b2e0d..8c44dfe11a6 100644 --- a/reflex/components/radix/themes/layout/list.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/list.py @@ -2,17 +2,17 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Iterable, MutableSequence from typing import Any, Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.foreach import Foreach -from reflex.components.el.elements.base import BaseHTML -from reflex.components.el.elements.typography import Li, Ol, Ul -from reflex.components.lucide.icon import Icon -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.typography.text import Text -from reflex.vars.base import Var +from reflex_components_core.core.foreach import Foreach +from reflex_components_core.core.markdown_component_map import MarkdownComponentMap +from reflex_components_core.el.elements.base import BaseHTML +from reflex_components_core.el.elements.typography import Li, Ol, Ul +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.typography.text import Text LiteralListStyleTypeUnordered = Literal[ "none", @@ -94,7 +94,7 @@ def add_style(self) -> dict[str, Any] | None: "direction": "column", } - def _exclude_props(self) -> list[str]: + def _exclude_props(self) -> MutableSequence[str]: return ["items", "list_style_type"] @@ -170,6 +170,8 @@ def create(cls, *children, **props): Returns: The list item component. """ + from reflex_components_lucide.icon import Icon + for child in children: if isinstance(child, Text): child.as_ = "span" # pyright: ignore[reportAttributeAccessIssue] @@ -187,19 +189,7 @@ class List(ComponentNamespace): __call__ = staticmethod(BaseList.create) -list_ns = List() +list = list_ns = List() list_item = list_ns.item ordered_list = list_ns.ordered unordered_list = list_ns.unordered - - -def __getattr__(name: Any): - # special case for when accessing list to avoid shadowing - # python's built in list object. - if name == "list": - return list_ns - try: - return globals()[name] - except KeyError: - msg = f"module '{__name__} has no attribute '{name}'" - raise AttributeError(msg) from None diff --git a/reflex/components/radix/themes/layout/section.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/section.py similarity index 63% rename from reflex/components/radix/themes/layout/section.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/section.py index 88fe24cbad9..22334616059 100644 --- a/reflex/components/radix/themes/layout/section.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/section.py @@ -4,11 +4,12 @@ from typing import Literal -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent -from reflex.vars.base import LiteralVar, Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import LiteralVar, Var + +from reflex_components_radix.themes.base import RadixThemesComponent LiteralSectionSize = Literal["1", "2", "3"] diff --git a/reflex/components/radix/themes/layout/spacer.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/spacer.py similarity index 100% rename from reflex/components/radix/themes/layout/spacer.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/spacer.py diff --git a/reflex/components/radix/themes/layout/stack.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/stack.py similarity index 87% rename from reflex/components/radix/themes/layout/stack.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/stack.py index 98f9c2f9170..b77863ea1d2 100644 --- a/reflex/components/radix/themes/layout/stack.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/stack.py @@ -2,10 +2,11 @@ from __future__ import annotations -from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAlign, LiteralSpacing -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_core.components.component import Component, field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAlign, LiteralSpacing from .flex import Flex, LiteralFlexDirection diff --git a/reflex/components/radix/themes/typography/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/__init__.py similarity index 57% rename from reflex/components/radix/themes/typography/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/__init__.py index 0ea695ac5ee..87330b5e825 100644 --- a/reflex/components/radix/themes/typography/__init__.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations -from reflex import RADIX_THEMES_TYPOGRAPHY_MAPPING -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader + +from reflex_components_radix.mappings import RADIX_THEMES_TYPOGRAPHY_MAPPING _SUBMOD_ATTRS: dict[str, list[str]] = { - "".join(k.split("components.radix.themes.typography.")[-1]): v + "".join(k.split("reflex_components_radix.themes.typography.")[-1]): v for k, v in RADIX_THEMES_TYPOGRAPHY_MAPPING.items() } diff --git a/reflex/components/radix/themes/typography/base.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/base.py similarity index 100% rename from reflex/components/radix/themes/typography/base.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/base.py diff --git a/reflex/components/radix/themes/typography/blockquote.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/blockquote.py similarity index 75% rename from reflex/components/radix/themes/typography/blockquote.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/blockquote.py index 836b8286bd7..7a608b6ff9e 100644 --- a/reflex/components/radix/themes/typography/blockquote.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/blockquote.py @@ -5,11 +5,12 @@ from __future__ import annotations -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .base import LiteralTextSize, LiteralTextWeight diff --git a/reflex/components/radix/themes/typography/code.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/code.py similarity index 74% rename from reflex/components/radix/themes/typography/code.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/code.py index ade8c441d1e..4efff1d54bb 100644 --- a/reflex/components/radix/themes/typography/code.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/code.py @@ -5,16 +5,17 @@ from __future__ import annotations -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.base import ( +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.markdown_component_map import MarkdownComponentMap +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralVariant, RadixThemesComponent, ) -from reflex.vars.base import Var from .base import LiteralTextSize, LiteralTextWeight diff --git a/reflex/components/radix/themes/typography/heading.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/heading.py similarity index 80% rename from reflex/components/radix/themes/typography/heading.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/heading.py index d970ad15ad8..f2ec063d124 100644 --- a/reflex/components/radix/themes/typography/heading.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/heading.py @@ -5,12 +5,13 @@ from __future__ import annotations -from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.markdown_component_map import MarkdownComponentMap +from reflex_components_core.el import elements +from reflex_core.components.component import field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .base import LiteralTextAlign, LiteralTextSize, LiteralTextTrim, LiteralTextWeight diff --git a/reflex/components/radix/themes/typography/link.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/link.py similarity index 85% rename from reflex/components/radix/themes/typography/link.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/link.py index 1e20f36942f..0317148246e 100644 --- a/reflex/components/radix/themes/typography/link.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/link.py @@ -7,16 +7,17 @@ from typing import Literal -from reflex.components.component import Component, MemoizationLeaf, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.colors import color -from reflex.components.core.cond import cond -from reflex.components.el.elements.inline import A -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.components.react_router.dom import ReactRouterLink -from reflex.utils.imports import ImportDict, ImportVar -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.colors import color +from reflex_components_core.core.cond import cond +from reflex_components_core.core.markdown_component_map import MarkdownComponentMap +from reflex_components_core.el.elements.inline import A +from reflex_components_core.react_router.dom import ReactRouterLink +from reflex_core.components.component import Component, MemoizationLeaf, field +from reflex_core.utils.imports import ImportDict, ImportVar +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .base import LiteralTextSize, LiteralTextTrim, LiteralTextWeight diff --git a/reflex/components/radix/themes/typography/text.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/text.py similarity index 88% rename from reflex/components/radix/themes/typography/text.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/text.py index 1c90bd83a32..c0adbd04b77 100644 --- a/reflex/components/radix/themes/typography/text.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/text.py @@ -7,12 +7,13 @@ from typing import Literal -from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.vars.base import Var +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.markdown_component_map import MarkdownComponentMap +from reflex_components_core.el import elements +from reflex_core.components.component import ComponentNamespace, field +from reflex_core.vars.base import Var + +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .base import LiteralTextAlign, LiteralTextSize, LiteralTextTrim, LiteralTextWeight diff --git a/packages/reflex-components-react-player/README.md b/packages/reflex-components-react-player/README.md new file mode 100644 index 00000000000..b1570fb70f5 --- /dev/null +++ b/packages/reflex-components-react-player/README.md @@ -0,0 +1,3 @@ +# reflex-components-react-player + +Reflex react-player components. diff --git a/packages/reflex-components-react-player/pyproject.toml b/packages/reflex-components-react-player/pyproject.toml new file mode 100644 index 00000000000..b8518aff675 --- /dev/null +++ b/packages/reflex-components-react-player/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "reflex-components-react-player" +dynamic = ["version"] +description = "Reflex react-player components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = ["reflex-components-core"] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-react-player-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = [ + "ruff", + "reflex-core", + "reflex-components-core", + "reflex-components-lucide", + "reflex-components-sonner", +] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/reflex/components/react_player/__init__.py b/packages/reflex-components-react-player/src/reflex_components_react_player/__init__.py similarity index 100% rename from reflex/components/react_player/__init__.py rename to packages/reflex-components-react-player/src/reflex_components_react_player/__init__.py diff --git a/reflex/components/react_player/audio.py b/packages/reflex-components-react-player/src/reflex_components_react_player/audio.py similarity index 63% rename from reflex/components/react_player/audio.py rename to packages/reflex-components-react-player/src/reflex_components_react_player/audio.py index 2f5cc5b6d8d..2580fa26944 100644 --- a/reflex/components/react_player/audio.py +++ b/packages/reflex-components-react-player/src/reflex_components_react_player/audio.py @@ -1,6 +1,6 @@ """A audio component.""" -from reflex.components.react_player.react_player import ReactPlayer +from reflex_components_react_player.react_player import ReactPlayer class Audio(ReactPlayer): diff --git a/reflex/components/react_player/react_player.py b/packages/reflex-components-react-player/src/reflex_components_react_player/react_player.py similarity index 96% rename from reflex/components/react_player/react_player.py rename to packages/reflex-components-react-player/src/reflex_components_react_player/react_player.py index f75779a322a..c117be99a3f 100644 --- a/reflex/components/react_player/react_player.py +++ b/packages/reflex-components-react-player/src/reflex_components_react_player/react_player.py @@ -4,12 +4,12 @@ from typing import Any, TypedDict -from reflex.components.component import Component, field -from reflex.components.core.cond import cond -from reflex.event import EventHandler, no_args_event_spec -from reflex.utils import console -from reflex.vars.base import Var -from reflex.vars.object import ObjectVar +from reflex_components_core.core.cond import cond +from reflex_core.components.component import Component, field +from reflex_core.event import EventHandler, no_args_event_spec +from reflex_core.utils import console +from reflex_core.vars.base import Var +from reflex_core.vars.object import ObjectVar ReactPlayerEvent = ObjectVar[dict[str, dict[str, dict[str, Any]]]] diff --git a/reflex/components/react_player/video.py b/packages/reflex-components-react-player/src/reflex_components_react_player/video.py similarity index 63% rename from reflex/components/react_player/video.py rename to packages/reflex-components-react-player/src/reflex_components_react_player/video.py index 70b513195e2..df0dba45502 100644 --- a/reflex/components/react_player/video.py +++ b/packages/reflex-components-react-player/src/reflex_components_react_player/video.py @@ -1,6 +1,6 @@ """A video component.""" -from reflex.components.react_player.react_player import ReactPlayer +from reflex_components_react_player.react_player import ReactPlayer class Video(ReactPlayer): diff --git a/packages/reflex-components-recharts/README.md b/packages/reflex-components-recharts/README.md new file mode 100644 index 00000000000..f5e6de0e026 --- /dev/null +++ b/packages/reflex-components-recharts/README.md @@ -0,0 +1,3 @@ +# reflex-components-recharts + +Reflex recharts components. diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml new file mode 100644 index 00000000000..20c868bf7d9 --- /dev/null +++ b/packages/reflex-components-recharts/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "reflex-components-recharts" +dynamic = ["version"] +description = "Reflex recharts components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-recharts-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = ["ruff", "reflex-core"] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/reflex/components/recharts/__init__.py b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py similarity index 98% rename from reflex/components/recharts/__init__.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py index 6b4b358095b..a2a654006e4 100644 --- a/reflex/components/recharts/__init__.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader _SUBMOD_ATTRS: dict = { "cartesian": [ diff --git a/reflex/components/recharts/cartesian.py b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py similarity index 99% rename from reflex/components/recharts/cartesian.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py index 314d6b91ce6..c75e0a5cc43 100644 --- a/reflex/components/recharts/cartesian.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py @@ -5,11 +5,11 @@ from collections.abc import Sequence from typing import Any, ClassVar, TypedDict -from reflex.components.component import field -from reflex.constants import EventTriggers -from reflex.constants.colors import Color -from reflex.event import EventHandler, no_args_event_spec -from reflex.vars.base import LiteralVar, Var +from reflex_core.components.component import field +from reflex_core.constants import EventTriggers +from reflex_core.constants.colors import Color +from reflex_core.event import EventHandler, no_args_event_spec +from reflex_core.vars.base import LiteralVar, Var from .recharts import ( ACTIVE_DOT_TYPE, diff --git a/reflex/components/recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py similarity index 98% rename from reflex/components/recharts/charts.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index 5a62c4d6dce..6df3ecccec3 100644 --- a/reflex/components/recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -5,12 +5,13 @@ from collections.abc import Sequence from typing import Any, ClassVar -from reflex.components.component import Component, field -from reflex.components.recharts.general import ResponsiveContainer -from reflex.constants import EventTriggers -from reflex.constants.colors import Color -from reflex.event import EventHandler, no_args_event_spec -from reflex.vars.base import Var +from reflex_core.components.component import Component, field +from reflex_core.constants import EventTriggers +from reflex_core.constants.colors import Color +from reflex_core.event import EventHandler, no_args_event_spec +from reflex_core.vars.base import Var + +from reflex_components_recharts.general import ResponsiveContainer from .recharts import ( LiteralAnimationEasing, diff --git a/reflex/components/recharts/general.py b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py similarity index 97% rename from reflex/components/recharts/general.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/general.py index aeb7fa98ecd..ab5ec1c1196 100644 --- a/reflex/components/recharts/general.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py @@ -5,10 +5,10 @@ from collections.abc import Sequence from typing import Any, ClassVar -from reflex.components.component import MemoizationLeaf, field -from reflex.constants.colors import Color -from reflex.event import EventHandler, no_args_event_spec -from reflex.vars.base import LiteralVar, Var +from reflex_core.components.component import MemoizationLeaf, field +from reflex_core.constants.colors import Color +from reflex_core.event import EventHandler, no_args_event_spec +from reflex_core.vars.base import LiteralVar, Var from .recharts import ( LiteralAnimationEasing, diff --git a/reflex/components/recharts/polar.py b/packages/reflex-components-recharts/src/reflex_components_recharts/polar.py similarity index 98% rename from reflex/components/recharts/polar.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/polar.py index bcbd2d87a8d..bf5e871163c 100644 --- a/reflex/components/recharts/polar.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/polar.py @@ -5,11 +5,11 @@ from collections.abc import Sequence from typing import Any, ClassVar -from reflex.components.component import field -from reflex.constants import EventTriggers -from reflex.constants.colors import Color -from reflex.event import EventHandler, no_args_event_spec -from reflex.vars.base import LiteralVar, Var +from reflex_core.components.component import field +from reflex_core.constants import EventTriggers +from reflex_core.constants.colors import Color +from reflex_core.event import EventHandler, no_args_event_spec +from reflex_core.vars.base import LiteralVar, Var from .recharts import ( ACTIVE_DOT_TYPE, diff --git a/reflex/components/recharts/recharts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py similarity index 97% rename from reflex/components/recharts/recharts.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py index e1a4dfa8abf..02ed370f8c2 100644 --- a/reflex/components/recharts/recharts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py @@ -2,7 +2,7 @@ from typing import Any, Literal -from reflex.components.component import Component, MemoizationLeaf, NoSSRComponent +from reflex_core.components.component import Component, MemoizationLeaf, NoSSRComponent class Recharts(Component): diff --git a/packages/reflex-components-sonner/README.md b/packages/reflex-components-sonner/README.md new file mode 100644 index 00000000000..cfc4e39ac49 --- /dev/null +++ b/packages/reflex-components-sonner/README.md @@ -0,0 +1,3 @@ +# reflex-components-sonner + +Reflex sonner components. diff --git a/packages/reflex-components-sonner/pyproject.toml b/packages/reflex-components-sonner/pyproject.toml new file mode 100644 index 00000000000..37099de6214 --- /dev/null +++ b/packages/reflex-components-sonner/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "reflex-components-sonner" +dynamic = ["version"] +description = "Reflex sonner components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [ + "reflex-components-lucide", +] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-sonner-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +targets.sdist.artifacts = ["*.pyi"] +targets.wheel.artifacts = ["*.pyi"] + +[tool.hatch.build.hooks.reflex-pyi] +dependencies = ["ruff", "reflex-core", "reflex-components-lucide"] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] +build-backend = "hatchling.build" diff --git a/reflex/components/sonner/__init__.py b/packages/reflex-components-sonner/src/reflex_components_sonner/__init__.py similarity index 100% rename from reflex/components/sonner/__init__.py rename to packages/reflex-components-sonner/src/reflex_components_sonner/__init__.py diff --git a/reflex/components/sonner/toast.py b/packages/reflex-components-sonner/src/reflex_components_sonner/toast.py similarity index 94% rename from reflex/components/sonner/toast.py rename to packages/reflex-components-sonner/src/reflex_components_sonner/toast.py index 330a24478c5..f967c93b8e8 100644 --- a/reflex/components/sonner/toast.py +++ b/packages/reflex-components-sonner/src/reflex_components_sonner/toast.py @@ -5,20 +5,20 @@ import dataclasses from typing import Any, Literal -from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.lucide.icon import Icon -from reflex.components.props import NoExtrasAllowedProps -from reflex.constants.base import Dirs -from reflex.event import EventSpec, run_script -from reflex.style import Style, resolved_color_mode -from reflex.utils import format -from reflex.utils.imports import ImportVar -from reflex.utils.serializers import serializer -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var -from reflex.vars.function import FunctionVar -from reflex.vars.number import ternary_operation -from reflex.vars.object import ObjectVar +from reflex_components_lucide.icon import Icon +from reflex_core.components.component import Component, ComponentNamespace, field +from reflex_core.components.props import NoExtrasAllowedProps +from reflex_core.constants.base import Dirs +from reflex_core.event import EventSpec, run_script +from reflex_core.style import Style, resolved_color_mode +from reflex_core.utils import format +from reflex_core.utils.imports import ImportVar +from reflex_core.utils.serializers import serializer +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.function import FunctionVar +from reflex_core.vars.number import ternary_operation +from reflex_core.vars.object import ObjectVar LiteralPosition = Literal[ "top-left", diff --git a/packages/reflex-core/README.md b/packages/reflex-core/README.md new file mode 100644 index 00000000000..77f3c868ca6 --- /dev/null +++ b/packages/reflex-core/README.md @@ -0,0 +1,3 @@ +# reflex-core + +Core types for the Reflex framework. diff --git a/packages/reflex-core/pyproject.toml b/packages/reflex-core/pyproject.toml new file mode 100644 index 00000000000..ea8c326ff8d --- /dev/null +++ b/packages/reflex-core/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "reflex-core" +dynamic = ["version"] +description = "Core types for the Reflex framework." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [ + "packaging >=24.2,<27", + "pydantic >=1.10.21,<3.0", + "rich >=13,<15", + "typing_extensions >=4.13.0", + "platformdirs >=4.3.7,<5.0", +] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-core-" +fallback-version = "0.0.0dev0" + +[tool.hatch.build] +artifacts = [".templates/**"] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/.templates/apps/blank/assets/favicon.ico b/packages/reflex-core/src/reflex_core/.templates/apps/blank/assets/favicon.ico similarity index 100% rename from reflex/.templates/apps/blank/assets/favicon.ico rename to packages/reflex-core/src/reflex_core/.templates/apps/blank/assets/favicon.ico diff --git a/packages/reflex-core/src/reflex_core/.templates/apps/blank/code/__init__.py b/packages/reflex-core/src/reflex_core/.templates/apps/blank/code/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/reflex/.templates/apps/blank/code/blank.py b/packages/reflex-core/src/reflex_core/.templates/apps/blank/code/blank.py similarity index 100% rename from reflex/.templates/apps/blank/code/blank.py rename to packages/reflex-core/src/reflex_core/.templates/apps/blank/code/blank.py diff --git a/reflex/.templates/web/.gitignore b/packages/reflex-core/src/reflex_core/.templates/web/.gitignore similarity index 100% rename from reflex/.templates/web/.gitignore rename to packages/reflex-core/src/reflex_core/.templates/web/.gitignore diff --git a/reflex/.templates/web/app/entry.client.js b/packages/reflex-core/src/reflex_core/.templates/web/app/entry.client.js similarity index 100% rename from reflex/.templates/web/app/entry.client.js rename to packages/reflex-core/src/reflex_core/.templates/web/app/entry.client.js diff --git a/reflex/.templates/web/app/routes.js b/packages/reflex-core/src/reflex_core/.templates/web/app/routes.js similarity index 100% rename from reflex/.templates/web/app/routes.js rename to packages/reflex-core/src/reflex_core/.templates/web/app/routes.js diff --git a/reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js b/packages/reflex-core/src/reflex_core/.templates/web/components/reflex/radix_themes_color_mode_provider.js similarity index 100% rename from reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js rename to packages/reflex-core/src/reflex_core/.templates/web/components/reflex/radix_themes_color_mode_provider.js diff --git a/reflex/.templates/web/components/shiki/code.js b/packages/reflex-core/src/reflex_core/.templates/web/components/shiki/code.js similarity index 100% rename from reflex/.templates/web/components/shiki/code.js rename to packages/reflex-core/src/reflex_core/.templates/web/components/shiki/code.js diff --git a/reflex/.templates/web/jsconfig.json b/packages/reflex-core/src/reflex_core/.templates/web/jsconfig.json similarity index 100% rename from reflex/.templates/web/jsconfig.json rename to packages/reflex-core/src/reflex_core/.templates/web/jsconfig.json diff --git a/reflex/.templates/web/postcss.config.js b/packages/reflex-core/src/reflex_core/.templates/web/postcss.config.js similarity index 100% rename from reflex/.templates/web/postcss.config.js rename to packages/reflex-core/src/reflex_core/.templates/web/postcss.config.js diff --git a/reflex/.templates/web/react-router.config.js b/packages/reflex-core/src/reflex_core/.templates/web/react-router.config.js similarity index 100% rename from reflex/.templates/web/react-router.config.js rename to packages/reflex-core/src/reflex_core/.templates/web/react-router.config.js diff --git a/reflex/.templates/web/styles/__reflex_style_reset.css b/packages/reflex-core/src/reflex_core/.templates/web/styles/__reflex_style_reset.css similarity index 100% rename from reflex/.templates/web/styles/__reflex_style_reset.css rename to packages/reflex-core/src/reflex_core/.templates/web/styles/__reflex_style_reset.css diff --git a/reflex/.templates/web/utils/helpers/dataeditor.js b/packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/dataeditor.js similarity index 100% rename from reflex/.templates/web/utils/helpers/dataeditor.js rename to packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/dataeditor.js diff --git a/reflex/.templates/web/utils/helpers/debounce.js b/packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/debounce.js similarity index 100% rename from reflex/.templates/web/utils/helpers/debounce.js rename to packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/debounce.js diff --git a/reflex/.templates/web/utils/helpers/paste.js b/packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/paste.js similarity index 100% rename from reflex/.templates/web/utils/helpers/paste.js rename to packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/paste.js diff --git a/reflex/.templates/web/utils/helpers/range.js b/packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/range.js similarity index 100% rename from reflex/.templates/web/utils/helpers/range.js rename to packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/range.js diff --git a/reflex/.templates/web/utils/helpers/throttle.js b/packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/throttle.js similarity index 100% rename from reflex/.templates/web/utils/helpers/throttle.js rename to packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/throttle.js diff --git a/reflex/.templates/web/utils/helpers/upload.js b/packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/upload.js similarity index 100% rename from reflex/.templates/web/utils/helpers/upload.js rename to packages/reflex-core/src/reflex_core/.templates/web/utils/helpers/upload.js diff --git a/reflex/.templates/web/utils/react-theme.js b/packages/reflex-core/src/reflex_core/.templates/web/utils/react-theme.js similarity index 100% rename from reflex/.templates/web/utils/react-theme.js rename to packages/reflex-core/src/reflex_core/.templates/web/utils/react-theme.js diff --git a/reflex/.templates/web/utils/state.js b/packages/reflex-core/src/reflex_core/.templates/web/utils/state.js similarity index 100% rename from reflex/.templates/web/utils/state.js rename to packages/reflex-core/src/reflex_core/.templates/web/utils/state.js diff --git a/reflex/.templates/web/vite-plugin-safari-cachebust.js b/packages/reflex-core/src/reflex_core/.templates/web/vite-plugin-safari-cachebust.js similarity index 100% rename from reflex/.templates/web/vite-plugin-safari-cachebust.js rename to packages/reflex-core/src/reflex_core/.templates/web/vite-plugin-safari-cachebust.js diff --git a/packages/reflex-core/src/reflex_core/__init__.py b/packages/reflex-core/src/reflex_core/__init__.py new file mode 100644 index 00000000000..4311653df9d --- /dev/null +++ b/packages/reflex-core/src/reflex_core/__init__.py @@ -0,0 +1 @@ +"""Reflex core types.""" diff --git a/reflex/components/core/breakpoints.py b/packages/reflex-core/src/reflex_core/breakpoints.py similarity index 100% rename from reflex/components/core/breakpoints.py rename to packages/reflex-core/src/reflex_core/breakpoints.py diff --git a/packages/reflex-core/src/reflex_core/compiler/__init__.py b/packages/reflex-core/src/reflex_core/compiler/__init__.py new file mode 100644 index 00000000000..0002dd54831 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/compiler/__init__.py @@ -0,0 +1 @@ +"""Reflex core compiler.""" diff --git a/packages/reflex-core/src/reflex_core/compiler/templates.py b/packages/reflex-core/src/reflex_core/compiler/templates.py new file mode 100644 index 00000000000..db988bcc9ee --- /dev/null +++ b/packages/reflex-core/src/reflex_core/compiler/templates.py @@ -0,0 +1,741 @@ +"""Templates to use in the reflex compiler.""" + +from __future__ import annotations + +import json +from collections.abc import Iterable, Mapping +from typing import TYPE_CHECKING, Any, Literal + +from reflex_core import constants +from reflex_core.constants import Hooks +from reflex_core.utils.format import format_state_name, json_dumps +from reflex_core.vars.base import VarData + +if TYPE_CHECKING: + from reflex.compiler.utils import _ImportDict + from reflex_core.components.component import Component, StatefulComponent + + +def _sort_hooks( + hooks: dict[str, VarData | None], +) -> tuple[list[str], list[str], list[str]]: + """Sort the hooks by their position. + + Args: + hooks: The hooks to sort. + + Returns: + The sorted hooks. + """ + internal_hooks = [] + pre_trigger_hooks = [] + post_trigger_hooks = [] + + for hook, data in hooks.items(): + if data and data.position and data.position == Hooks.HookPosition.INTERNAL: + internal_hooks.append(hook) + elif not data or ( + not data.position + or data.position == constants.Hooks.HookPosition.PRE_TRIGGER + ): + pre_trigger_hooks.append(hook) + elif ( + data + and data.position + and data.position == constants.Hooks.HookPosition.POST_TRIGGER + ): + post_trigger_hooks.append(hook) + + return internal_hooks, pre_trigger_hooks, post_trigger_hooks + + +class _RenderUtils: + @staticmethod + def render(component: Mapping[str, Any] | str) -> str: + if isinstance(component, str): + return component or "null" + if "iterable" in component: + return _RenderUtils.render_iterable_tag(component) + if "match_cases" in component: + return _RenderUtils.render_match_tag(component) + if "cond_state" in component: + return _RenderUtils.render_condition_tag(component) + if (contents := component.get("contents")) is not None: + return contents or "null" + return _RenderUtils.render_tag(component) + + @staticmethod + def render_tag(component: Mapping[str, Any]) -> str: + name = component.get("name") or "Fragment" + props = f"{{{','.join(component['props'])}}}" + rendered_children = [ + _RenderUtils.render(child) + for child in component.get("children", []) + if child + ] + + return f"jsx({name},{props},{','.join(rendered_children)})" + + @staticmethod + def render_condition_tag(component: Any) -> str: + return f"({component['cond_state']}?({_RenderUtils.render(component['true_value'])}):({_RenderUtils.render(component['false_value'])}))" + + @staticmethod + def render_iterable_tag(component: Any) -> str: + children_rendered = "".join([ + _RenderUtils.render(child) for child in component.get("children", []) + ]) + return f"Array.prototype.map.call({component['iterable_state']} ?? [],(({component['arg_name']},{component['arg_index']})=>({children_rendered})))" + + @staticmethod + def render_match_tag(component: Any) -> str: + cases_code = "" + for conditions, return_value in component["match_cases"]: + for condition in conditions: + cases_code += f" case JSON.stringify({condition}):\n" + cases_code += f""" return {_RenderUtils.render(return_value)}; + break; +""" + + return f"""(() => {{ + switch (JSON.stringify({component["cond"]})) {{ +{cases_code} default: + return {_RenderUtils.render(component["default"])}; + break; + }} +}})()""" + + @staticmethod + def get_import(module: _ImportDict) -> str: + default_import = module["default"] + rest_imports = module["rest"] + + if default_import and rest_imports: + rest_imports_str = ",".join(sorted(rest_imports)) + return f'import {default_import}, {{{rest_imports_str}}} from "{module["lib"]}"' + if default_import: + return f'import {default_import} from "{module["lib"]}"' + if rest_imports: + rest_imports_str = ",".join(sorted(rest_imports)) + return f'import {{{rest_imports_str}}} from "{module["lib"]}"' + return f'import "{module["lib"]}"' + + +def rxconfig_template(app_name: str): + """Template for the Reflex config file. + + Args: + app_name: The name of the application. + + Returns: + Rendered Reflex config file content as string. + """ + return f"""import reflex as rx + +config = rx.Config( + app_name="{app_name}", + plugins=[ + rx.plugins.SitemapPlugin(), + rx.plugins.TailwindV4Plugin(), + ] +)""" + + +def document_root_template(*, imports: list[_ImportDict], document: dict[str, Any]): + """Template for the document root. + + Args: + imports: List of import statements. + document: Document root component. + + Returns: + Rendered document root code as string. + """ + imports_rendered = "\n".join([_RenderUtils.get_import(mod) for mod in imports]) + return f"""{imports_rendered} + +export function Layout({{children}}) {{ + return ( + {_RenderUtils.render(document)} + ) +}}""" + + +def app_root_template( + *, + imports: list[_ImportDict], + custom_codes: Iterable[str], + hooks: dict[str, VarData | None], + window_libraries: list[tuple[str, str]], + render: dict[str, Any], + dynamic_imports: set[str], +): + """Template for the App root. + + Args: + imports: The list of import statements. + custom_codes: The set of custom code snippets. + hooks: The dictionary of hooks. + window_libraries: The list of window libraries. + render: The dictionary of render functions. + dynamic_imports: The set of dynamic imports. + + Returns: + Rendered App root component as string. + """ + imports_str = "\n".join([_RenderUtils.get_import(mod) for mod in imports]) + dynamic_imports_str = "\n".join(dynamic_imports) + + custom_code_str = "\n".join(custom_codes) + + import_window_libraries = "\n".join([ + f'import * as {lib_alias} from "{lib_path}";' + for lib_alias, lib_path in window_libraries + ]) + + window_imports_str = "\n".join([ + f' "{lib_path}": {lib_alias},' for lib_alias, lib_path in window_libraries + ]) + + return f""" +{imports_str} +{dynamic_imports_str} +import {{ EventLoopProvider, StateProvider, defaultColorMode }} from "$/utils/context"; +import {{ ThemeProvider }} from '$/utils/react-theme'; +import {{ Layout as AppLayout }} from './_document'; +import {{ Outlet }} from 'react-router'; +{import_window_libraries} + +{custom_code_str} + +function AppWrap({{children}}) {{ +{_render_hooks(hooks)} +return ({_RenderUtils.render(render)}) +}} + + +export function Layout({{children}}) {{ + useEffect(() => {{ + // Make contexts and state objects available globally for dynamic eval'd components + let windowImports = {{ + {window_imports_str} + }}; + window["__reflex"] = windowImports; + }}, []); + + return jsx(AppLayout, {{}}, + jsx(ThemeProvider, {{defaultTheme: defaultColorMode, attribute: "class"}}, + jsx(StateProvider, {{}}, + jsx(EventLoopProvider, {{}}, + jsx(AppWrap, {{}}, children) + ) + ) + ) + ); +}} + +export default function App() {{ + return jsx(Outlet, {{}}); +}} + +""" + + +def theme_template(theme: str): + """Template for the theme file. + + Args: + theme: The theme to render. + + Returns: + Rendered theme file content as string. + """ + return f"""export default {theme}""" + + +def context_template( + *, + is_dev_mode: bool, + default_color_mode: str, + initial_state: dict[str, Any] | None = None, + state_name: str | None = None, + client_storage: dict[str, dict[str, dict[str, Any]]] | None = None, +): + """Template for the context file. + + Args: + initial_state: The initial state for the context. + state_name: The name of the state. + client_storage: The client storage for the context. + is_dev_mode: Whether the app is in development mode. + default_color_mode: The default color mode for the context. + + Returns: + Rendered context file content as string. + """ + initial_state = initial_state or {} + state_contexts_str = "".join([ + f"{format_state_name(state_name)}: createContext(null)," + for state_name in initial_state + ]) + + state_str = ( + rf""" +export const state_name = "{state_name}" + +export const exception_state_name = "{constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL}" + +// These events are triggered on initial load and each page navigation. +export const onLoadInternalEvent = () => {{ + const internal_events = []; + + // Get tracked cookie and local storage vars to send to the backend. + const client_storage_vars = hydrateClientStorage(clientStorage); + // But only send the vars if any are actually set in the browser. + if (client_storage_vars && Object.keys(client_storage_vars).length !== 0) {{ + internal_events.push( + ReflexEvent( + '{state_name}.{constants.CompileVars.UPDATE_VARS_INTERNAL}', + {{vars: client_storage_vars}}, + ), + ); + }} + + // `on_load_internal` triggers the correct on_load event(s) for the current page. + // If the page does not define any on_load event, this will just set `is_hydrated = true`. + internal_events.push(ReflexEvent('{state_name}.{constants.CompileVars.ON_LOAD_INTERNAL}')); + + return internal_events; +}} + +// The following events are sent when the websocket connects or reconnects. +export const initialEvents = () => [ + ReflexEvent('{state_name}.{constants.CompileVars.HYDRATE}'), + ...onLoadInternalEvent() +] + """ + if state_name + else """ +export const state_name = undefined + +export const exception_state_name = undefined + +export const onLoadInternalEvent = () => [] + +export const initialEvents = () => [] +""" + ) + + state_reducer_str = "\n".join( + rf'const [{format_state_name(state_name)}, dispatch_{format_state_name(state_name)}] = useReducer(applyDelta, initialState["{state_name}"])' + for state_name in initial_state + ) + + create_state_contexts_str = "\n".join( + rf"createElement(StateContexts.{format_state_name(state_name)},{{value: {format_state_name(state_name)}}}," + for state_name in initial_state + ) + + dispatchers_str = "\n".join( + f'"{state_name}": dispatch_{format_state_name(state_name)},' + for state_name in initial_state + ) + + return rf"""import {{ createContext, useContext, useMemo, useReducer, useState, createElement, useEffect }} from "react" +import {{ applyDelta, ReflexEvent, hydrateClientStorage, useEventLoop, refs }} from "$/utils/state" +import {{ jsx }} from "@emotion/react"; + +export const initialState = {"{}" if not initial_state else json_dumps(initial_state)} + +export const defaultColorMode = {default_color_mode} +export const ColorModeContext = createContext(null); +export const UploadFilesContext = createContext(null); +export const DispatchContext = createContext(null); +export const StateContexts = {{{state_contexts_str}}}; +export const EventLoopContext = createContext(null); +export const clientStorage = {"{}" if client_storage is None else json.dumps(client_storage)} + +{state_str} + +export const isDevMode = {json.dumps(is_dev_mode)}; + +export function UploadFilesProvider({{ children }}) {{ + const [filesById, setFilesById] = useState({{}}) + refs["__clear_selected_files"] = (id) => setFilesById(filesById => {{ + const newFilesById = {{...filesById}} + delete newFilesById[id] + return newFilesById + }}) + return createElement( + UploadFilesContext.Provider, + {{ value: [filesById, setFilesById] }}, + children + ); +}} + +export function ClientSide(component) {{ + return ({{ children, ...props }}) => {{ + const [Component, setComponent] = useState(null); + useEffect(() => {{ + async function load() {{ + const comp = await component(); + setComponent(() => comp); + }} + load(); + }}, []); + return Component ? jsx(Component, props, children) : null; + }}; +}} + +export function EventLoopProvider({{ children }}) {{ + const dispatch = useContext(DispatchContext) + const [addEvents, connectErrors] = useEventLoop( + dispatch, + initialEvents, + clientStorage, + ) + return createElement( + EventLoopContext.Provider, + {{ value: [addEvents, connectErrors] }}, + children + ); +}} + +export function StateProvider({{ children }}) {{ + {state_reducer_str} + const dispatchers = useMemo(() => {{ + return {{ + {dispatchers_str} + }} + }}, []) + + return ( + {create_state_contexts_str} + createElement(DispatchContext, {{value: dispatchers}}, children) + {")" * len(initial_state)} + ) +}}""" + + +def component_template(component: Component | StatefulComponent): + """Template to render a component tag. + + Args: + component: The component to render. + + Returns: + Rendered component as string. + """ + return _RenderUtils.render(component.render()) + + +def page_template( + imports: Iterable[_ImportDict], + dynamic_imports: Iterable[str], + custom_codes: Iterable[str], + hooks: dict[str, VarData | None], + render: dict[str, Any], +): + """Template for a single react page. + + Args: + imports: List of import statements. + dynamic_imports: List of dynamic import statements. + custom_codes: List of custom code snippets. + hooks: Dictionary of hooks. + render: Render function for the component. + + Returns: + Rendered React page component as string. + """ + imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports]) + custom_code_str = "\n".join(custom_codes) + dynamic_imports_str = "\n".join(dynamic_imports) + + hooks_str = _render_hooks(hooks) + return f"""{imports_str} + +{dynamic_imports_str} + +{custom_code_str} + +export default function Component() {{ +{hooks_str} + + return ( + {_RenderUtils.render(render)} + ) +}}""" + + +def package_json_template( + scripts: dict[str, str], + dependencies: dict[str, str], + dev_dependencies: dict[str, str], + overrides: dict[str, str], +): + """Template for package.json. + + Args: + scripts: The scripts to include in the package.json file. + dependencies: The dependencies to include in the package.json file. + dev_dependencies: The devDependencies to include in the package.json file. + overrides: The overrides to include in the package.json file. + + Returns: + Rendered package.json content as string. + """ + return json.dumps({ + "name": "reflex", + "type": "module", + "scripts": scripts, + "dependencies": dependencies, + "devDependencies": dev_dependencies, + "overrides": overrides, + }) + + +def vite_config_template( + base: str, + hmr: bool, + force_full_reload: bool, + experimental_hmr: bool, + sourcemap: bool | Literal["inline", "hidden"], + allowed_hosts: bool | list[str] = False, +): + """Template for vite.config.js. + + Args: + base: The base path for the Vite config. + hmr: Whether to enable hot module replacement. + force_full_reload: Whether to force a full reload on changes. + experimental_hmr: Whether to enable experimental HMR features. + sourcemap: The sourcemap configuration. + allowed_hosts: Allow all hosts (True), specific hosts (list of strings), or only localhost (False). + + Returns: + Rendered vite.config.js content as string. + """ + if allowed_hosts is True: + allowed_hosts_line = "\n allowedHosts: true," + elif isinstance(allowed_hosts, list) and allowed_hosts: + allowed_hosts_line = f"\n allowedHosts: {json.dumps(allowed_hosts)}," + else: + allowed_hosts_line = "" + return rf"""import {{ fileURLToPath, URL }} from "url"; +import {{ reactRouter }} from "@react-router/dev/vite"; +import {{ defineConfig }} from "vite"; +import safariCacheBustPlugin from "./vite-plugin-safari-cachebust"; + +// Ensure that bun always uses the react-dom/server.node functions. +function alwaysUseReactDomServerNode() {{ + return {{ + name: "vite-plugin-always-use-react-dom-server-node", + enforce: "pre", + + resolveId(source, importer) {{ + if ( + typeof importer === "string" && + importer.endsWith("/entry.server.node.tsx") && + source.includes("react-dom/server") + ) {{ + return this.resolve("react-dom/server.node", importer, {{ + skipSelf: true, + }}); + }} + return null; + }}, + }}; +}} + +function fullReload() {{ + return {{ + name: "full-reload", + enforce: "pre", + handleHotUpdate({{ server }}) {{ + server.ws.send({{ + type: "full-reload", + }}); + return []; + }} + }}; +}} + +export default defineConfig((config) => ({{ + plugins: [ + alwaysUseReactDomServerNode(), + reactRouter(), + safariCacheBustPlugin(), + ].concat({"[fullReload()]" if force_full_reload else "[]"}), + build: {{ + assetsDir: "{base}assets".slice(1), + sourcemap: {"true" if sourcemap is True else "false" if sourcemap is False else repr(sourcemap)}, + rollupOptions: {{ + onwarn(warning, warn) {{ + if (warning.code === "EVAL" && warning.id && warning.id.endsWith("state.js")) return; + warn(warning); + }}, + jsx: {{}}, + output: {{ + advancedChunks: {{ + groups: [ + {{ + test: /env.json/, + name: "reflex-env", + }}, + ], + }}, + }}, + }}, + }}, + experimental: {{ + enableNativePlugin: false, + hmr: {"true" if experimental_hmr else "false"}, + }}, + server: {{ + port: process.env.PORT,{allowed_hosts_line} + hmr: {"true" if hmr else "false"}, + watch: {{ + ignored: [ + "**/.web/backend/**", + "**/.web/reflex.install_frontend_packages.cached", + ], + }}, + }}, + resolve: {{ + mainFields: ["browser", "module", "jsnext"], + alias: [ + {{ + find: "$", + replacement: fileURLToPath(new URL("./", import.meta.url)), + }}, + {{ + find: "@", + replacement: fileURLToPath(new URL("./public", import.meta.url)), + }}, + ], + }}, +}}));""" + + +def stateful_component_template( + tag_name: str, memo_trigger_hooks: list[str], component: Component, export: bool +): + """Template for stateful component. + + Args: + tag_name: The tag name for the component. + memo_trigger_hooks: The memo trigger hooks for the component. + component: The component to render. + export: Whether to export the component. + + Returns: + Rendered stateful component code as string. + """ + all_hooks = component._get_all_hooks() + return f""" +{"export " if export else ""}function {tag_name} () {{ + {_render_hooks(all_hooks, memo_trigger_hooks)} + return ( + {_RenderUtils.render(component.render())} + ) +}} +""" + + +def stateful_components_template(imports: list[_ImportDict], memoized_code: str) -> str: + """Template for stateful components. + + Args: + imports: List of import statements. + memoized_code: Memoized code for stateful components. + + Returns: + Rendered stateful components code as string. + """ + imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports]) + return f"{imports_str}\n{memoized_code}" + + +def memo_components_template( + imports: list[_ImportDict], + components: list[dict[str, Any]], + functions: list[dict[str, Any]], + dynamic_imports: Iterable[str], + custom_codes: Iterable[str], +) -> str: + """Template for custom component. + + Args: + imports: List of import statements. + components: List of component definitions. + functions: List of function definitions. + dynamic_imports: List of dynamic import statements. + custom_codes: List of custom code snippets. + + Returns: + Rendered custom component code as string. + """ + imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports]) + dynamic_imports_str = "\n".join(dynamic_imports) + custom_code_str = "\n".join(custom_codes) + + components_code = "" + for component in components: + components_code += f""" +export const {component["name"]} = memo(({component["signature"]}) => {{ + {_render_hooks(component.get("hooks", {}))} + return( + {_RenderUtils.render(component["render"])} + ) +}}); +""" + + functions_code = "" + for function in functions: + functions_code += ( + f"\nexport const {function['name']} = {function['function']};\n" + ) + + return f""" +{imports_str} + +{dynamic_imports_str} + +{custom_code_str} + +{functions_code} + +{components_code}""" + + +def styles_template(stylesheets: list[str]) -> str: + """Template for styles.css. + + Args: + stylesheets: List of stylesheets to include. + + Returns: + Rendered styles.css content as string. + """ + return "@layer __reflex_base;\n" + "\n".join([ + f"@import url('{sheet_name}');" for sheet_name in stylesheets + ]) + + +def _render_hooks(hooks: dict[str, VarData | None], memo: list | None = None) -> str: + """Render hooks for macros. + + Args: + hooks: Dictionary of hooks to render. + memo: Optional list of memo hooks. + + Returns: + Rendered hooks code as string. + """ + internal, pre_trigger, post_trigger = _sort_hooks(hooks) + internal_str = "\n".join(internal) + pre_trigger_str = "\n".join(pre_trigger) + post_trigger_str = "\n".join(post_trigger) + memo_str = "\n".join(memo) if memo is not None else "" + return f"{internal_str}\n{pre_trigger_str}\n{memo_str}\n{post_trigger_str}" diff --git a/packages/reflex-core/src/reflex_core/components/__init__.py b/packages/reflex-core/src/reflex_core/components/__init__.py new file mode 100644 index 00000000000..44b8f771ba9 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/__init__.py @@ -0,0 +1 @@ +"""Reflex core components.""" diff --git a/packages/reflex-core/src/reflex_core/components/component.py b/packages/reflex-core/src/reflex_core/components/component.py new file mode 100644 index 00000000000..82d60203b22 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/component.py @@ -0,0 +1,3022 @@ +"""Base component definitions.""" + +from __future__ import annotations + +import contextlib +import copy +import dataclasses +import enum +import functools +import inspect +import typing +from abc import ABC, ABCMeta, abstractmethod +from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence +from dataclasses import _MISSING_TYPE, MISSING +from functools import wraps +from hashlib import md5 +from types import SimpleNamespace +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast, get_args, get_origin + +from rich.markup import escape +from typing_extensions import dataclass_transform + +from reflex_core import constants +from reflex_core.breakpoints import Breakpoints +from reflex_core.compiler.templates import stateful_component_template +from reflex_core.components.dynamic import load_dynamic_serializer +from reflex_core.components.field import BaseField, FieldBasedMeta +from reflex_core.components.tags import Tag +from reflex_core.constants import ( + Dirs, + EventTriggers, + Hooks, + Imports, + MemoizationDisposition, + MemoizationMode, + PageNames, +) +from reflex_core.constants.compiler import SpecialAttributes +from reflex_core.constants.state import CAMEL_CASE_MEMO_MARKER, FRONTEND_EVENT_STATE +from reflex_core.event import ( + EventCallback, + EventChain, + EventHandler, + EventSpec, + args_specs_from_fields, + no_args_event_spec, + parse_args_spec, + pointer_event_spec, + run_script, + unwrap_var_annotation, +) +from reflex_core.style import Style, format_as_emotion +from reflex_core.utils import console, format, imports, types +from reflex_core.utils.imports import ImportDict, ImportVar, ParsedImportDict +from reflex_core.vars import VarData +from reflex_core.vars.base import ( + CachedVarOperation, + LiteralNoneVar, + LiteralVar, + Var, + cached_property_no_lock, +) +from reflex_core.vars.function import ( + ArgsFunctionOperation, + FunctionStringVar, + FunctionVar, +) +from reflex_core.vars.number import ternary_operation +from reflex_core.vars.object import ObjectVar +from reflex_core.vars.sequence import LiteralArrayVar, LiteralStringVar, StringVar + +if TYPE_CHECKING: + import reflex.state + +FIELD_TYPE = TypeVar("FIELD_TYPE") + + +class ComponentField(BaseField[FIELD_TYPE]): + """A field for a component.""" + + def __init__( + self, + default: FIELD_TYPE | _MISSING_TYPE = MISSING, + default_factory: Callable[[], FIELD_TYPE] | None = None, + is_javascript: bool | None = None, + annotated_type: type[Any] | _MISSING_TYPE = MISSING, + doc: str | None = None, + ) -> None: + """Initialize the field. + + Args: + default: The default value for the field. + default_factory: The default factory for the field. + is_javascript: Whether the field is a javascript property. + annotated_type: The annotated type for the field. + doc: Documentation string for the field. + """ + super().__init__(default, default_factory, annotated_type) + self.doc = doc + self.is_javascript = is_javascript + + def __repr__(self) -> str: + """Represent the field in a readable format. + + Returns: + The string representation of the field. + """ + annotated_type_str = ( + f", annotated_type={self.annotated_type!r}" + if self.annotated_type is not MISSING + else "" + ) + if self.default is not MISSING: + return f"ComponentField(default={self.default!r}, is_javascript={self.is_javascript!r}{annotated_type_str})" + return f"ComponentField(default_factory={self.default_factory!r}, is_javascript={self.is_javascript!r}{annotated_type_str})" + + +def field( + default: FIELD_TYPE | _MISSING_TYPE = MISSING, + default_factory: Callable[[], FIELD_TYPE] | None = None, + is_javascript_property: bool | None = None, + doc: str | None = None, +) -> FIELD_TYPE: + """Create a field for a component. + + Args: + default: The default value for the field. + default_factory: The default factory for the field. + is_javascript_property: Whether the field is a javascript property. + doc: Documentation string for the field. + + Returns: + The field for the component. + + Raises: + ValueError: If both default and default_factory are specified. + """ + if default is not MISSING and default_factory is not None: + msg = "cannot specify both default and default_factory" + raise ValueError(msg) + return ComponentField( # pyright: ignore [reportReturnType] + default=default, + default_factory=default_factory, + is_javascript=is_javascript_property, + doc=doc, + ) + + +@dataclass_transform(kw_only_default=True, field_specifiers=(field,)) +class BaseComponentMeta(FieldBasedMeta, ABCMeta): + """Meta class for BaseComponent.""" + + if TYPE_CHECKING: + _inherited_fields: Mapping[str, ComponentField] + _own_fields: Mapping[str, ComponentField] + _fields: Mapping[str, ComponentField] + _js_fields: Mapping[str, ComponentField] + + @classmethod + def _process_annotated_fields( + cls, + namespace: dict[str, Any], + annotations: dict[str, Any], + inherited_fields: dict[str, ComponentField], + ) -> dict[str, ComponentField]: + own_fields: dict[str, ComponentField] = {} + + for key, annotation in annotations.items(): + value = namespace.get(key, MISSING) + + if types.is_classvar(annotation): + # If the annotation is a classvar, skip it. + continue + + if value is MISSING: + value = ComponentField( + default=None, + is_javascript=(key[0] != "_"), + annotated_type=annotation, + ) + elif not isinstance(value, ComponentField): + value = ComponentField( + default=value, + is_javascript=( + (key[0] != "_") + if (existing_field := inherited_fields.get(key)) is None + else existing_field.is_javascript + ), + annotated_type=annotation, + ) + else: + is_js = value.is_javascript + if is_js is None: + if (existing_field := inherited_fields.get(key)) is not None: + is_js = existing_field.is_javascript + else: + is_js = key[0] != "_" + default = value.default + # If no default or factory provided, default to None + # (same behavior as bare annotations without field()) + if default is MISSING and value.default_factory is None: + default = None + value = ComponentField( + default=default, + default_factory=value.default_factory, + is_javascript=is_js, + annotated_type=annotation, + doc=value.doc, + ) + + own_fields[key] = value + + return own_fields + + @classmethod + def _create_field( + cls, + annotated_type: Any, + default: Any = MISSING, + default_factory: Callable[[], Any] | None = None, + ) -> ComponentField: + return ComponentField( + annotated_type=annotated_type, + default=default, + default_factory=default_factory, + is_javascript=True, # Default for components + ) + + @classmethod + def _process_field_overrides( + cls, + namespace: dict[str, Any], + annotations: dict[str, Any], + inherited_fields: dict[str, Any], + ) -> dict[str, ComponentField]: + own_fields: dict[str, ComponentField] = {} + + for key, value, inherited_field in [ + (key, value, inherited_field) + for key, value in namespace.items() + if key not in annotations + and ((inherited_field := inherited_fields.get(key)) is not None) + ]: + new_field = ComponentField( + default=value, + is_javascript=inherited_field.is_javascript, + annotated_type=inherited_field.annotated_type, + ) + own_fields[key] = new_field + + return own_fields + + @classmethod + def _finalize_fields( + cls, + namespace: dict[str, Any], + inherited_fields: dict[str, ComponentField], + own_fields: dict[str, ComponentField], + ) -> None: + # Call parent implementation + super()._finalize_fields(namespace, inherited_fields, own_fields) + + # Add JavaScript fields mapping + all_fields = namespace["_fields"] + namespace["_js_fields"] = { + key: value + for key, value in all_fields.items() + if value.is_javascript is True + } + + +class BaseComponent(metaclass=BaseComponentMeta): + """The base class for all Reflex components. + + This is something that can be rendered as a Component via the Reflex compiler. + """ + + children: list[BaseComponent] = field( + doc="The children nested within the component.", + default_factory=list, + is_javascript_property=False, + ) + + # The library that the component is based on. + library: str | None = field(default=None, is_javascript_property=False) + + lib_dependencies: list[str] = field( + doc="List here the non-react dependency needed by `library`", + default_factory=list, + is_javascript_property=False, + ) + + # The tag to use when rendering the component. + tag: str | None = field(default=None, is_javascript_property=False) + + def __init__( + self, + **kwargs, + ): + """Initialize the component. + + Args: + **kwargs: The kwargs to pass to the component. + """ + for key, value in kwargs.items(): + setattr(self, key, value) + for name, value in self.get_fields().items(): + if name not in kwargs: + setattr(self, name, value.default_value()) + + def set(self, **kwargs): + """Set the component props. + + Args: + **kwargs: The kwargs to set. + + Returns: + The component with the updated props. + """ + for key, value in kwargs.items(): + setattr(self, key, value) + return self + + def __eq__(self, value: Any) -> bool: + """Check if the component is equal to another value. + + Args: + value: The value to compare to. + + Returns: + Whether the component is equal to the value. + """ + return type(self) is type(value) and bool( + getattr(self, key) == getattr(value, key) for key in self.get_fields() + ) + + @classmethod + def get_fields(cls) -> Mapping[str, ComponentField]: + """Get the fields of the component. + + Returns: + The fields of the component. + """ + return cls._fields + + @classmethod + def get_js_fields(cls) -> Mapping[str, ComponentField]: + """Get the javascript fields of the component. + + Returns: + The javascript fields of the component. + """ + return cls._js_fields + + @abstractmethod + def render(self) -> dict: + """Render the component. + + Returns: + The dictionary for template of the component. + """ + + @abstractmethod + def _get_all_hooks_internal(self) -> dict[str, VarData | None]: + """Get the reflex internal hooks for the component and its children. + + Returns: + The code that should appear just before user-defined hooks. + """ + + @abstractmethod + def _get_all_hooks(self) -> dict[str, VarData | None]: + """Get the React hooks for this component. + + Returns: + The code that should appear just before returning the rendered component. + """ + + @abstractmethod + def _get_all_imports(self) -> ParsedImportDict: + """Get all the libraries and fields that are used by the component. + + Returns: + The import dict with the required imports. + """ + + @abstractmethod + def _get_all_dynamic_imports(self) -> set[str]: + """Get dynamic imports for the component. + + Returns: + The dynamic imports. + """ + + @abstractmethod + def _get_all_custom_code(self) -> dict[str, None]: + """Get custom code for the component. + + Returns: + The custom code. + """ + + @abstractmethod + def _get_all_refs(self) -> dict[str, None]: + """Get the refs for the children of the component. + + Returns: + The refs for the children. + """ + + +class ComponentNamespace(SimpleNamespace): + """A namespace to manage components with subcomponents.""" + + def __hash__(self) -> int: # pyright: ignore [reportIncompatibleVariableOverride] + """Get the hash of the namespace. + + Returns: + The hash of the namespace. + """ + return hash(type(self).__name__) + + +def evaluate_style_namespaces(style: ComponentStyle) -> dict: + """Evaluate namespaces in the style. + + Args: + style: The style to evaluate. + + Returns: + The evaluated style. + """ + return { + k.__call__ if isinstance(k, ComponentNamespace) else k: v + for k, v in style.items() + } + + +# Map from component to styling. +ComponentStyle = dict[str | type[BaseComponent] | Callable | ComponentNamespace, Any] +ComponentChildTypes = (*types.PrimitiveTypes, Var, BaseComponent, type(None)) + + +def _satisfies_type_hint(obj: Any, type_hint: Any) -> bool: + return types._isinstance( + obj, + type_hint, + nested=1, + treat_var_as_type=True, + treat_mutable_obj_as_immutable=( + isinstance(obj, Var) and not isinstance(obj, LiteralVar) + ), + ) + + +def satisfies_type_hint(obj: Any, type_hint: Any) -> bool: + """Check if an object satisfies a type hint. + + Args: + obj: The object to check. + type_hint: The type hint to check against. + + Returns: + Whether the object satisfies the type hint. + """ + if _satisfies_type_hint(obj, type_hint): + return True + if _satisfies_type_hint(obj, type_hint | None): + obj = ( + obj + if not isinstance(obj, Var) + else (obj._var_value if isinstance(obj, LiteralVar) else obj) + ) + console.warn( + "Passing None to a Var that is not explicitly marked as Optional (| None) is deprecated. " + f"Passed {obj!s} of type {escape(str(type(obj) if not isinstance(obj, Var) else obj._var_type))} to {escape(str(type_hint))}." + ) + return True + return False + + +def _components_from( + component_or_var: BaseComponent | Var, +) -> tuple[BaseComponent, ...]: + """Get the components from a component or Var. + + Args: + component_or_var: The component or Var to get the components from. + + Returns: + The components. + """ + if isinstance(component_or_var, Var): + var_data = component_or_var._get_all_var_data() + return var_data.components if var_data else () + if isinstance(component_or_var, BaseComponent): + return (component_or_var,) + return () + + +def _hash_str(value: str) -> str: + return md5(f'"{value}"'.encode(), usedforsecurity=False).hexdigest() + + +def _hash_sequence(value: Sequence) -> str: + return _hash_str(str([_deterministic_hash(v) for v in value])) + + +def _hash_dict(value: dict) -> str: + return _hash_sequence( + sorted([(k, _deterministic_hash(v)) for k, v in value.items()]) + ) + + +def _deterministic_hash(value: object) -> str: + """Hash a rendered dictionary. + + Args: + value: The dictionary to hash. + + Returns: + The hash of the dictionary. + + Raises: + TypeError: If the value is not hashable. + """ + if value is None: + # Hash None as a special case. + return "None" + if isinstance(value, (int, float, enum.Enum)): + # Hash numbers and booleans directly. + return str(value) + if isinstance(value, str): + return _hash_str(value) + if isinstance(value, dict): + return _hash_dict(value) + if isinstance(value, (tuple, list)): + # Hash tuples by hashing each element. + return _hash_sequence(value) + if isinstance(value, Var): + return _hash_str( + str((value._js_expr, _deterministic_hash(value._get_all_var_data()))) + ) + if dataclasses.is_dataclass(value): + return _hash_dict({ + k.name: getattr(value, k.name) for k in dataclasses.fields(value) + }) + if isinstance(value, BaseComponent): + # If the value is a component, hash its rendered code. + return _hash_dict(value.render()) + + msg = ( + f"Cannot hash value `{value}` of type `{type(value).__name__}`. " + "Only BaseComponent, Var, VarData, dict, str, tuple, and enum.Enum are supported." + ) + raise TypeError(msg) + + +@dataclasses.dataclass(kw_only=True, frozen=True, slots=True) +class TriggerDefinition: + """A default event trigger with its args spec and description.""" + + spec: types.ArgsSpec | Sequence[types.ArgsSpec] + description: str + + +DEFAULT_TRIGGERS_AND_DESC: Mapping[str, TriggerDefinition] = { + EventTriggers.ON_FOCUS: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the element (or some element inside of it) receives focus. For example, it is called when the user clicks on a text input.", + ), + EventTriggers.ON_BLUR: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when focus has left the element (or left some element inside of it). For example, it is called when the user clicks outside of a focused text input.", + ), + EventTriggers.ON_CLICK: TriggerDefinition( + spec=pointer_event_spec, # pyright: ignore [reportArgumentType] + description="Fired when the user clicks on an element. For example, it's called when the user clicks on a button.", + ), + EventTriggers.ON_CONTEXT_MENU: TriggerDefinition( + spec=pointer_event_spec, # pyright: ignore [reportArgumentType] + description="Fired when the user right-clicks on an element.", + ), + EventTriggers.ON_DOUBLE_CLICK: TriggerDefinition( + spec=pointer_event_spec, # pyright: ignore [reportArgumentType] + description="Fired when the user double-clicks on an element.", + ), + EventTriggers.ON_MOUSE_DOWN: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the user presses a mouse button on an element.", + ), + EventTriggers.ON_MOUSE_ENTER: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the mouse pointer enters the element.", + ), + EventTriggers.ON_MOUSE_LEAVE: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the mouse pointer leaves the element.", + ), + EventTriggers.ON_MOUSE_MOVE: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the mouse pointer moves over the element.", + ), + EventTriggers.ON_MOUSE_OUT: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the mouse pointer moves out of the element.", + ), + EventTriggers.ON_MOUSE_OVER: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the mouse pointer moves onto the element.", + ), + EventTriggers.ON_MOUSE_UP: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the user releases a mouse button on an element.", + ), + EventTriggers.ON_SCROLL: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the user scrolls the element.", + ), + EventTriggers.ON_SCROLL_END: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when scrolling ends on the element.", + ), + EventTriggers.ON_MOUNT: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the component is mounted to the page.", + ), + EventTriggers.ON_UNMOUNT: TriggerDefinition( + spec=no_args_event_spec, + description="Fired when the component is removed from the page. Only called during navigation, not on page refresh.", + ), +} +DEFAULT_TRIGGERS = { + name: trigger.spec for name, trigger in DEFAULT_TRIGGERS_AND_DESC.items() +} + +T = TypeVar("T", bound="Component") + + +class Component(BaseComponent, ABC): + """A component with style, event trigger and other props.""" + + style: Style = field( + doc="The style of the component.", + default_factory=Style, + is_javascript_property=False, + ) + + event_triggers: dict[str, EventChain | Var] = field( + doc="A mapping from event triggers to event chains.", + default_factory=dict, + is_javascript_property=False, + ) + + # The alias for the tag. + alias: str | None = field(default=None, is_javascript_property=False) + + # Whether the component is a global scope tag. True for tags like `html`, `head`, `body`. + _is_tag_in_global_scope: ClassVar[bool] = False + + # Whether the import is default or named. + is_default: bool | None = field(default=False, is_javascript_property=False) + + key: Any = field( + doc="A unique key for the component.", + default=None, + is_javascript_property=False, + ) + + id: Any = field( + doc="The id for the component.", default=None, is_javascript_property=False + ) + + ref: Var | None = field( + doc="The Var to pass as the ref to the component.", + default=None, + is_javascript_property=False, + ) + + class_name: Any = field( + doc="The class name for the component.", + default=None, + is_javascript_property=False, + ) + + special_props: list[Var] = field( + doc="Special component props.", + default_factory=list, + is_javascript_property=False, + ) + + # components that cannot be children + _invalid_children: ClassVar[list[str]] = [] + + # only components that are allowed as children + _valid_children: ClassVar[list[str]] = [] + + # only components that are allowed as parent + _valid_parents: ClassVar[list[str]] = [] + + # props to change the name of + _rename_props: ClassVar[dict[str, str]] = {} + + custom_attrs: dict[str, Var | Any] = field( + doc="custom attribute", default_factory=dict, is_javascript_property=False + ) + + _memoization_mode: MemoizationMode = field( + doc="When to memoize this component and its children.", + default_factory=MemoizationMode, + is_javascript_property=False, + ) + + # State class associated with this component instance + State: type[reflex.state.State] | None = field( + default=None, is_javascript_property=False + ) + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the component. + + This method should be implemented by subclasses to add new imports for the component. + + Implementations do NOT need to call super(). The result of calling + add_imports in each parent class will be merged internally. + + Returns: + The additional imports for this component subclass. + + The format of the return value is a dictionary where the keys are the + library names (with optional npm-style version specifications) mapping + to a single name to be imported, or a list names to be imported. + + For advanced use cases, the values can be ImportVar instances (for + example, to provide an alias or mark that an import is the default + export from the given library). + + ```python + return { + "react": "useEffect", + "react-draggable": ["DraggableCore", rx.ImportVar(tag="Draggable", is_default=True)], + } + ``` + """ + return {} + + 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 + React component function. + + Each logical hook should be a separate string in the list. + + Common strings will be deduplicated and inserted into the component + function only once, so define const variables and other identical code + in their own strings to avoid defining the same const or hook multiple + times. + + If a hook depends on specific data from the component instance, be sure + to use unique values inside the string to _avoid_ deduplication. + + Implementations do NOT need to call super(). The result of calling + add_hooks in each parent class will be merged and deduplicated internally. + + Returns: + The additional hooks for this component subclass. + + ```python + return [ + "const [count, setCount] = useState(0);", + "useEffect(() => { setCount((prev) => prev + 1); console.log(`mounted ${count} times`); }, []);", + ] + ``` + """ + return [] + + def add_custom_code(self) -> list[str]: + """Add custom Javascript code into the page that contains this component. + + Custom code is inserted at module level, after any imports. + + Each string of custom code is deduplicated per-page, so take care to + avoid defining the same const or function differently from different + component instances. + + Custom code is useful for defining global functions or constants which + can then be referenced inside hooks or used by component vars. + + Implementations do NOT need to call super(). The result of calling + add_custom_code in each parent class will be merged and deduplicated internally. + + Returns: + The additional custom code for this component subclass. + + ```python + return [ + "const translatePoints = (event) => { return { x: event.clientX, y: event.clientY }; };", + ] + ``` + """ + return [] + + @classmethod + def __init_subclass__(cls, **kwargs): + """Set default properties. + + Args: + **kwargs: The kwargs to pass to the superclass. + """ + super().__init_subclass__(**kwargs) + + # Ensure renamed props from parent classes are applied to the subclass. + if cls._rename_props: + inherited_rename_props = {} + for parent in reversed(cls.mro()): + if issubclass(parent, Component) and parent._rename_props: + inherited_rename_props.update(parent._rename_props) + cls._rename_props = inherited_rename_props + + def __init__(self, **kwargs): + """Initialize the custom component. + + Args: + **kwargs: The kwargs to pass to the component. + """ + console.error( + "Instantiating components directly is not supported." + f" Use `{self.__class__.__name__}.create` method instead." + ) + + def _post_init(self, *args, **kwargs): + """Initialize the component. + + Args: + *args: Args to initialize the component. + **kwargs: Kwargs to initialize the component. + + Raises: + TypeError: If an invalid prop is passed. + ValueError: If an event trigger passed is not valid. + """ + # Set the id and children initially. + children = kwargs.get("children", []) + + self._validate_component_children(children) + + # Get the component fields, triggers, and props. + fields = self.get_fields() + component_specific_triggers = self.get_event_triggers() + props = self.get_props() + + # Add any events triggers. + if "event_triggers" not in kwargs: + kwargs["event_triggers"] = {} + kwargs["event_triggers"] = kwargs["event_triggers"].copy() + + # Iterate through the kwargs and set the props. + for key, value in kwargs.items(): + if ( + key.startswith("on_") + and key not in component_specific_triggers + and key not in props + ): + valid_triggers = sorted(component_specific_triggers.keys()) + msg = ( + f"The {(comp_name := type(self).__name__)} does not take in an `{key}` event trigger. " + f"Valid triggers for {comp_name}: {valid_triggers}. " + f"If {comp_name} is a third party component make sure to add `{key}` to the component's event triggers. " + f"visit https://reflex.dev/docs/wrapping-react/guide/#event-triggers for more info." + ) + raise ValueError(msg) + if key in component_specific_triggers: + # Event triggers are bound to event chains. + is_var = False + elif key in props: + # Set the field type. + is_var = ( + field.type_origin is Var if (field := fields.get(key)) else False + ) + else: + continue + + # Check whether the key is a component prop. + if is_var: + try: + kwargs[key] = LiteralVar.create(value) + + # Get the passed type and the var type. + passed_type = kwargs[key]._var_type + expected_type = types.get_args( + types.get_field_type(type(self), key) + )[0] + except TypeError: + # If it is not a valid var, check the base types. + passed_type = type(value) + expected_type = types.get_field_type(type(self), key) + + if not satisfies_type_hint(value, expected_type): + value_name = value._js_expr if isinstance(value, Var) else value + + additional_info = ( + " You can call `.bool()` on the value to convert it to a boolean." + if expected_type is bool and isinstance(value, Var) + else "" + ) + + raise TypeError( + f"Invalid var passed for prop {type(self).__name__}.{key}, expected type {expected_type}, got value {value_name} of type {passed_type}." + + additional_info + ) + # Check if the key is an event trigger. + if key in component_specific_triggers: + kwargs["event_triggers"][key] = EventChain.create( + value=value, + args_spec=component_specific_triggers[key], + key=key, + ) + + # Remove any keys that were added as events. + for key in kwargs["event_triggers"]: + kwargs.pop(key, None) + + # Place data_ and aria_ attributes into custom_attrs + special_attributes = [ + key + for key in kwargs + if key not in fields and SpecialAttributes.is_special(key) + ] + if special_attributes: + custom_attrs = kwargs.setdefault("custom_attrs", {}) + custom_attrs.update({ + format.to_kebab_case(key): kwargs.pop(key) for key in special_attributes + }) + + # Add style props to the component. + style = kwargs.get("style", {}) + if isinstance(style, Sequence): + if any(not isinstance(s, Mapping) for s in style): + msg = "Style must be a dictionary or a list of dictionaries." + raise TypeError(msg) + # Merge styles, the later ones overriding keys in the earlier ones. + style = { + k: v + for style_dict in style + for k, v in cast(Mapping, style_dict).items() + } + + if isinstance(style, (Breakpoints, Var)): + style = { + # Assign the Breakpoints to the self-referential selector to avoid squashing down to a regular dict. + "&": style, + } + + fields_style = self.get_fields()["style"] + + kwargs["style"] = Style({ + **fields_style.default_value(), + **style, + **{attr: value for attr, value in kwargs.items() if attr not in fields}, + }) + + # Convert class_name to str if it's list + class_name = kwargs.get("class_name", "") + if isinstance(class_name, (list, tuple)): + has_var = False + for c in class_name: + if isinstance(c, str): + continue + if isinstance(c, Var): + if not isinstance(c, StringVar) and not issubclass( + c._var_type, str + ): + msg = f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {c._js_expr} of type {c._var_type}." + raise TypeError(msg) + has_var = True + else: + msg = f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {c} of type {type(c)}." + raise TypeError(msg) + if has_var: + kwargs["class_name"] = LiteralArrayVar.create( + class_name, _var_type=list[str] + ).join(" ") + else: + kwargs["class_name"] = " ".join(class_name) + elif ( + isinstance(class_name, Var) + and not isinstance(class_name, StringVar) + and not issubclass(class_name._var_type, str) + ): + msg = f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {class_name._js_expr} of type {class_name._var_type}." + raise TypeError(msg) + # Construct the component. + for key, value in kwargs.items(): + setattr(self, key, value) + + @classmethod + def get_event_triggers(cls) -> dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]]: + """Get the event triggers for the component. + + Returns: + The event triggers. + """ + # Look for component specific triggers, + # e.g. variable declared as EventHandler types. + return DEFAULT_TRIGGERS | args_specs_from_fields(cls.get_fields()) # pyright: ignore [reportOperatorIssue] + + def __repr__(self) -> str: + """Represent the component in React. + + Returns: + The code to render the component. + """ + return format.json_dumps(self.render()) + + def __str__(self) -> str: + """Represent the component in React. + + Returns: + The code to render the component. + """ + from reflex.compiler.compiler import _compile_component + + return _compile_component(self) + + def _exclude_props(self) -> list[str]: + """Props to exclude when adding the component props to the Tag. + + Returns: + A list of component props to exclude. + """ + return [] + + def _render(self, props: dict[str, Any] | None = None) -> Tag: + """Define how to render the component in React. + + Args: + props: The props to render (if None, then use get_props). + + Returns: + The tag to render. + """ + # Create the base tag. + name = (self.tag if not self.alias else self.alias) or "" + if self._is_tag_in_global_scope and self.library is None: + name = '"' + name + '"' + + # Create the base tag. + tag = Tag( + name=name, + special_props=self.special_props.copy(), + ) + + if props is None: + # Add component props to the tag. + props = { + attr.removesuffix("_"): getattr(self, attr) for attr in self.get_props() + } + + # Add ref to element if `ref` is None and `id` is not None. + if self.ref is not None: + props["ref"] = self.ref + elif (ref := self.get_ref()) is not None: + props["ref"] = Var(_js_expr=ref) + else: + props = props.copy() + + props.update( + **{ + trigger: handler + for trigger, handler in self.event_triggers.items() + if trigger not in {EventTriggers.ON_MOUNT, EventTriggers.ON_UNMOUNT} + }, + key=self.key, + id=self.id, + class_name=self.class_name, + ) + props.update(self._get_style()) + props.update(self.custom_attrs) + + # remove excluded props from prop dict before adding to tag. + for prop_to_exclude in self._exclude_props(): + props.pop(prop_to_exclude, None) + + return tag.add_props(**props) + + @classmethod + @functools.cache + def get_props(cls) -> Iterable[str]: + """Get the unique fields for the component. + + Returns: + The unique fields. + """ + return cls.get_js_fields() + + @classmethod + @functools.cache + def get_initial_props(cls) -> set[str]: + """Get the initial props to set for the component. + + Returns: + The initial props to set. + """ + return set() + + @functools.cached_property + def _get_component_prop_property(self) -> Sequence[BaseComponent]: + return [ + component + for prop in self.get_props() + if (value := getattr(self, prop)) is not None + and isinstance(value, (BaseComponent, Var)) + for component in _components_from(value) + ] + + def _get_components_in_props(self) -> Sequence[BaseComponent]: + """Get the components in the props. + + Returns: + The components in the props + """ + return self._get_component_prop_property + + @classmethod + def _validate_children(cls, children: tuple | list): + from reflex_core.utils.exceptions import ChildrenTypeError + + for child in children: + if isinstance(child, (tuple, list)): + cls._validate_children(child) + + # Make sure the child is a valid type. + if isinstance(child, dict) or not isinstance(child, ComponentChildTypes): + raise ChildrenTypeError(component=cls.__name__, child=child) + + @classmethod + def create(cls: type[T], *children, **props) -> T: + """Create the component. + + Args: + *children: The children of the component. + **props: The props of the component. + + Returns: + The component. + """ + # Import here to avoid circular imports. + from reflex_components_core.base.bare import Bare + from reflex_components_core.base.fragment import Fragment + + # Filter out None props + props = {key: value for key, value in props.items() if value is not None} + + # Validate all the children. + cls._validate_children(children) + + children_normalized = [ + ( + child + if isinstance(child, Component) + else ( + Fragment.create(*child) + if isinstance(child, tuple) + else Bare.create(contents=LiteralVar.create(child)) + ) + ) + for child in children + ] + + return cls._create(children_normalized, **props) + + @classmethod + def _create(cls: type[T], children: Sequence[BaseComponent], **props: Any) -> T: + """Create the component. + + Args: + children: The children of the component. + **props: The props of the component. + + Returns: + The component. + """ + comp = cls.__new__(cls) + super(Component, comp).__init__(id=props.get("id"), children=list(children)) + comp._post_init(children=list(children), **props) + return comp + + @classmethod + def _unsafe_create( + cls: type[T], children: Sequence[BaseComponent], **props: Any + ) -> T: + """Create the component without running post_init. + + Args: + children: The children of the component. + **props: The props of the component. + + Returns: + The component. + """ + comp = cls.__new__(cls) + super(Component, comp).__init__(id=props.get("id"), children=list(children)) + for prop, value in props.items(): + setattr(comp, prop, value) + return comp + + def add_style(self) -> dict[str, Any] | None: + """Add style to the component. + + Downstream components can override this method to return a style dict + that will be applied to the component. + + Returns: + The style to add. + """ + return None + + def _add_style(self) -> Style: + """Call add_style for all bases in the MRO. + + Downstream components should NOT override. Use add_style instead. + + Returns: + The style to add. + """ + styles = [] + + # Walk the MRO to call all `add_style` methods. + for base in self._iter_parent_classes_with_method("add_style"): + s = base.add_style(self) + if s is not None: + styles.append(s) + + style_ = Style() + for s in reversed(styles): + style_.update(s) + return style_ + + def _get_component_style(self, styles: ComponentStyle | Style) -> Style | None: + """Get the style to the component from `App.style`. + + Args: + styles: The style to apply. + + Returns: + The style of the component. + """ + component_style = None + if (style := styles.get(type(self))) is not None: # pyright: ignore [reportArgumentType] + component_style = Style(style) + if (style := styles.get(self.create)) is not None: # pyright: ignore [reportArgumentType] + component_style = Style(style) + return component_style + + def _add_style_recursive( + self, style: ComponentStyle | Style, theme: Component | None = None + ) -> Component: + """Add additional style to the component and its children. + + Apply order is as follows (with the latest overriding the earliest): + 1. Default style from `_add_style`/`add_style`. + 2. User-defined style from `App.style`. + 3. User-defined style from `Component.style`. + 4. style dict and css props passed to the component instance. + + Args: + style: A dict from component to styling. + theme: The theme to apply. (for retro-compatibility with deprecated _apply_theme API) + + Returns: + The component with the additional style. + + Raises: + UserWarning: If `_add_style` has been overridden. + """ + # 1. Default style from `_add_style`/`add_style`. + if type(self)._add_style != Component._add_style: + msg = "Do not override _add_style directly. Use add_style instead." + raise UserWarning(msg) + new_style = self._add_style() + style_vars = [new_style._var_data] + + # 2. User-defined style from `App.style`. + component_style = self._get_component_style(style) + if component_style: + new_style.update(component_style) + style_vars.append(component_style._var_data) + + # 4. style dict and css props passed to the component instance. + new_style.update(self.style) + style_vars.append(self.style._var_data) + + new_style._var_data = VarData.merge(*style_vars) + + # Assign the new style + self.style = new_style + + # Recursively add style to the children. + for child in self.children: + # Skip BaseComponent and StatefulComponent children. + if not isinstance(child, Component): + continue + child._add_style_recursive(style, theme) + return self + + def _get_style(self) -> dict: + """Get the style for the component. + + Returns: + The dictionary of the component style as value and the style notation as key. + """ + if isinstance(self.style, Var): + return {"css": self.style} + emotion_style = format_as_emotion(self.style) + return ( + {"css": LiteralVar.create(emotion_style)} + if emotion_style is not None + else {} + ) + + def render(self) -> dict: + """Render the component. + + Returns: + The dictionary for template of component. + """ + tag = self._render() + rendered_dict = dict( + tag.set( + children=[child.render() for child in self.children], + ) + ) + self._replace_prop_names(rendered_dict) + return rendered_dict + + def _replace_prop_names(self, rendered_dict: dict) -> None: + """Replace the prop names in the render dictionary. + + Args: + rendered_dict: The render dictionary with all the component props and event handlers. + """ + # fast path + if not self._rename_props: + return + + for ix, prop in enumerate(rendered_dict["props"]): + for old_prop, new_prop in self._rename_props.items(): + if prop.startswith(old_prop): + rendered_dict["props"][ix] = prop.replace(old_prop, new_prop, 1) + + def _validate_component_children(self, children: list[Component]): + """Validate the children components. + + Args: + children: The children of the component. + + """ + from reflex_components_core.base.fragment import Fragment + from reflex_components_core.core.cond import Cond + from reflex_components_core.core.foreach import Foreach + from reflex_components_core.core.match import Match + + no_valid_parents_defined = all(child._valid_parents == [] for child in children) + if ( + not self._invalid_children + and not self._valid_children + and no_valid_parents_defined + ): + return + + comp_name = type(self).__name__ + allowed_components = [ + comp.__name__ for comp in (Fragment, Foreach, Cond, Match) + ] + + def validate_child(child: Any): + child_name = type(child).__name__ + + # Iterate through the immediate children of fragment + if isinstance(child, Fragment): + for c in child.children: + validate_child(c) + + if isinstance(child, Cond): + validate_child(child.children[0]) + validate_child(child.children[1]) + + if isinstance(child, Match): + for cases in child.match_cases: + validate_child(cases[-1]) + validate_child(child.default) + + if self._invalid_children and child_name in self._invalid_children: + msg = f"The component `{comp_name}` cannot have `{child_name}` as a child component" + raise ValueError(msg) + + if self._valid_children and child_name not in [ + *self._valid_children, + *allowed_components, + ]: + valid_child_list = ", ".join([ + f"`{v_child}`" for v_child in self._valid_children + ]) + msg = f"The component `{comp_name}` only allows the components: {valid_child_list} as children. Got `{child_name}` instead." + raise ValueError(msg) + + if child._valid_parents and all( + clz_name not in [*child._valid_parents, *allowed_components] + for clz_name in self._iter_parent_classes_names() + ): + valid_parent_list = ", ".join([ + f"`{v_parent}`" for v_parent in child._valid_parents + ]) + msg = f"The component `{child_name}` can only be a child of the components: {valid_parent_list}. Got `{comp_name}` instead." + raise ValueError(msg) + + for child in children: + validate_child(child) + + @staticmethod + def _get_vars_from_event_triggers( + event_triggers: dict[str, EventChain | Var], + ) -> Iterator[tuple[str, list[Var]]]: + """Get the Vars associated with each event trigger. + + Args: + event_triggers: The event triggers from the component instance. + + Yields: + tuple of (event_name, event_vars) + """ + for event_trigger, event in event_triggers.items(): + if isinstance(event, Var): + yield event_trigger, [event] + elif isinstance(event, EventChain): + event_args = [] + for spec in event.events: + if isinstance(spec, EventSpec): + for args in spec.args: + event_args.extend(args) + else: + event_args.append(spec) + yield event_trigger, event_args + + def _get_vars( + self, include_children: bool = False, ignore_ids: set[int] | None = None + ) -> Iterator[Var]: + """Walk all Vars used in this component. + + Args: + include_children: Whether to include Vars from children. + ignore_ids: The ids to ignore. + + Yields: + Each var referenced by the component (props, styles, event handlers). + """ + ignore_ids = ignore_ids or set() + vars: list[Var] | None = getattr(self, "__vars", None) + if vars is not None: + yield from vars + vars = self.__vars = [] + # Get Vars associated with event trigger arguments. + for _, event_vars in self._get_vars_from_event_triggers(self.event_triggers): + vars.extend(event_vars) + + # Get Vars associated with component props. + for prop in self.get_props(): + prop_var = getattr(self, prop) + if isinstance(prop_var, Var): + vars.append(prop_var) + + # Style keeps track of its own VarData instance, so embed in a temp Var that is yielded. + if (isinstance(self.style, dict) and self.style) or isinstance(self.style, Var): + vars.append( + Var( + _js_expr="style", + _var_type=str, + _var_data=VarData.merge(self.style._var_data), + ) + ) + + # Special props are always Var instances. + vars.extend(self.special_props) + + # Get Vars associated with common Component props. + for comp_prop in ( + self.class_name, + self.id, + self.key, + *self.custom_attrs.values(), + ): + if isinstance(comp_prop, Var): + vars.append(comp_prop) + elif isinstance(comp_prop, str): + # Collapse VarData encoded in f-strings. + var = LiteralStringVar.create(comp_prop) + if var._get_all_var_data() is not None: + vars.append(var) + + # Get Vars associated with children. + if include_children: + for child in self.children: + if not isinstance(child, Component) or id(child) in ignore_ids: + continue + ignore_ids.add(id(child)) + child_vars = child._get_vars( + include_children=include_children, ignore_ids=ignore_ids + ) + vars.extend(child_vars) + + yield from vars + + def _event_trigger_values_use_state(self) -> bool: + """Check if the values of a component's event trigger use state. + + Returns: + True if any of the component's event trigger values uses State. + """ + for trigger in self.event_triggers.values(): + if isinstance(trigger, EventChain): + for event in trigger.events: + if isinstance(event, EventCallback): + continue + if isinstance(event, EventSpec): + if ( + event.handler.state_full_name + and event.handler.state_full_name != FRONTEND_EVENT_STATE + ): + return True + else: + if event._var_state: + return True + elif isinstance(trigger, Var) and trigger._var_state: + return True + return False + + def _has_stateful_event_triggers(self): + """Check if component or children have any event triggers that use state. + + Returns: + True if the component or children have any event triggers that uses state. + """ + if self.event_triggers and self._event_trigger_values_use_state(): + return True + for child in self.children: + if isinstance(child, Component) and child._has_stateful_event_triggers(): + return True + return False + + @classmethod + def _iter_parent_classes_names(cls) -> Iterator[str]: + for clz in cls.mro(): + if clz is Component: + break + yield clz.__name__ + + @classmethod + def _iter_parent_classes_with_method(cls, method: str) -> Sequence[type[Component]]: + """Iterate through parent classes that define a given method. + + Used for handling the `add_*` API functions that internally simulate a super() call chain. + + Args: + method: The method to look for. + + Returns: + A sequence of parent classes that define the method (differently than the base). + """ + current_class_method = getattr(Component, method, None) + seen_methods = ( + {current_class_method} if current_class_method is not None else set() + ) + clzs: list[type[Component]] = [] + for clz in cls.mro(): + if clz is Component: + break + if not issubclass(clz, Component): + continue + method_func = getattr(clz, method, None) + if not callable(method_func) or method_func in seen_methods: + continue + seen_methods.add(method_func) + clzs.append(clz) + return clzs + + def _get_custom_code(self) -> str | None: + """Get custom code for the component. + + Returns: + The custom code. + """ + return None + + def _get_all_custom_code(self) -> dict[str, None]: + """Get custom code for the component and its children. + + Returns: + The custom code. + """ + # Store the code in a set to avoid duplicates. + code: dict[str, None] = {} + + # Add the custom code for this component. + custom_code = self._get_custom_code() + if custom_code is not None: + code[custom_code] = None + + for component in self._get_components_in_props(): + code |= component._get_all_custom_code() + + # Add the custom code from add_custom_code method. + for clz in self._iter_parent_classes_with_method("add_custom_code"): + for item in clz.add_custom_code(self): + code[item] = None + + # Add the custom code for the children. + for child in self.children: + code |= child._get_all_custom_code() + + # Return the code. + return code + + def _get_dynamic_imports(self) -> str | None: + """Get dynamic import for the component. + + Returns: + The dynamic import. + """ + return None + + def _get_all_dynamic_imports(self) -> set[str]: + """Get dynamic imports for the component and its children. + + Returns: + The dynamic imports. + """ + # Store the import in a set to avoid duplicates. + dynamic_imports: set[str] = set() + + # Get dynamic import for this component. + dynamic_import = self._get_dynamic_imports() + if dynamic_import: + dynamic_imports.add(dynamic_import) + + # Get the dynamic imports from children + for child in self.children: + dynamic_imports |= child._get_all_dynamic_imports() + + for component in self._get_components_in_props(): + dynamic_imports |= component._get_all_dynamic_imports() + + # Return the dynamic imports + return dynamic_imports + + def _get_dependencies_imports(self) -> ParsedImportDict: + """Get the imports from lib_dependencies for installing. + + Returns: + The dependencies imports of the component. + """ + return { + dep: [ImportVar(tag=None, render=False)] for dep in self.lib_dependencies + } + + def _get_hooks_imports(self) -> ParsedImportDict: + """Get the imports required by certain hooks. + + Returns: + The imports required for all selected hooks. + """ + imports_ = {} + + if self._get_ref_hook() is not None: + # Handle hooks needed for attaching react refs to DOM nodes. + imports_.setdefault("react", set()).add(ImportVar(tag="useRef")) + imports_.setdefault(f"$/{Dirs.STATE_PATH}", set()).add( + ImportVar(tag="refs") + ) + + if self._get_mount_lifecycle_hook(): + # Handle hooks for `on_mount` / `on_unmount`. + imports_.setdefault("react", set()).add(ImportVar(tag="useEffect")) + + other_imports = [] + user_hooks = self._get_hooks() + user_hooks_data = ( + VarData.merge(user_hooks._get_all_var_data()) + if user_hooks is not None and isinstance(user_hooks, Var) + else None + ) + if user_hooks_data is not None: + other_imports.append(user_hooks_data.imports) + other_imports.extend( + hook_vardata.imports + for hook_vardata in self._get_added_hooks().values() + if hook_vardata is not None + ) + + return imports.merge_imports(imports_, *other_imports) + + def _get_imports(self) -> ParsedImportDict: + """Get all the libraries and fields that are used by the component. + + Returns: + The imports needed by the component. + """ + imports_ = ( + {self.library: [self.import_var]} + if self.library is not None and self.tag is not None + else {} + ) + + # Get static imports required for event processing. + event_imports = Imports.EVENTS if self.event_triggers else {} + + # Collect imports from Vars used directly by this component. + var_imports = [ + dict(var_data.imports) + for var in self._get_vars() + if (var_data := var._get_all_var_data()) is not None + ] + + added_import_dicts: list[ParsedImportDict] = [] + for clz in self._iter_parent_classes_with_method("add_imports"): + list_of_import_dict = clz.add_imports(self) + + if not isinstance(list_of_import_dict, list): + added_import_dicts.append(imports.parse_imports(list_of_import_dict)) + else: + added_import_dicts.extend([ + imports.parse_imports(item) for item in list_of_import_dict + ]) + + return imports.merge_parsed_imports( + self._get_dependencies_imports(), + self._get_hooks_imports(), + imports_, + event_imports, + *var_imports, + *added_import_dicts, + ) + + def _get_all_imports(self, collapse: bool = False) -> ParsedImportDict: + """Get all the libraries and fields that are used by the component and its children. + + Args: + collapse: Whether to collapse the imports by removing duplicates. + + Returns: + The import dict with the required imports. + """ + imports_ = imports.merge_parsed_imports( + self._get_imports(), *[child._get_all_imports() for child in self.children] + ) + return imports.collapse_imports(imports_) if collapse else imports_ + + def _get_mount_lifecycle_hook(self) -> str | None: + """Generate the component lifecycle hook. + + Returns: + The useEffect hook for managing `on_mount` and `on_unmount` events. + """ + # pop on_mount and on_unmount from event_triggers since these are handled by + # hooks, not as actually props in the component + on_mount = self.event_triggers.get(EventTriggers.ON_MOUNT, None) + on_unmount = self.event_triggers.get(EventTriggers.ON_UNMOUNT, None) + if on_mount is not None: + on_mount = str(LiteralVar.create(on_mount)) + "()" + if on_unmount is not None: + on_unmount = str(LiteralVar.create(on_unmount)) + "()" + if on_mount is not None or on_unmount is not None: + return f""" + useEffect(() => {{ + {on_mount or ""} + return () => {{ + {on_unmount or ""} + }} + }}, []);""" + return None + + def _get_ref_hook(self) -> Var | None: + """Generate the ref hook for the component. + + Returns: + The useRef hook for managing refs. + """ + ref = self.get_ref() + if ref is not None: + return Var( + f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};", + _var_data=VarData(position=Hooks.HookPosition.INTERNAL), + ) + return None + + def _get_vars_hooks(self) -> dict[str, VarData | None]: + """Get the hooks required by vars referenced in this component. + + Returns: + The hooks for the vars. + """ + vars_hooks = {} + for var in self._get_vars(): + var_data = var._get_all_var_data() + if var_data is not None: + vars_hooks.update( + var_data.hooks + if isinstance(var_data.hooks, dict) + else { + k: VarData(position=Hooks.HookPosition.INTERNAL) + for k in var_data.hooks + } + ) + for component in var_data.components: + vars_hooks.update(component._get_all_hooks()) + return vars_hooks + + def _get_events_hooks(self) -> dict[str, VarData | None]: + """Get the hooks required by events referenced in this component. + + Returns: + The hooks for the events. + """ + return ( + {Hooks.EVENTS: VarData(position=Hooks.HookPosition.INTERNAL)} + if self.event_triggers + else {} + ) + + def _get_hooks_internal(self) -> dict[str, VarData | None]: + """Get the React hooks for this component managed by the framework. + + Downstream components should NOT override this method to avoid breaking + framework functionality. + + Returns: + The internally managed hooks. + """ + return { + **{ + str(hook): VarData(position=Hooks.HookPosition.INTERNAL) + for hook in [self._get_ref_hook(), self._get_mount_lifecycle_hook()] + if hook is not None + }, + **self._get_vars_hooks(), + **self._get_events_hooks(), + } + + def _get_added_hooks(self) -> dict[str, VarData | None]: + """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): + var_data = VarData.merge(hook._get_all_var_data()) + if var_data is not None: + for sub_hook in var_data.hooks: + code[sub_hook] = None + + if str(hook) in code: + code[str(hook)] = VarData.merge(var_data, code[str(hook)]) + else: + code[str(hook)] = var_data + + # 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(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] = None + + return code + + def _get_hooks(self) -> str | None: + """Get the React hooks for this component. + + Downstream components should override this method to add their own hooks. + + Returns: + The hooks for just this component. + """ + return + + def _get_all_hooks_internal(self) -> dict[str, VarData | None]: + """Get the reflex internal hooks for the component and its children. + + Returns: + The code that should appear just before user-defined hooks. + """ + # Store the code in a set to avoid duplicates. + code = self._get_hooks_internal() + + # Add the hook code for the children. + for child in self.children: + code.update(child._get_all_hooks_internal()) + + return code + + def _get_all_hooks(self) -> dict[str, VarData | None]: + """Get the React hooks for this component and its children. + + Returns: + The code that should appear just before returning the rendered component. + """ + code = {} + + # Add the internal hooks for this component. + code.update(self._get_hooks_internal()) + + # Add the hook code for this component. + hooks = self._get_hooks() + if hooks is not None: + code[hooks] = None + + code.update(self._get_added_hooks()) + + # Add the hook code for the children. + for child in self.children: + code.update(child._get_all_hooks()) + + return code + + def get_ref(self) -> str | None: + """Get the name of the ref for the component. + + Returns: + The ref name. + """ + # do not create a ref if the id is dynamic or unspecified + if self.id is None or isinstance(self.id, Var): + return None + return format.format_ref(self.id) + + def _get_all_refs(self) -> dict[str, None]: + """Get the refs for the children of the component. + + Returns: + The refs for the children. + """ + refs = {} + ref = self.get_ref() + if ref is not None: + refs[ref] = None + for child in self.children: + refs |= child._get_all_refs() + for component in self._get_components_in_props(): + refs |= component._get_all_refs() + + return refs + + @property + def import_var(self): + """The tag to import. + + Returns: + An import var. + """ + # If the tag is dot-qualified, only import the left-most name. + tag = self.tag.partition(".")[0] if self.tag else None + alias = self.alias.partition(".")[0] if self.alias else None + return ImportVar(tag=tag, is_default=self.is_default, alias=alias) + + @staticmethod + def _get_app_wrap_components() -> dict[tuple[int, str], Component]: + """Get the app wrap components for the component. + + Returns: + The app wrap components. + """ + return {} + + def _get_all_app_wrap_components( + self, *, ignore_ids: set[int] | None = None + ) -> dict[tuple[int, str], Component]: + """Get the app wrap components for the component and its children. + + Args: + ignore_ids: A set of component IDs to ignore. Used to avoid duplicates. + + Returns: + The app wrap components. + """ + ignore_ids = ignore_ids or set() + # Store the components in a set to avoid duplicates. + components = self._get_app_wrap_components() + + for component in tuple(components.values()): + component_id = id(component) + if component_id in ignore_ids: + continue + ignore_ids.add(component_id) + components.update( + component._get_all_app_wrap_components(ignore_ids=ignore_ids) + ) + + # Add the app wrap components for the children. + for child in self.children: + child_id = id(child) + # Skip BaseComponent and StatefulComponent children. + if not isinstance(child, Component) or child_id in ignore_ids: + continue + ignore_ids.add(child_id) + components.update(child._get_all_app_wrap_components(ignore_ids=ignore_ids)) + + # Return the components. + return components + + +class CustomComponent(Component): + """A custom user-defined component.""" + + # Use the components library. + library = f"$/{Dirs.COMPONENTS_PATH}" + + component_fn: Callable[..., Component] = field( + doc="The function that creates the component.", default=Component.create + ) + + props: dict[str, Any] = field( + doc="The props of the component.", default_factory=dict + ) + + def _post_init(self, **kwargs): + """Initialize the custom component. + + Args: + **kwargs: The kwargs to pass to the component. + """ + component_fn = kwargs.get("component_fn") + + # Set the props. + props_types = typing.get_type_hints(component_fn) if component_fn else {} + props = {key: value for key, value in kwargs.items() if key in props_types} + kwargs = {key: value for key, value in kwargs.items() if key not in props_types} + + event_types = { + key + for key in props + if ( + (get_origin((annotation := props_types.get(key))) or annotation) + == EventHandler + ) + } + + def get_args_spec(key: str) -> types.ArgsSpec | Sequence[types.ArgsSpec]: + type_ = props_types[key] + + return ( + args[0] + if (args := get_args(type_)) + else ( + annotation_args[1] + if get_origin( + annotation := inspect.getfullargspec(component_fn).annotations[ + key + ] + ) + is typing.Annotated + and (annotation_args := get_args(annotation)) + else no_args_event_spec + ) + ) + + super()._post_init( + event_triggers={ + key: EventChain.create( + value=props[key], + args_spec=get_args_spec(key), + key=key, + ) + for key in event_types + }, + **kwargs, + ) + + to_camel_cased_props = { + format.to_camel_case(key): None for key in props if key not in event_types + } + self.get_props = lambda: to_camel_cased_props # pyright: ignore [reportIncompatibleVariableOverride] + + # Unset the style. + self.style = Style() + + # Set the tag to the name of the function. + self.tag = format.to_title_case(self.component_fn.__name__) + + for key, value in props.items(): + # Skip kwargs that are not props. + if key not in props_types: + continue + + camel_cased_key = format.to_camel_case(key) + + # Get the type based on the annotation. + type_ = props_types[key] + + # Handle event chains. + if type_ is EventHandler: + inspect.getfullargspec(component_fn).annotations[key] + self.props[camel_cased_key] = EventChain.create( + value=value, args_spec=get_args_spec(key), key=key + ) + continue + + value = LiteralVar.create(value) + self.props[camel_cased_key] = value + setattr(self, camel_cased_key, value) + + def __eq__(self, other: Any) -> bool: + """Check if the component is equal to another. + + Args: + other: The other component. + + Returns: + Whether the component is equal to the other. + """ + return isinstance(other, CustomComponent) and self.tag == other.tag + + def __hash__(self) -> int: + """Get the hash of the component. + + Returns: + The hash of the component. + """ + return hash(self.tag) + + @classmethod + def get_props(cls) -> Iterable[str]: + """Get the props for the component. + + Returns: + The set of component props. + """ + return () + + @staticmethod + def _get_event_spec_from_args_spec(name: str, event: EventChain) -> Callable: + """Get the event spec from the args spec. + + Args: + name: The name of the event + event: The args spec. + + Returns: + The event spec. + """ + + def fn(*args): + return run_script(Var(name).to(FunctionVar).call(*args)) + + if event.args_spec: + arg_spec = ( + event.args_spec + if not isinstance(event.args_spec, Sequence) + else event.args_spec[0] + ) + names = inspect.getfullargspec(arg_spec).args + fn.__signature__ = inspect.Signature( # pyright: ignore[reportFunctionMemberAccess] + parameters=[ + inspect.Parameter( + name=name, + kind=inspect.Parameter.POSITIONAL_ONLY, + annotation=arg._var_type, + ) + for name, arg in zip( + names, parse_args_spec(event.args_spec)[0], strict=True + ) + ] + ) + + return fn + + def get_prop_vars(self) -> list[Var | Callable]: + """Get the prop vars. + + Returns: + The prop vars. + """ + return [ + Var( + _js_expr=name + CAMEL_CASE_MEMO_MARKER, + _var_type=(prop._var_type if isinstance(prop, Var) else type(prop)), + ).guess_type() + if isinstance(prop, Var) or not isinstance(prop, EventChain) + else CustomComponent._get_event_spec_from_args_spec( + name + CAMEL_CASE_MEMO_MARKER, prop + ) + for name, prop in self.props.items() + ] + + @functools.cache # noqa: B019 + def get_component(self) -> Component: + """Render the component. + + Returns: + The code to render the component. + """ + component = self.component_fn(*self.get_prop_vars()) + + try: + from reflex.utils.prerequisites import get_and_validate_app + + style = get_and_validate_app().app.style + except Exception: + style = {} + + component._add_style_recursive(style) + return component + + def _get_all_app_wrap_components( + self, *, ignore_ids: set[int] | None = None + ) -> dict[tuple[int, str], Component]: + """Get the app wrap components for the custom component. + + Args: + ignore_ids: A set of IDs to ignore to avoid infinite recursion. + + Returns: + The app wrap components. + """ + ignore_ids = ignore_ids or set() + component = self.get_component() + if id(component) in ignore_ids: + return {} + ignore_ids.add(id(component)) + return self.get_component()._get_all_app_wrap_components(ignore_ids=ignore_ids) + + +CUSTOM_COMPONENTS: dict[str, CustomComponent] = {} + + +def _register_custom_component( + component_fn: Callable[..., Component], +): + """Register a custom component to be compiled. + + Args: + component_fn: The function that creates the component. + + Returns: + The custom component. + + Raises: + TypeError: If the tag name cannot be determined. + """ + dummy_props = { + prop: ( + Var( + "", + _var_type=unwrap_var_annotation(annotation), + ).guess_type() + if not types.safe_issubclass(annotation, EventHandler) + else EventSpec(handler=EventHandler(fn=no_args_event_spec)) + ) + for prop, annotation in typing.get_type_hints(component_fn).items() + if prop != "return" + } + dummy_component = CustomComponent._create( + children=[], + component_fn=component_fn, + **dummy_props, + ) + if dummy_component.tag is None: + msg = f"Could not determine the tag name for {component_fn!r}" + raise TypeError(msg) + CUSTOM_COMPONENTS[dummy_component.tag] = dummy_component + return dummy_component + + +def custom_component( + component_fn: Callable[..., Component], +) -> Callable[..., CustomComponent]: + """Create a custom component from a function. + + Args: + component_fn: The function that creates the component. + + Returns: + The decorated function. + """ + + @wraps(component_fn) + def wrapper(*children, **props) -> CustomComponent: + # Remove the children from the props. + props.pop("children", None) + return CustomComponent._create( + children=list(children), component_fn=component_fn, **props + ) + + # Register this component so it can be compiled. + dummy_component = _register_custom_component(component_fn) + if tag := dummy_component.tag: + object.__setattr__( + wrapper, + "_as_var", + lambda: Var( + tag, + _var_type=type[Component], + _var_data=VarData( + imports={ + f"$/{constants.Dirs.UTILS}/components": [ImportVar(tag=tag)], + "@emotion/react": [ + ImportVar(tag="jsx"), + ], + } + ), + ), + ) + + return wrapper + + +# Alias memo to custom_component. +memo = custom_component + + +class NoSSRComponent(Component): + """A dynamic component that is not rendered on the server.""" + + def _get_import_name(self) -> str | None: + if not self.library: + return None + return f"${self.library}" if self.library.startswith("/") else self.library + + def _get_imports(self) -> ParsedImportDict: + """Get the imports for the component. + + Returns: + The imports for dynamically importing the component at module load time. + """ + # React lazy import mechanism. + dynamic_import = { + f"$/{constants.Dirs.UTILS}/context": [ImportVar(tag="ClientSide")], + } + + # The normal imports for this component. + imports_ = super()._get_imports() + + # Do NOT import the main library/tag statically. + import_name = self._get_import_name() + if import_name is not None: + with contextlib.suppress(ValueError): + imports_[import_name].remove(self.import_var) + imports_[import_name].append(ImportVar(tag=None, render=False)) + + return imports.merge_imports( + dynamic_import, + imports_, + self._get_dependencies_imports(), + ) + + def _get_dynamic_imports(self) -> str: + # extract the correct import name from library name + base_import_name = self._get_import_name() + if base_import_name is None: + msg = "Undefined library for NoSSRComponent" + raise ValueError(msg) + import_name = format.format_library_name(base_import_name) + + library_import = f"import('{import_name}')" + mod_import = ( + # https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#with-named-exports + f".then((mod) => mod.{self.tag})" + if not self.is_default + else ".then((mod) => mod.default.default ?? mod.default)" + ) + return ( + f"const {self.alias or self.tag} = ClientSide(() => " + + library_import + + mod_import + + ")" + ) + + +class StatefulComponent(BaseComponent): + """A component that depends on state and is rendered outside of the page component. + + If a StatefulComponent is used in multiple pages, it will be rendered to a common file and + imported into each page that uses it. + + A stateful component has a tag name that includes a hash of the code that it renders + to. This tag name refers to the specific component with the specific props that it + was created with. + """ + + # A lookup table to caching memoized component instances. + tag_to_stateful_component: ClassVar[dict[str, StatefulComponent]] = {} + + # Reference to the original component that was memoized into this component. + component: Component = field( + default_factory=Component, is_javascript_property=False + ) + + references: int = field( + doc="How many times this component is referenced in the app.", + default=0, + is_javascript_property=False, + ) + + rendered_as_shared: bool = field( + doc="Whether the component has already been rendered to a shared file.", + default=False, + is_javascript_property=False, + ) + + memo_trigger_hooks: list[str] = field( + default_factory=list, is_javascript_property=False + ) + + @classmethod + def create(cls, component: Component) -> StatefulComponent | None: + """Create a stateful component from a component. + + Args: + component: The component to memoize. + + Returns: + The stateful component or None if the component should not be memoized. + """ + from reflex_components_core.core.foreach import Foreach + + if component._memoization_mode.disposition == MemoizationDisposition.NEVER: + # Never memoize this component. + return None + + if component.tag is None: + # Only memoize components with a tag. + return None + + # If _var_data is found in this component, it is a candidate for auto-memoization. + should_memoize = False + + # If the component requests to be memoized, then ignore other checks. + if component._memoization_mode.disposition == MemoizationDisposition.ALWAYS: + should_memoize = True + + if not should_memoize: + # Determine if any Vars have associated data. + for prop_var in component._get_vars(include_children=True): + if prop_var._get_all_var_data(): + should_memoize = True + break + + if not should_memoize: + # Check for special-cases in child components. + for child in component.children: + # Skip BaseComponent and StatefulComponent children. + if not isinstance(child, Component): + continue + # Always consider Foreach something that must be memoized by the parent. + if isinstance(child, Foreach): + should_memoize = True + break + child = cls._child_var(child) + if isinstance(child, Var) and child._get_all_var_data(): + should_memoize = True + break + + if should_memoize or component.event_triggers: + # Render the component to determine tag+hash based on component code. + tag_name = cls._get_tag_name(component) + if tag_name is None: + return None + + # Look up the tag in the cache + stateful_component = cls.tag_to_stateful_component.get(tag_name) + if stateful_component is None: + memo_trigger_hooks = cls._fix_event_triggers(component) + # Set the stateful component in the cache for the given tag. + stateful_component = cls.tag_to_stateful_component.setdefault( + tag_name, + cls( + children=component.children, + component=component, + tag=tag_name, + memo_trigger_hooks=memo_trigger_hooks, + ), + ) + # Bump the reference count -- multiple pages referencing the same component + # will result in writing it to a common file. + stateful_component.references += 1 + return stateful_component + + # Return None to indicate this component should not be memoized. + return None + + @staticmethod + def _child_var(child: Component) -> Var | Component: + """Get the Var from a child component. + + This method is used for special cases when the StatefulComponent should actually + wrap the parent component of the child instead of recursing into the children + and memoizing them independently. + + Args: + child: The child component. + + Returns: + The Var from the child component or the child itself (for regular cases). + """ + from reflex_components_core.base.bare import Bare + from reflex_components_core.core.cond import Cond + from reflex_components_core.core.foreach import Foreach + from reflex_components_core.core.match import Match + + if isinstance(child, Bare): + return child.contents + if isinstance(child, Cond): + return child.cond + if isinstance(child, Foreach): + return child.iterable + if isinstance(child, Match): + return child.cond + return child + + @classmethod + def _get_tag_name(cls, component: Component) -> str | None: + """Get the tag based on rendering the given component. + + Args: + component: The component to render. + + Returns: + The tag for the stateful component. + """ + # Get the render dict for the component. + rendered_code = component.render() + if not rendered_code: + # Never memoize non-visual components. + return None + + # Compute the hash based on the rendered code. + code_hash = _hash_str(_deterministic_hash(rendered_code)) + + # Format the tag name including the hash. + return format.format_state_name( + f"{component.tag or 'Comp'}_{code_hash}" + ).capitalize() + + def _render_stateful_code( + self, + export: bool = False, + ) -> str: + if not self.tag: + return "" + # Render the code for this component and hooks. + return stateful_component_template( + tag_name=self.tag, + memo_trigger_hooks=self.memo_trigger_hooks, + component=self.component, + export=export, + ) + + @classmethod + def _fix_event_triggers( + cls, + component: Component, + ) -> list[str]: + """Render the code for a stateful component. + + Args: + component: The component to render. + + Returns: + The memoized event trigger hooks for the component. + """ + # Memoize event triggers useCallback to avoid unnecessary re-renders. + memo_event_triggers = tuple(cls._get_memoized_event_triggers(component).items()) + + # Trigger hooks stored separately to write after the normal hooks (see stateful_component.js.jinja2) + memo_trigger_hooks: list[str] = [] + + if memo_event_triggers: + # Copy the component to avoid mutating the original. + component = copy.copy(component) + + for event_trigger, ( + memo_trigger, + memo_trigger_hook, + ) in memo_event_triggers: + # Replace the event trigger with the memoized version. + memo_trigger_hooks.append(memo_trigger_hook) + component.event_triggers[event_trigger] = memo_trigger + + return memo_trigger_hooks + + @staticmethod + def _get_hook_deps(hook: str) -> list[str]: + """Extract var deps from a hook. + + Args: + hook: The hook line to extract deps from. + + Returns: + A list of var names created by the hook declaration. + """ + # Ensure that the hook is a var declaration. + var_decl = hook.partition("=")[0].strip() + if not any(var_decl.startswith(kw) for kw in ["const ", "let ", "var "]): + return [] + + # Extract the var name from the declaration. + _, _, var_name = var_decl.partition(" ") + var_name = var_name.strip() + + # Break up array and object destructuring if used. + if var_name.startswith(("[", "{")): + return [ + v.strip().replace("...", "") for v in var_name.strip("[]{}").split(",") + ] + return [var_name] + + @staticmethod + def _get_deps_from_event_trigger( + event: EventChain | EventSpec | Var, + ) -> dict[str, None]: + """Get the dependencies accessed by event triggers. + + Args: + event: The event trigger to extract deps from. + + Returns: + The dependencies accessed by the event triggers. + """ + events: list = [event] + deps = {} + + if isinstance(event, EventChain): + events.extend(event.events) + + for ev in events: + if isinstance(ev, EventSpec): + for arg in ev.args: + for a in arg: + var_datas = VarData.merge(a._get_all_var_data()) + if var_datas and var_datas.deps is not None: + deps |= {str(dep): None for dep in var_datas.deps} + return deps + + @classmethod + def _get_memoized_event_triggers( + cls, + component: Component, + ) -> dict[str, tuple[Var, str]]: + """Memoize event handler functions with useCallback to avoid unnecessary re-renders. + + Args: + component: The component with events to memoize. + + Returns: + A dict of event trigger name to a tuple of the memoized event trigger Var and + the hook code that memoizes the event handler. + """ + trigger_memo = {} + for event_trigger, event_args in component._get_vars_from_event_triggers( + component.event_triggers + ): + if event_trigger in { + EventTriggers.ON_MOUNT, + EventTriggers.ON_UNMOUNT, + EventTriggers.ON_SUBMIT, + }: + # Do not memoize lifecycle or submit events. + continue + + # Get the actual EventSpec and render it. + event = component.event_triggers[event_trigger] + rendered_chain = str(LiteralVar.create(event)) + + # Hash the rendered EventChain to get a deterministic function name. + chain_hash = md5(str(rendered_chain).encode("utf-8")).hexdigest() + memo_name = f"{event_trigger}_{chain_hash}" + + # Calculate Var dependencies accessed by the handler for useCallback dep array. + var_deps = ["addEvents", "ReflexEvent"] + + # Get deps from event trigger var data. + var_deps.extend(cls._get_deps_from_event_trigger(event)) + + # Get deps from hooks. + for arg in event_args: + var_data = arg._get_all_var_data() + if var_data is None: + continue + for hook in var_data.hooks: + var_deps.extend(cls._get_hook_deps(hook)) + memo_var_data = VarData.merge( + *[var._get_all_var_data() for var in event_args], + VarData( + imports={"react": [ImportVar(tag="useCallback")]}, + ), + ) + + # Store the memoized function name and hook code for this event trigger. + trigger_memo[event_trigger] = ( + Var(_js_expr=memo_name)._replace( + _var_type=EventChain, merge_var_data=memo_var_data + ), + f"const {memo_name} = useCallback({rendered_chain}, [{', '.join(var_deps)}])", + ) + return trigger_memo + + def _get_all_hooks_internal(self) -> dict[str, VarData | None]: + """Get the reflex internal hooks for the component and its children. + + Returns: + The code that should appear just before user-defined hooks. + """ + return {} + + def _get_all_hooks(self) -> dict[str, VarData | None]: + """Get the React hooks for this component. + + Returns: + The code that should appear just before returning the rendered component. + """ + return {} + + def _get_all_imports(self) -> ParsedImportDict: + """Get all the libraries and fields that are used by the component. + + Returns: + The import dict with the required imports. + """ + if self.rendered_as_shared: + return { + f"$/{Dirs.UTILS}/{PageNames.STATEFUL_COMPONENTS}": [ + ImportVar(tag=self.tag) + ] + } + return self.component._get_all_imports() + + def _get_all_dynamic_imports(self) -> set[str]: + """Get dynamic imports for the component. + + Returns: + The dynamic imports. + """ + if self.rendered_as_shared: + return set() + return self.component._get_all_dynamic_imports() + + def _get_all_custom_code(self, export: bool = False) -> dict[str, None]: + """Get custom code for the component. + + Args: + export: Whether to export the component. + + Returns: + The custom code. + """ + if self.rendered_as_shared: + return {} + return self.component._get_all_custom_code() | ({ + self._render_stateful_code(export=export): None + }) + + def _get_all_refs(self) -> dict[str, None]: + """Get the refs for the children of the component. + + Returns: + The refs for the children. + """ + if self.rendered_as_shared: + return {} + return self.component._get_all_refs() + + def render(self) -> dict: + """Define how to render the component in React. + + Returns: + The tag to render. + """ + return dict(Tag(name=self.tag or "")) + + def __str__(self) -> str: + """Represent the component in React. + + Returns: + The code to render the component. + """ + from reflex.compiler.compiler import _compile_component + + return _compile_component(self) + + @classmethod + def compile_from(cls, component: BaseComponent) -> BaseComponent: + """Walk through the component tree and memoize all stateful components. + + Args: + component: The component to memoize. + + Returns: + The memoized component tree. + """ + if isinstance(component, Component): + if component._memoization_mode.recursive: + # Recursively memoize stateful children (default). + component.children = [ + cls.compile_from(child) for child in component.children + ] + # Memoize this component if it depends on state. + stateful_component = cls.create(component) + if stateful_component is not None: + return stateful_component + return component + + +class MemoizationLeaf(Component): + """A component that does not separately memoize its children. + + Any component which depends on finding the exact names of children + components within it, should be a memoization leaf so the compiler + does not replace the provided child tags with memoized tags. + + During creation, a memoization leaf will mark itself as wanting to be + memoized if any of its children return any hooks. + """ + + _memoization_mode = MemoizationMode(recursive=False) + + @classmethod + def create(cls, *children, **props) -> Component: + """Create a new memoization leaf component. + + Args: + *children: The children of the component. + **props: The props of the component. + + Returns: + The memoization leaf + """ + comp = super().create(*children, **props) + if comp._get_all_hooks(): + comp._memoization_mode = dataclasses.replace( + comp._memoization_mode, disposition=MemoizationDisposition.ALWAYS + ) + return comp + + +load_dynamic_serializer() + + +class ComponentVar(Var[Component], python_types=BaseComponent): + """A Var that represents a Component.""" + + +def empty_component() -> Component: + """Create an empty component. + + Returns: + An empty component. + """ + from reflex_components_core.base.bare import Bare + + return Bare.create("") + + +def render_dict_to_var(tag: dict | Component | str) -> Var: + """Convert a render dict to a Var. + + Args: + tag: The render dict. + + Returns: + The Var. + """ + if not isinstance(tag, dict): + if isinstance(tag, Component): + return render_dict_to_var(tag.render()) + return Var.create(tag) + + if "contents" in tag: + return Var(tag["contents"]) + + if "iterable" in tag: + function_return = LiteralArrayVar.create([ + render_dict_to_var(child.render()) for child in tag["children"] + ]) + + func = ArgsFunctionOperation.create( + (tag["arg_var_name"], tag["index_var_name"]), + function_return, + ) + + return FunctionStringVar.create("Array.prototype.map.call").call( + tag["iterable"] + if not isinstance(tag["iterable"], ObjectVar) + else tag["iterable"].items(), + func, + ) + + if "match_cases" in tag: + element = Var(tag["cond"]) + + conditionals = render_dict_to_var(tag["default"]) + + for case in tag["match_cases"][::-1]: + conditions, return_value = case + condition = Var.create(False) + for pattern in conditions: + condition = condition | ( + Var(pattern).to_string() == element.to_string() + ) + + conditionals = ternary_operation( + condition, + render_dict_to_var(return_value), + conditionals, + ) + + return conditionals + + if "cond_state" in tag: + return ternary_operation( + Var(tag["cond_state"]), + render_dict_to_var(tag["true_value"]), + render_dict_to_var(tag["false_value"]) + if tag["false_value"] is not None + else LiteralNoneVar.create(), + ) + + props = Var("({" + ",".join(tag["props"]) + "})") + + raw_tag_name = tag.get("name") + tag_name = Var(raw_tag_name or "Fragment") + + return FunctionStringVar.create( + "jsx", + ).call( + tag_name, + props, + *[render_dict_to_var(child) for child in tag["children"]], + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar): + """A Var that represents a Component.""" + + _var_value: BaseComponent = dataclasses.field(default_factory=empty_component) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """Get the name of the var. + + Returns: + The name of the var. + """ + return str(render_dict_to_var(self._var_value.render())) + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> VarData | None: + """Get the VarData for the var. + + Returns: + The VarData for the var. + """ + return VarData.merge( + self._var_data, + VarData( + imports={ + "@emotion/react": ["jsx"], + "react": ["Fragment"], + }, + ), + VarData( + imports=self._var_value._get_all_imports(), + ), + ) + + def __hash__(self) -> int: + """Get the hash of the var. + + Returns: + The hash of the var. + """ + return hash((type(self).__name__, self._js_expr)) + + @classmethod + def create( + cls, + value: Component, + _var_data: VarData | None = None, + ): + """Create a var from a value. + + Args: + value: The value of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + var_datas = [ + var_data + for var in value._get_vars(include_children=True) + if (var_data := var._get_all_var_data()) + ] + + return LiteralComponentVar( + _js_expr="", + _var_type=type(value), + _var_data=VarData.merge( + _var_data, + *var_datas, + VarData( + components=(value,), + ), + ), + _var_value=value, + ) diff --git a/packages/reflex-core/src/reflex_core/components/dynamic.py b/packages/reflex-core/src/reflex_core/components/dynamic.py new file mode 100644 index 00000000000..c207f3e24f7 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/dynamic.py @@ -0,0 +1,216 @@ +"""Components that are dynamically generated on the backend.""" + +from typing import TYPE_CHECKING, Union + +from reflex_core import constants +from reflex_core.utils import imports +from reflex_core.utils.exceptions import DynamicComponentMissingLibraryError +from reflex_core.utils.format import format_library_name +from reflex_core.utils.serializers import serializer +from reflex_core.vars import Var, get_unique_variable_name +from reflex_core.vars.base import VarData, transform + +if TYPE_CHECKING: + from reflex_core.components.component import Component + + +def get_cdn_url(lib: str) -> str: + """Get the CDN URL for a library. + + Args: + lib: The library to get the CDN URL for. + + Returns: + The CDN URL for the library. + """ + return f"https://cdn.jsdelivr.net/npm/{lib}" + "/+esm" + + +bundled_libraries = [ + "react", + "@radix-ui/themes", + "@emotion/react", + f"$/{constants.Dirs.UTILS}/context", + f"$/{constants.Dirs.UTILS}/state", + f"$/{constants.Dirs.UTILS}/components", +] + + +def bundle_library(component: Union["Component", str]): + """Bundle a library with the component. + + Args: + component: The component to bundle the library with. + + Raises: + DynamicComponentMissingLibraryError: Raised when a dynamic component is missing a library. + """ + if isinstance(component, str): + bundled_libraries.append(component) + return + if component.library is None: + msg = "Component must have a library to bundle." + raise DynamicComponentMissingLibraryError(msg) + bundled_libraries.append(format_library_name(component.library)) + + +def load_dynamic_serializer(): + """Load the serializer for dynamic components.""" + # Causes a circular import, so we import here. + from reflex_core.components.component import Component + + @serializer + def make_component(component: Component) -> str: + """Generate the code for a dynamic component. + + Args: + component: The component to generate code for. + + Returns: + The generated code + """ + # Causes a circular import, so we import here. + from reflex_components_core.base.bare import Bare + + from reflex.compiler import compiler, templates, utils + + component = Bare.create(Var.create(component)) + + rendered_components = {} + # Include dynamic imports in the shared component. + if dynamic_imports := component._get_all_dynamic_imports(): + rendered_components.update(dict.fromkeys(dynamic_imports)) + + # Include custom code in the shared component. + rendered_components.update(component._get_all_custom_code()) + + rendered_components[ + templates.stateful_component_template( + tag_name="MySSRComponent", + memo_trigger_hooks=[], + component=component, + export=True, + ) + ] = None + + libs_in_window = bundled_libraries + + component_imports = component._get_all_imports() + compiler._apply_common_imports(component_imports) + + imports = {} + for lib, names in component_imports.items(): + formatted_lib_name = format_library_name(lib) + if ( + not lib.startswith((".", "/", "$/")) + and not lib.startswith("http") + and formatted_lib_name not in libs_in_window + ): + imports[get_cdn_url(lib)] = names + else: + imports[lib] = names + + module_code_lines = templates.stateful_components_template( + imports=utils.compile_imports(imports), + memoized_code="\n".join(rendered_components), + ).splitlines() + + # Rewrite imports from `/` to destructure from window + for ix, line in enumerate(module_code_lines[:]): + if line.startswith("import "): + if 'from "$/' in line or 'from "/' in line: + module_code_lines[ix] = ( + line + .replace("import ", "const ", 1) + .replace(" as ", ": ") + .replace(" from ", " = window['__reflex'][", 1) + + "]" + ) + else: + for lib in libs_in_window: + if f'from "{lib}"' in line: + module_code_lines[ix] = ( + line + .replace("import ", "const ", 1) + .replace( + f' from "{lib}"', f" = window.__reflex['{lib}']", 1 + ) + .replace(" as ", ": ") + ) + if line.startswith("export function"): + module_code_lines[ix] = line.replace( + "export function", "export default function", 1 + ) + line_stripped = line.strip() + if line_stripped.startswith("{") and line_stripped.endswith("}"): + module_code_lines[ix] = line_stripped[1:-1] + + module_code_lines.insert(0, "const React = window.__reflex.react;") + + function_line = next( + index + for index, line in enumerate(module_code_lines) + if line.startswith("export default function") + ) + + module_code_lines = [ + line + for _, line in sorted( + enumerate(module_code_lines), + key=lambda x: ( + not (x[1].startswith("import ") and x[0] < function_line), + x[0], + ), + ) + ] + + return "\n".join([ + "//__reflex_evaluate", + *module_code_lines, + ]) + + @transform + def evaluate_component(js_string: Var[str]) -> Var[Component]: + """Evaluate a component. + + Args: + js_string: The JavaScript string to evaluate. + + Returns: + The evaluated JavaScript string. + """ + unique_var_name = get_unique_variable_name() + + return js_string._replace( + _js_expr=unique_var_name, + _var_type=Component, + merge_var_data=VarData.merge( + VarData( + imports={ + f"$/{constants.Dirs.STATE_PATH}": [ + imports.ImportVar(tag="evalReactComponent"), + ], + "react": [ + imports.ImportVar(tag="useState"), + imports.ImportVar(tag="useEffect"), + ], + }, + hooks={ + f"const [{unique_var_name}, set_{unique_var_name}] = useState(null);": None, + "useEffect(() => {" + "let isMounted = true;" + f"evalReactComponent({js_string!s})" + ".then((component) => {" + "if (isMounted) {" + f"set_{unique_var_name}(component);" + "}" + "});" + "return () => {" + "isMounted = false;" + "};" + "}" + f", [{js_string!s}]);": None, + }, + ), + ), + ) diff --git a/packages/reflex-core/src/reflex_core/components/field.py b/packages/reflex-core/src/reflex_core/components/field.py new file mode 100644 index 00000000000..9b2bf2dfec8 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/field.py @@ -0,0 +1,183 @@ +"""Shared field infrastructure for components and props.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import _MISSING_TYPE, MISSING +from typing import Annotated, Any, Generic, TypeVar, get_origin + +from reflex_core.utils import types +from reflex_core.utils.compat import annotations_from_namespace + +FIELD_TYPE = TypeVar("FIELD_TYPE") + + +class BaseField(Generic[FIELD_TYPE]): + """Base field class used by internal metadata classes.""" + + def __init__( + self, + default: FIELD_TYPE | _MISSING_TYPE = MISSING, + default_factory: Callable[[], FIELD_TYPE] | None = None, + annotated_type: type[Any] | _MISSING_TYPE = MISSING, + ) -> None: + """Initialize the field. + + Args: + default: The default value for the field. + default_factory: The default factory for the field. + annotated_type: The annotated type for the field. + """ + self.default = default + self.default_factory = default_factory + self.outer_type_ = self.annotated_type = annotated_type + + # Process type annotation + type_origin = get_origin(annotated_type) or annotated_type + if type_origin is Annotated: + type_origin = annotated_type.__origin__ # pyright: ignore [reportAttributeAccessIssue] + # For Annotated types, use the actual type inside the annotation + self.type_ = annotated_type + else: + # For other types (including Union), preserve the original type + self.type_ = annotated_type + self.type_origin = type_origin + + def default_value(self) -> FIELD_TYPE: + """Get the default value for the field. + + Returns: + The default value for the field. + + Raises: + ValueError: If no default value or factory is provided. + """ + if self.default is not MISSING: + return self.default + if self.default_factory is not None: + return self.default_factory() + msg = "No default value or factory provided." + raise ValueError(msg) + + +class FieldBasedMeta(type): + """Shared metaclass for field-based classes like components and props. + + Provides common field inheritance and processing logic for both + PropsBaseMeta and BaseComponentMeta. + """ + + def __new__( + cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any] + ) -> type: + """Create a new field-based class. + + Args: + name: The name of the class. + bases: The base classes. + namespace: The class namespace. + + Returns: + The new class. + """ + # Collect inherited fields from base classes + inherited_fields = cls._collect_inherited_fields(bases) + + # Get annotations from the namespace + annotations = cls._resolve_annotations(namespace, name) + + # Process field overrides (fields with values but no annotations) + own_fields = cls._process_field_overrides( + namespace, annotations, inherited_fields + ) + + # Process annotated fields + own_fields.update( + cls._process_annotated_fields(namespace, annotations, inherited_fields) + ) + + # Finalize fields and store on class + cls._finalize_fields(namespace, inherited_fields, own_fields) + + return super().__new__(cls, name, bases, namespace) + + @classmethod + def _collect_inherited_fields(cls, bases: tuple[type, ...]) -> dict[str, Any]: + inherited_fields: dict[str, Any] = {} + + # Collect inherited fields from base classes + for base in bases[::-1]: + if hasattr(base, "_inherited_fields"): + inherited_fields.update(base._inherited_fields) + for base in bases[::-1]: + if hasattr(base, "_own_fields"): + inherited_fields.update(base._own_fields) + + return inherited_fields + + @classmethod + def _resolve_annotations( + cls, namespace: dict[str, Any], name: str + ) -> dict[str, Any]: + return types.resolve_annotations( + annotations_from_namespace(namespace), + namespace["__module__"], + ) + + @classmethod + def _process_field_overrides( + cls, + namespace: dict[str, Any], + annotations: dict[str, Any], + inherited_fields: dict[str, Any], + ) -> dict[str, Any]: + own_fields: dict[str, Any] = {} + + for key, value in namespace.items(): + if key not in annotations and key in inherited_fields: + inherited_field = inherited_fields[key] + new_field = cls._create_field( + annotated_type=inherited_field.annotated_type, + default=value, + default_factory=None, + ) + own_fields[key] = new_field + + return own_fields + + @classmethod + def _process_annotated_fields( + cls, + namespace: dict[str, Any], + annotations: dict[str, Any], + inherited_fields: dict[str, Any], + ) -> dict[str, Any]: + raise NotImplementedError + + @classmethod + def _create_field( + cls, + annotated_type: Any, + default: Any = MISSING, + default_factory: Callable[[], Any] | None = None, + ) -> Any: + raise NotImplementedError + + @classmethod + def _finalize_fields( + cls, + namespace: dict[str, Any], + inherited_fields: dict[str, Any], + own_fields: dict[str, Any], + ) -> None: + # Combine all fields + all_fields = inherited_fields | own_fields + + # Set field names for compatibility + for field_name, field in all_fields.items(): + field._name = field_name + + # Store field mappings on the class + namespace["_own_fields"] = own_fields + namespace["_inherited_fields"] = inherited_fields + namespace["_fields"] = all_fields diff --git a/packages/reflex-core/src/reflex_core/components/literals.py b/packages/reflex-core/src/reflex_core/components/literals.py new file mode 100644 index 00000000000..0616677acdf --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/literals.py @@ -0,0 +1,34 @@ +"""Literal custom type used by Reflex.""" + +from typing import Literal + +# Base Literals +LiteralInputType = Literal[ + "button", + "checkbox", + "color", + "date", + "datetime-local", + "email", + "file", + "hidden", + "image", + "month", + "number", + "password", + "radio", + "range", + "reset", + "search", + "submit", + "tel", + "text", + "time", + "url", + "week", +] + + +LiteralRowMarker = Literal[ + "none", "number", "checkbox", "both", "checkbox-visible", "clickable-number" +] diff --git a/packages/reflex-core/src/reflex_core/components/props.py b/packages/reflex-core/src/reflex_core/components/props.py new file mode 100644 index 00000000000..2ce49c4f7b8 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/props.py @@ -0,0 +1,441 @@ +"""A class that holds props to be passed or applied to a component.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import _MISSING_TYPE, MISSING +from typing import Any, TypeVar, get_args, get_origin + +from typing_extensions import dataclass_transform + +from reflex_core.components.field import BaseField, FieldBasedMeta +from reflex_core.event import EventChain, args_specs_from_fields +from reflex_core.utils import format +from reflex_core.utils.exceptions import InvalidPropValueError +from reflex_core.utils.serializers import serializer +from reflex_core.utils.types import is_union +from reflex_core.vars.object import LiteralObjectVar + +PROPS_FIELD_TYPE = TypeVar("PROPS_FIELD_TYPE") + + +def _get_props_subclass(field_type: Any) -> type | None: + """Extract the Props subclass from a field type annotation. + + Args: + field_type: The type annotation to check. + + Returns: + The Props subclass if found, None otherwise. + """ + from reflex_core.utils.types import typehint_issubclass + + # For direct class types, we can return them directly if they're Props subclasses + if isinstance(field_type, type): + return field_type if typehint_issubclass(field_type, PropsBase) else None + + # For Union types, check each union member + if is_union(field_type): + for arg in get_args(field_type): + result = _get_props_subclass(arg) + if result is not None: + return result + + return None + + +def _find_props_in_list_annotation(field_type: Any) -> type | None: + """Find Props subclass within a list type annotation. + + Args: + field_type: The type annotation to check (e.g., list[SomeProps] or list[SomeProps] | None). + + Returns: + The Props subclass if found in a list annotation, None otherwise. + """ + origin = get_origin(field_type) + if origin is list: + args = get_args(field_type) + if args: + return _get_props_subclass(args[0]) + + # Handle Union types - check if any union member is a list + if is_union(field_type): + for arg in get_args(field_type): + if arg is not type(None): # Skip None from Optional + list_element = _find_props_in_list_annotation(arg) + if list_element is not None: + return list_element + + return None + + +class PropsField(BaseField[PROPS_FIELD_TYPE]): + """A field for a props class.""" + + def __init__( + self, + default: PROPS_FIELD_TYPE | _MISSING_TYPE = MISSING, + default_factory: Callable[[], PROPS_FIELD_TYPE] | None = None, + annotated_type: type[Any] | _MISSING_TYPE = MISSING, + ) -> None: + """Initialize the field. + + Args: + default: The default value for the field. + default_factory: The default factory for the field. + annotated_type: The annotated type for the field. + """ + super().__init__(default, default_factory, annotated_type) + self._name: str = "" # Will be set by metaclass + + @property + def required(self) -> bool: + """Check if the field is required (for Pydantic compatibility). + + Returns: + True if the field has no default value or factory. + """ + return self.default is MISSING and self.default_factory is None + + @property + def name(self) -> str | None: + """Field name (for Pydantic compatibility). + + Note: This is set by the metaclass when processing fields. + + Returns: + The field name if set, None otherwise. + """ + return getattr(self, "_name", None) + + def get_default(self) -> Any: + """Get the default value (for Pydantic compatibility). + + Returns: + The default value for the field, or None if required. + """ + try: + return self.default_value() + except ValueError: + # Field is required (no default) + return None + + def __repr__(self) -> str: + """Represent the field in a readable format. + + Returns: + The string representation of the field. + """ + annotated_type_str = ( + f", annotated_type={self.annotated_type!r}" + if self.annotated_type is not MISSING + else "" + ) + if self.default is not MISSING: + return f"PropsField(default={self.default!r}{annotated_type_str})" + return ( + f"PropsField(default_factory={self.default_factory!r}{annotated_type_str})" + ) + + +def props_field( + default: PROPS_FIELD_TYPE | _MISSING_TYPE = MISSING, + default_factory: Callable[[], PROPS_FIELD_TYPE] | None = None, +) -> PROPS_FIELD_TYPE: + """Create a field for a props class. + + Args: + default: The default value for the field. + default_factory: The default factory for the field. + + Returns: + The field for the props class. + + Raises: + ValueError: If both default and default_factory are specified. + """ + if default is not MISSING and default_factory is not None: + msg = "cannot specify both default and default_factory" + raise ValueError(msg) + return PropsField( # pyright: ignore [reportReturnType] + default=default, + default_factory=default_factory, + annotated_type=MISSING, + ) + + +@dataclass_transform(field_specifiers=(props_field,)) +class PropsBaseMeta(FieldBasedMeta): + """Meta class for PropsBase.""" + + @classmethod + def _process_annotated_fields( + cls, + namespace: dict[str, Any], + annotations: dict[str, Any], + inherited_fields: dict[str, PropsField], + ) -> dict[str, PropsField]: + own_fields: dict[str, PropsField] = {} + + for key, annotation in annotations.items(): + value = namespace.get(key, MISSING) + + if value is MISSING: + # Field with only annotation, no default value + field = PropsField(annotated_type=annotation, default=None) + elif not isinstance(value, PropsField): + # Field with default value + field = PropsField(annotated_type=annotation, default=value) + else: + # Field is already a PropsField, update annotation + field = PropsField( + annotated_type=annotation, + default=value.default, + default_factory=value.default_factory, + ) + + own_fields[key] = field + + return own_fields + + @classmethod + def _create_field( + cls, + annotated_type: Any, + default: Any = MISSING, + default_factory: Callable[[], Any] | None = None, + ) -> PropsField: + return PropsField( + annotated_type=annotated_type, + default=default, + default_factory=default_factory, + ) + + @classmethod + def _finalize_fields( + cls, + namespace: dict[str, Any], + inherited_fields: dict[str, PropsField], + own_fields: dict[str, PropsField], + ) -> None: + # Call parent implementation + super()._finalize_fields(namespace, inherited_fields, own_fields) + + # Add Pydantic compatibility + namespace["__fields__"] = namespace["_fields"] + + +class PropsBase(metaclass=PropsBaseMeta): + """Base for a class containing props that can be serialized as a JS object.""" + + def __init__(self, **kwargs): + """Initialize the props with field values. + + Args: + **kwargs: The field values to set. + """ + # Set field values from kwargs with nested object instantiation + for key, value in kwargs.items(): + field_info = self.get_fields().get(key) + if field_info: + field_type = field_info.annotated_type + + # Check if this field expects a specific Props type and we got a dict + if isinstance(value, dict): + props_class = _get_props_subclass(field_type) + if props_class is not None: + value = props_class(**value) + + # Check if this field expects a list of Props and we got a list of dicts + elif isinstance(value, list): + element_type = _find_props_in_list_annotation(field_type) + if element_type is not None: + # Convert each dict in the list to the appropriate Props class + value = [ + element_type(**item) if isinstance(item, dict) else item + for item in value + ] + + setattr(self, key, value) + + # Set default values for fields not provided + for field_name, field in self.get_fields().items(): + if field_name not in kwargs: + if field.default is not MISSING: + setattr(self, field_name, field.default) + elif field.default_factory is not None: + setattr(self, field_name, field.default_factory()) + # Note: Fields with no default and no factory remain unset (required fields) + + # Convert EventHandler to EventChain + args_specs = args_specs_from_fields(self.get_fields()) + for handler_name, args_spec in args_specs.items(): + if (handler := getattr(self, handler_name, None)) is not None: + setattr( + self, + handler_name, + EventChain.create( + value=handler, + args_spec=args_spec, + key=handler_name, + ), + ) + + @classmethod + def get_fields(cls) -> dict[str, Any]: + """Get the fields of the object. + + Returns: + The fields of the object. + """ + return getattr(cls, "_fields", {}) + + 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 LiteralObjectVar.create({ + format.to_camel_case(key): value for key, value in self.dict().items() + }).json() + + def dict( + self, + exclude_none: bool = True, + include: set[str] | None = None, + exclude: set[str] | None = None, + **kwargs, + ): + """Convert the object to a dictionary. + + Keys will be converted to camelCase. + By default, None values are excluded (exclude_none=True). + + Args: + exclude_none: Whether to exclude None values. + include: Fields to include in the output. + exclude: Fields to exclude from the output. + **kwargs: Additional keyword arguments (for compatibility). + + Returns: + The object as a dictionary. + """ + result = {} + + for field_name in self.get_fields(): + if hasattr(self, field_name): + value = getattr(self, field_name) + + # Apply include/exclude filters + if include is not None and field_name not in include: + continue + if exclude is not None and field_name in exclude: + continue + + # Apply exclude_none logic + if exclude_none and value is None: + continue + + # Recursively convert nested structures + value = self._convert_to_camel_case( + value, exclude_none, include, exclude + ) + + # Convert key to camelCase + camel_key = format.to_camel_case(field_name) + result[camel_key] = value + + return result + + def _convert_to_camel_case( + self, + value: Any, + exclude_none: bool = True, + include: set[str] | None = None, + exclude: set[str] | None = None, + ) -> Any: + """Recursively convert nested dictionaries and lists to camelCase. + + Args: + value: The value to convert. + exclude_none: Whether to exclude None values. + include: Fields to include in the output. + exclude: Fields to exclude from the output. + + Returns: + The converted value with camelCase keys. + """ + if isinstance(value, PropsBase): + # Convert nested PropsBase objects + return value.dict( + exclude_none=exclude_none, include=include, exclude=exclude + ) + if isinstance(value, dict): + # Convert dictionary keys to camelCase + return { + format.to_camel_case(k): self._convert_to_camel_case( + v, exclude_none, include, exclude + ) + for k, v in value.items() + if not (exclude_none and v is None) + } + if isinstance(value, (list, tuple)): + # Convert list/tuple items recursively + return [ + self._convert_to_camel_case(item, exclude_none, include, exclude) + for item in value + ] + # Return primitive values as-is + return value + + +@serializer(to=dict) +def serialize_props_base(value: PropsBase) -> dict: + """Serialize a PropsBase instance. + + Unlike serialize_base, this preserves callables (lambdas) since they're + needed for AG Grid and other components that process them on the frontend. + + Args: + value: The PropsBase instance to serialize. + + Returns: + Dictionary representation of the PropsBase instance. + """ + return value.dict() + + +class NoExtrasAllowedProps(PropsBase): + """A class that holds props to be passed or applied to a component with no extra props allowed.""" + + def __init__(self, component_name: str | None = None, **kwargs): + """Initialize the props with validation. + + Args: + component_name: The custom name of the component. + kwargs: Kwargs to initialize the props. + + Raises: + InvalidPropValueError: If invalid props are passed on instantiation. + """ + component_name = component_name or type(self).__name__ + + # Validate fields BEFORE setting them + known_fields = set(self.__class__.get_fields().keys()) + provided_fields = set(kwargs.keys()) + invalid_fields = provided_fields - known_fields + + if invalid_fields: + invalid_fields_str = ", ".join(invalid_fields) + supported_props_str = ", ".join(f'"{field}"' for field in known_fields) + msg = f"Invalid prop(s) {invalid_fields_str} for {component_name!r}. Supported props are {supported_props_str}" + raise InvalidPropValueError(msg) + + # Use parent class initialization after validation + super().__init__(**kwargs) diff --git a/packages/reflex-core/src/reflex_core/components/tags/__init__.py b/packages/reflex-core/src/reflex_core/components/tags/__init__.py new file mode 100644 index 00000000000..993da11fe69 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/tags/__init__.py @@ -0,0 +1,6 @@ +"""Representations for React tags.""" + +from .cond_tag import CondTag +from .iter_tag import IterTag +from .match_tag import MatchTag +from .tag import Tag diff --git a/packages/reflex-core/src/reflex_core/components/tags/cond_tag.py b/packages/reflex-core/src/reflex_core/components/tags/cond_tag.py new file mode 100644 index 00000000000..b101871e9e3 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/tags/cond_tag.py @@ -0,0 +1,31 @@ +"""Tag to conditionally render components.""" + +import dataclasses +from collections.abc import Iterator, Mapping +from typing import Any + +from reflex_core.components.tags.tag import Tag + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class CondTag(Tag): + """A conditional tag.""" + + # The condition to determine which component to render. + cond_state: str + + # The code to render if the condition is true. + true_value: Mapping + + # The code to render if the condition is false. + false_value: Mapping | None = None + + def __iter__(self) -> Iterator[tuple[str, Any]]: + """Iterate over the tag's attributes. + + Yields: + An iterator over the tag's attributes. + """ + yield ("cond_state", self.cond_state) + yield ("true_value", self.true_value) + yield ("false_value", self.false_value) diff --git a/packages/reflex-core/src/reflex_core/components/tags/iter_tag.py b/packages/reflex-core/src/reflex_core/components/tags/iter_tag.py new file mode 100644 index 00000000000..fad8ac6fb51 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/tags/iter_tag.py @@ -0,0 +1,117 @@ +"""Tag to loop through a list of components.""" + +from __future__ import annotations + +import dataclasses +import inspect +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING + +from reflex_core.components.tags.tag import Tag +from reflex_core.utils.types import GenericType +from reflex_core.vars import LiteralArrayVar, Var, get_unique_variable_name +from reflex_core.vars.sequence import _determine_value_of_array_index + +if TYPE_CHECKING: + from reflex_core.components.component import Component + + +@dataclasses.dataclass(frozen=True) +class IterTag(Tag): + """An iterator tag.""" + + # The var to iterate over. + iterable: Var[Iterable] = dataclasses.field( + default_factory=lambda: LiteralArrayVar.create([]) + ) + + # The component render function for each item in the iterable. + render_fn: Callable = dataclasses.field(default_factory=lambda: lambda x: x) + + # The name of the arg var. + arg_var_name: str = dataclasses.field(default_factory=get_unique_variable_name) + + # The name of the index var. + index_var_name: str = dataclasses.field(default_factory=get_unique_variable_name) + + def get_iterable_var_type(self) -> GenericType: + """Get the type of the iterable var. + + Returns: + The type of the iterable var. + """ + return _determine_value_of_array_index(self.iterable._var_type) + + def get_index_var(self) -> Var: + """Get the index var for the tag (with curly braces). + + This is used to reference the index var within the tag. + + Returns: + The index var. + """ + return Var( + _js_expr=self.index_var_name, + _var_type=int, + ).guess_type() + + def get_arg_var(self) -> Var: + """Get the arg var for the tag (with curly braces). + + This is used to reference the arg var within the tag. + + Returns: + The arg var. + """ + return Var( + _js_expr=self.arg_var_name, + _var_type=self.get_iterable_var_type(), + ).guess_type() + + def render_component(self) -> Component: + """Render the component. + + Returns: + The rendered component. + + Raises: + ValueError: If the render function takes more than 2 arguments. + ValueError: If the render function doesn't return a component. + """ + # Import here to avoid circular imports. + from reflex_components_core.base.fragment import Fragment + from reflex_components_core.core.cond import Cond + from reflex_components_core.core.foreach import Foreach + + from reflex.compiler.compiler import _into_component_once + + # Get the render function arguments. + args = inspect.getfullargspec(self.render_fn).args + arg = self.get_arg_var() + index = self.get_index_var() + + if len(args) == 1: + # If the render function doesn't take the index as an argument. + component = self.render_fn(arg) + else: + # If the render function takes the index as an argument. + if len(args) != 2: + msg = "The render function must take 2 arguments." + raise ValueError(msg) + component = self.render_fn(arg, index) + + # Nested foreach components or cond must be wrapped in fragments. + if isinstance(component, (Foreach, Cond)): + component = Fragment.create(component) + + component = _into_component_once(component) + + if component is None: + msg = "The render function must return a component." + raise ValueError(msg) + + # Set the component key. + if component.key is None: + component.key = index + + return component diff --git a/packages/reflex-core/src/reflex_core/components/tags/match_tag.py b/packages/reflex-core/src/reflex_core/components/tags/match_tag.py new file mode 100644 index 00000000000..3f49882ae1b --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/tags/match_tag.py @@ -0,0 +1,31 @@ +"""Tag to conditionally match cases.""" + +import dataclasses +from collections.abc import Iterator, Mapping, Sequence +from typing import Any + +from reflex_core.components.tags.tag import Tag + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class MatchTag(Tag): + """A match tag.""" + + # The condition to determine which case to match. + cond: str + + # The list of match cases to be matched. + match_cases: Sequence[tuple[Sequence[str], Mapping]] + + # The catchall case to match. + default: Any + + def __iter__(self) -> Iterator[tuple[str, Any]]: + """Iterate over the tag's attributes. + + Yields: + An iterator over the tag's attributes. + """ + yield ("cond", self.cond) + yield ("match_cases", self.match_cases) + yield ("default", self.default) diff --git a/packages/reflex-core/src/reflex_core/components/tags/tag.py b/packages/reflex-core/src/reflex_core/components/tags/tag.py new file mode 100644 index 00000000000..3d9312c6ad4 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/tags/tag.py @@ -0,0 +1,137 @@ +"""A React tag.""" + +from __future__ import annotations + +import dataclasses +from collections.abc import Iterator, Mapping, Sequence +from typing import Any + +from reflex_core.event import EventChain +from reflex_core.utils import format +from reflex_core.vars.base import LiteralVar, Var + + +def render_prop(value: Any) -> Any: + """Render the prop. + + Args: + value: The value to render. + + Returns: + The rendered value. + """ + from reflex_core.components.component import BaseComponent + + if isinstance(value, BaseComponent): + return value.render() + if isinstance(value, Sequence) and not isinstance(value, str): + return [render_prop(v) for v in value] + if callable(value) and not isinstance(value, Var): + return None + return value + + +@dataclasses.dataclass(frozen=True) +class Tag: + """A React tag.""" + + # The name of the tag. + name: str = "" + + # The props of the tag. + props: Mapping[str, Any] = dataclasses.field(default_factory=dict) + + # Special props that aren't key value pairs. + special_props: Sequence[Var] = dataclasses.field(default_factory=list) + + # The children components. + children: Sequence[Any] = dataclasses.field(default_factory=list) + + def format_props(self) -> list[str]: + """Format the tag's props. + + Returns: + The formatted props list. + """ + return format.format_props(*self.special_props, **self.props) + + def set(self, **kwargs: Any): + """Return a new tag with the given fields set. + + Args: + **kwargs: The fields to set. + + Returns: + The tag with the fields set. + """ + return dataclasses.replace(self, **kwargs) + + def __iter__(self) -> Iterator[tuple[str, Any]]: + """Iterate over the tag's fields. + + Yields: + tuple[str, Any]: The field name and value. + """ + for field in dataclasses.fields(self): + if field.name == "props": + yield "props", self.format_props() + elif field.name != "special_props": + rendered_value = render_prop(getattr(self, field.name)) + if rendered_value is not None: + yield field.name, rendered_value + + def add_props(self, **kwargs: Any | None) -> Tag: + """Return a new tag with the given props added. + + Args: + **kwargs: The props to add. + + Returns: + The tag with the props added. + """ + return dataclasses.replace( + self, + props={ + **self.props, + **{ + format.to_camel_case(name, treat_hyphens_as_underscores=False): ( + prop + if isinstance(prop, (EventChain, Mapping)) + else LiteralVar.create(prop) + ) + for name, prop in kwargs.items() + if self.is_valid_prop(prop) + }, + }, + ) + + def remove_props(self, *args: str) -> Tag: + """Return a new tag with the given props removed. + + Args: + *args: The names of the props to remove. + + Returns: + The tag with the props removed. + """ + formatted_args = [format.to_camel_case(arg) for arg in args] + return dataclasses.replace( + self, + props={ + name: value + for name, value in self.props.items() + if name not in formatted_args + }, + ) + + @staticmethod + def is_valid_prop(prop: Var | None) -> bool: + """Check if the prop is valid. + + Args: + prop: The prop to check. + + Returns: + Whether the prop is valid. + """ + return prop is not None and not (isinstance(prop, dict) and len(prop) == 0) diff --git a/packages/reflex-core/src/reflex_core/components/tags/tagless.py b/packages/reflex-core/src/reflex_core/components/tags/tagless.py new file mode 100644 index 00000000000..43978f5eeb9 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/components/tags/tagless.py @@ -0,0 +1,36 @@ +"""A tag with no tag.""" + +import dataclasses + +from reflex_core.components.tags import Tag +from reflex_core.utils import format + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class Tagless(Tag): + """A tag with no tag.""" + + # The inner contents of the tag. + contents: str + + def __str__(self) -> str: + """Return the string representation of the tag. + + Returns: + The string representation of the tag. + """ + out = self.contents + space = format.wrap(" ", "{") + if len(self.contents) > 0 and self.contents[0] == " ": + out = space + out + if len(self.contents) > 0 and self.contents[-1] == " ": + out = out + space + return out + + def __iter__(self): + """Iterate over the tag's fields. + + Yields: + tuple[str, Any]: The field name and value. + """ + yield "contents", self.contents diff --git a/packages/reflex-core/src/reflex_core/config.py b/packages/reflex-core/src/reflex_core/config.py new file mode 100644 index 00000000000..b598b9d039a --- /dev/null +++ b/packages/reflex-core/src/reflex_core/config.py @@ -0,0 +1,643 @@ +"""The Reflex config.""" + +import dataclasses +import importlib +import os +import sys +import threading +import urllib.parse +from collections.abc import Sequence +from importlib.util import find_spec +from pathlib import Path +from types import ModuleType +from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal + +from reflex_core import constants +from reflex_core.constants.base import LogLevel +from reflex_core.environment import EnvironmentVariables as EnvironmentVariables +from reflex_core.environment import EnvVar as EnvVar +from reflex_core.environment import ( + ExistingPath, + SequenceOptions, + _load_dotenv_from_files, + _paths_from_env_files, + interpret_env_var_value, +) +from reflex_core.environment import env_var as env_var +from reflex_core.environment import environment as environment +from reflex_core.plugins import Plugin +from reflex_core.plugins.sitemap import SitemapPlugin +from reflex_core.utils import console +from reflex_core.utils.exceptions import ConfigError + + +@dataclasses.dataclass(kw_only=True) +class DBConfig: + """Database config.""" + + engine: str + username: str | None = "" + password: str | None = "" + host: str | None = "" + port: int | None = None + database: str + + @classmethod + def postgresql( + cls, + database: str, + username: str, + password: str | None = None, + host: str | None = None, + port: int | None = 5432, + ) -> "DBConfig": + """Create an instance with postgresql engine. + + Args: + database: Database name. + username: Database username. + password: Database password. + host: Database host. + port: Database port. + + Returns: + DBConfig instance. + """ + return cls( + engine="postgresql", + username=username, + password=password, + host=host, + port=port, + database=database, + ) + + @classmethod + def postgresql_psycopg( + cls, + database: str, + username: str, + password: str | None = None, + host: str | None = None, + port: int | None = 5432, + ) -> "DBConfig": + """Create an instance with postgresql+psycopg engine. + + Args: + database: Database name. + username: Database username. + password: Database password. + host: Database host. + port: Database port. + + Returns: + DBConfig instance. + """ + return cls( + engine="postgresql+psycopg", + username=username, + password=password, + host=host, + port=port, + database=database, + ) + + @classmethod + def sqlite( + cls, + database: str, + ) -> "DBConfig": + """Create an instance with sqlite engine. + + Args: + database: Database name. + + Returns: + DBConfig instance. + """ + return cls( + engine="sqlite", + database=database, + ) + + def get_url(self) -> str: + """Get database URL. + + Returns: + The database URL. + """ + host = ( + f"{self.host}:{self.port}" if self.host and self.port else self.host or "" + ) + username = urllib.parse.quote_plus(self.username) if self.username else "" + password = urllib.parse.quote_plus(self.password) if self.password else "" + + if username: + path = f"{username}:{password}@{host}" if password else f"{username}@{host}" + else: + path = f"{host}" + + return f"{self.engine}://{path}/{self.database}" + + +# These vars are not logged because they may contain sensitive information. +_sensitive_env_vars = {"DB_URL", "ASYNC_DB_URL", "REDIS_URL"} + + +@dataclasses.dataclass(kw_only=True) +class BaseConfig: + """Base config for the Reflex app. + + Attributes: + app_name: The name of the app (should match the name of the app directory). + app_module_import: The path to the app module. + loglevel: The log level to use. + frontend_port: The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken. + frontend_path: The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app + backend_port: The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken. + api_url: The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production. + deploy_url: The url the frontend will be hosted on. + backend_host: The url the backend will be hosted on. + db_url: The database url used by rx.Model. + async_db_url: The async database url used by rx.Model. + redis_url: The redis url. + telemetry_enabled: Telemetry opt-in. + bun_path: The bun path. + static_page_generation_timeout: Timeout to do a production build of a frontend page. + cors_allowed_origins: Comma separated list of origins that are allowed to connect to the backend API. + vite_allowed_hosts: Allowed hosts for the Vite dev server. Set to True to allow all hosts, or provide a list of hostnames (e.g. ["myservice.local"]) to allow specific ones. Prevents 403 errors in Docker, Codespaces, reverse proxies, etc. + react_strict_mode: Whether to use React strict mode. + frontend_packages: Additional frontend packages to install. + state_manager_mode: Indicate which type of state manager to use. + redis_lock_expiration: Maximum expiration lock time for redis state manager. + redis_lock_warning_threshold: Maximum lock time before warning for redis state manager. + redis_token_expiration: Token expiration time for redis state manager. + env_file: Path to file containing key-values pairs to override in the environment; Dotenv format. + state_auto_setters: Whether to automatically create setters for state base vars. + show_built_with_reflex: Whether to display the sticky "Built with Reflex" badge on all pages. + is_reflex_cloud: Whether the app is running in the reflex cloud environment. + extra_overlay_function: Extra overlay function to run after the app is built. Formatted such that `from path_0.path_1... import path[-1]`, and calling it with no arguments would work. For example, "reflex_components_moment.moment". + plugins: List of plugins to use in the app. + disable_plugins: List of plugin types to disable in the app. + transport: The transport method for client-server communication. + """ + + app_name: str + + app_module_import: str | None = None + + loglevel: constants.LogLevel = constants.LogLevel.DEFAULT + + frontend_port: int | None = None + + frontend_path: str = "" + + backend_port: int | None = None + + api_url: str = f"http://localhost:{constants.DefaultPorts.BACKEND_PORT}" + + deploy_url: str | None = f"http://localhost:{constants.DefaultPorts.FRONTEND_PORT}" + + backend_host: str = "0.0.0.0" + + db_url: str | None = "sqlite:///reflex.db" + + async_db_url: str | None = None + + redis_url: str | None = None + + telemetry_enabled: bool = True + + bun_path: ExistingPath = constants.Bun.DEFAULT_PATH + + static_page_generation_timeout: int = 60 + + cors_allowed_origins: Annotated[ + Sequence[str], + SequenceOptions(delimiter=","), + ] = dataclasses.field(default=("*",)) + + vite_allowed_hosts: bool | list[str] = False + + react_strict_mode: bool = True + + frontend_packages: list[str] = dataclasses.field(default_factory=list) + + state_manager_mode: constants.StateManagerMode = constants.StateManagerMode.DISK + + redis_lock_expiration: int = constants.Expiration.LOCK + + redis_lock_warning_threshold: int = constants.Expiration.LOCK_WARNING_THRESHOLD + + redis_token_expiration: int = constants.Expiration.TOKEN + + # Attributes that were explicitly set by the user. + _non_default_attributes: set[str] = dataclasses.field( + default_factory=set, init=False + ) + + env_file: str | None = None + + state_auto_setters: bool | None = None + + show_built_with_reflex: bool | None = None + + is_reflex_cloud: bool = False + + extra_overlay_function: str | None = None + + plugins: list[Plugin] = dataclasses.field(default_factory=list) + + disable_plugins: list[type[Plugin]] = dataclasses.field(default_factory=list) + + transport: Literal["websocket", "polling"] = "websocket" + + # Whether to skip plugin checks. + _skip_plugins_checks: bool = dataclasses.field(default=False, repr=False) + + _prefixes: ClassVar[list[str]] = ["REFLEX_"] + + +_PLUGINS_ENABLED_BY_DEFAULT = [ + SitemapPlugin, +] + + +@dataclasses.dataclass(kw_only=True, init=False) +class Config(BaseConfig): + """Configuration class for Reflex applications. + + The config defines runtime settings for your app including server ports, database connections, + frontend packages, and deployment settings. + + By default, the config is defined in an `rxconfig.py` file in the root of your app: + + ```python + # rxconfig.py + import reflex as rx + + config = rx.Config( + app_name="myapp", + # Server configuration + frontend_port=3000, + backend_port=8000, + # Database + db_url="postgresql://user:pass@localhost:5432/mydb", + # Additional frontend packages + frontend_packages=["react-icons"], + # CORS settings for production + cors_allowed_origins=["https://mydomain.com"], + ) + ``` + + ## Environment Variable Overrides + + Any config value can be overridden by setting an environment variable with the `REFLEX_` + prefix and the parameter name in uppercase: + + ```bash + REFLEX_DB_URL="postgresql://user:pass@localhost/db" reflex run + REFLEX_FRONTEND_PORT=3001 reflex run + ``` + + ## Key Configuration Areas + + - **App Settings**: `app_name`, `loglevel`, `telemetry_enabled` + - **Server**: `frontend_port`, `backend_port`, `api_url`, `cors_allowed_origins` + - **Database**: `db_url`, `async_db_url`, `redis_url` + - **Frontend**: `frontend_packages`, `react_strict_mode` + - **State Management**: `state_manager_mode`, `state_auto_setters` + - **Plugins**: `plugins`, `disable_plugins` + + See the [configuration docs](https://reflex.dev/docs/advanced-onboarding/configuration) for complete details on all available options. + """ + + # Track whether the app name has already been validated for this Config instance. + _app_name_is_valid: bool = dataclasses.field(default=False, repr=False) + + def _post_init(self, **kwargs): + """Post-initialization method to set up the config. + + This method is called after the config is initialized. It sets up the + environment variables, updates the config from the environment, and + replaces default URLs if ports were set. + + Args: + **kwargs: The kwargs passed to the Pydantic init method. + + Raises: + ConfigError: If some values in the config are invalid. + """ + class_fields = self.class_fields() + for key, value in kwargs.items(): + if key not in class_fields: + setattr(self, key, value) + + # Clean up this code when we remove plain envvar in 0.8.0 + env_loglevel = os.environ.get("REFLEX_LOGLEVEL") + if env_loglevel is not None: + env_loglevel = LogLevel(env_loglevel.lower()) + if env_loglevel or self.loglevel != LogLevel.DEFAULT: + console.set_log_level(env_loglevel or self.loglevel) + + # Update the config from environment variables. + env_kwargs = self.update_from_env() + for key, env_value in env_kwargs.items(): + setattr(self, key, env_value) + + # Normalize disable_plugins: convert strings and Plugin subclasses to instances. + self._normalize_disable_plugins() + + # Add builtin plugins if not disabled. + if not self._skip_plugins_checks: + self._add_builtin_plugins() + + # Update default URLs if ports were set + kwargs.update(env_kwargs) + self._non_default_attributes = set(kwargs.keys()) + self._replace_defaults(**kwargs) + + if ( + self.state_manager_mode == constants.StateManagerMode.REDIS + and not self.redis_url + ): + msg = f"{self._prefixes[0]}REDIS_URL is required when using the redis state manager." + raise ConfigError(msg) + + def _normalize_disable_plugins(self): + """Normalize disable_plugins list entries to Plugin subclasses. + + Handles backward compatibility by converting strings (fully qualified + import paths) and Plugin instances to their associated classes. + """ + normalized: list[type[Plugin]] = [] + for entry in self.disable_plugins: + if isinstance(entry, type) and issubclass(entry, Plugin): + normalized.append(entry) + elif isinstance(entry, Plugin): + normalized.append(type(entry)) + elif isinstance(entry, str): + console.deprecate( + feature_name="Passing strings to disable_plugins", + reason="pass Plugin classes directly instead, e.g. disable_plugins=[SitemapPlugin]", + deprecation_version="0.8.28", + removal_version="0.9.0", + ) + try: + from reflex_core.environment import interpret_plugin_class_env + + normalized.append( + interpret_plugin_class_env(entry, "disable_plugins") + ) + except Exception: + console.warn( + f"Failed to import plugin from string {entry!r} in disable_plugins. " + "Please pass Plugin subclasses directly.", + ) + else: + console.warn( + f"reflex.Config.disable_plugins should contain Plugin subclasses, but got {entry!r}.", + ) + self.disable_plugins = normalized + + def _add_builtin_plugins(self): + """Add the builtin plugins to the config.""" + for plugin in _PLUGINS_ENABLED_BY_DEFAULT: + plugin_name = plugin.__module__ + "." + plugin.__qualname__ + if plugin not in self.disable_plugins: + if not any(isinstance(p, plugin) for p in self.plugins): + console.warn( + f"`{plugin_name}` plugin is enabled by default, but not explicitly added to the config. " + "If you want to use it, please add it to the `plugins` list in your config inside of `rxconfig.py`. " + f"To disable this plugin, add `{plugin.__name__}` to the `disable_plugins` list.", + ) + self.plugins.append(plugin()) + else: + if any(isinstance(p, plugin) for p in self.plugins): + console.warn( + f"`{plugin_name}` is disabled in the config, but it is still present in the `plugins` list. " + "Please remove it from the `plugins` list in your config inside of `rxconfig.py`.", + ) + + for disabled_plugin in self.disable_plugins: + if disabled_plugin not in _PLUGINS_ENABLED_BY_DEFAULT: + console.warn( + f"`{disabled_plugin!r}` is disabled in the config, but it is not a built-in plugin. " + "Please remove it from the `disable_plugins` list in your config inside of `rxconfig.py`.", + ) + + @classmethod + def class_fields(cls) -> set[str]: + """Get the fields of the config class. + + Returns: + The fields of the config class. + """ + return {field.name for field in dataclasses.fields(cls)} + + if not TYPE_CHECKING: + + def __init__(self, **kwargs): + """Initialize the config values. + + Args: + **kwargs: The kwargs to pass to the Pydantic init method. + + # noqa: DAR101 self + """ + class_fields = self.class_fields() + super().__init__(**{k: v for k, v in kwargs.items() if k in class_fields}) + self._post_init(**kwargs) + + def json(self) -> str: + """Get the config as a JSON string. + + Returns: + The config as a JSON string. + """ + import json + + from reflex_core.utils.serializers import serialize + + return json.dumps(self, default=serialize) + + @property + def app_module(self) -> ModuleType | None: + """Return the app module if `app_module_import` is set. + + Returns: + The app module. + """ + return ( + importlib.import_module(self.app_module_import) + if self.app_module_import + else None + ) + + @property + def module(self) -> str: + """Get the module name of the app. + + Returns: + The module name. + """ + if self.app_module_import is not None: + return self.app_module_import + return self.app_name + "." + self.app_name + + def update_from_env(self) -> dict[str, Any]: + """Update the config values based on set environment variables. + If there is a set env_file, it is loaded first. + + Returns: + The updated config values. + """ + if self.env_file: + _load_dotenv_from_files(_paths_from_env_files(self.env_file)) + + updated_values = {} + # Iterate over the fields. + for field in dataclasses.fields(self): + # The env var name is the key in uppercase. + environment_variable = None + for prefix in self._prefixes: + if environment_variable := os.environ.get( + f"{prefix}{field.name.upper()}" + ): + break + + # If the env var is set, override the config value. + if environment_variable and environment_variable.strip(): + # Interpret the value. + value = interpret_env_var_value( + environment_variable, + field.type, + field.name, + ) + + # Set the value. + updated_values[field.name] = value + + if field.name.upper() in _sensitive_env_vars: + environment_variable = "***" + + if value != getattr(self, field.name): + console.debug( + f"Overriding config value {field.name} with env var {field.name.upper()}={environment_variable}", + dedupe=True, + ) + return updated_values + + def get_event_namespace(self) -> str: + """Get the path that the backend Websocket server lists on. + + Returns: + The namespace for websocket. + """ + event_url = constants.Endpoint.EVENT.get_url() + return urllib.parse.urlsplit(event_url).path + + def _replace_defaults(self, **kwargs): + """Replace formatted defaults when the caller provides updates. + + Args: + **kwargs: The kwargs passed to the config or from the env. + """ + if "api_url" not in self._non_default_attributes and "backend_port" in kwargs: + self.api_url = f"http://localhost:{kwargs['backend_port']}" + + if ( + "deploy_url" not in self._non_default_attributes + and "frontend_port" in kwargs + ): + self.deploy_url = f"http://localhost:{kwargs['frontend_port']}" + + if "api_url" not in self._non_default_attributes: + # If running in Github Codespaces, override API_URL + codespace_name = os.getenv("CODESPACE_NAME") + github_codespaces_port_forwarding_domain = os.getenv( + "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" + ) + # If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port + replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN") + backend_port = kwargs.get("backend_port", self.backend_port) + if codespace_name and github_codespaces_port_forwarding_domain: + self.api_url = ( + f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}" + f".{github_codespaces_port_forwarding_domain}" + ) + elif replit_dev_domain and backend_port: + self.api_url = f"https://{replit_dev_domain}:{backend_port}" + + def _set_persistent(self, **kwargs): + """Set values in this config and in the environment so they persist into subprocess. + + Args: + **kwargs: The kwargs passed to the config. + """ + for key, value in kwargs.items(): + if value is not None: + os.environ[self._prefixes[0] + key.upper()] = str(value) + setattr(self, key, value) + self._non_default_attributes.update(kwargs) + self._replace_defaults(**kwargs) + + +def _get_config() -> Config: + """Get the app config. + + Returns: + The app config. + """ + # only import the module if it exists. If a module spec exists then + # the module exists. + spec = find_spec(constants.Config.MODULE) + if not spec: + # we need this condition to ensure that a ModuleNotFound error is not thrown when + # running unit/integration tests or during `reflex init`. + return Config(app_name="", _skip_plugins_checks=True) + rxconfig = importlib.import_module(constants.Config.MODULE) + return rxconfig.config + + +# Protect sys.path from concurrent modification +_config_lock = threading.RLock() + + +def get_config(reload: bool = False) -> Config: + """Get the app config. + + Args: + reload: Re-import the rxconfig module from disk + + Returns: + The app config. + """ + cached_rxconfig = sys.modules.get(constants.Config.MODULE, None) + if cached_rxconfig is not None: + if reload: + # Remove any cached module when `reload` is requested. + del sys.modules[constants.Config.MODULE] + else: + return cached_rxconfig.config + + with _config_lock: + orig_sys_path = sys.path.copy() + sys.path.clear() + sys.path.append(str(Path.cwd())) + try: + # Try to import the module with only the current directory in the path. + return _get_config() + except Exception: + # If the module import fails, try to import with the original sys.path. + sys.path.extend(orig_sys_path) + return _get_config() + finally: + # Find any entries added to sys.path by rxconfig.py itself. + extra_paths = [ + p for p in sys.path if p not in orig_sys_path and p != str(Path.cwd()) + ] + # Restore the original sys.path. + sys.path.clear() + sys.path.extend(extra_paths) + sys.path.extend(orig_sys_path) diff --git a/packages/reflex-core/src/reflex_core/constants/__init__.py b/packages/reflex-core/src/reflex_core/constants/__init__.py new file mode 100644 index 00000000000..69f79271d9e --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/__init__.py @@ -0,0 +1,118 @@ +"""The constants package.""" + +from .base import ( + APP_HARNESS_FLAG, + COOKIES, + IS_LINUX, + IS_MACOS, + IS_WINDOWS, + LOCAL_STORAGE, + POLLING_MAX_HTTP_BUFFER_SIZE, + PYTEST_CURRENT_TEST, + REFLEX_VAR_CLOSING_TAG, + REFLEX_VAR_OPENING_TAG, + SESSION_STORAGE, + ColorMode, + Dirs, + Env, + LogLevel, + Ping, + ReactRouter, + Reflex, + ReflexHostingCLI, + Templates, +) +from .compiler import ( + NOCOMPILE_FILE, + SETTER_PREFIX, + CompileContext, + CompileVars, + ComponentName, + Ext, + Hooks, + Imports, + MemoizationDisposition, + MemoizationMode, + PageNames, +) +from .config import ( + ALEMBIC_CONFIG, + Config, + DefaultPorts, + Expiration, + GitIgnore, + PyprojectToml, + RequirementsTxt, +) +from .custom_components import CustomComponents +from .event import Endpoint, EventTriggers, SocketEvent +from .installer import Bun, Node, PackageJson +from .route import ( + ROUTE_NOT_FOUND, + ROUTER, + ROUTER_DATA, + ROUTER_DATA_INCLUDE, + DefaultPage, + Page404, + RouteArgType, + RouteRegex, + RouteVar, +) +from .state import StateManagerMode + +__all__ = [ + "ALEMBIC_CONFIG", + "APP_HARNESS_FLAG", + "COOKIES", + "IS_LINUX", + "IS_MACOS", + "IS_WINDOWS", + "LOCAL_STORAGE", + "NOCOMPILE_FILE", + "POLLING_MAX_HTTP_BUFFER_SIZE", + "PYTEST_CURRENT_TEST", + "REFLEX_VAR_CLOSING_TAG", + "REFLEX_VAR_OPENING_TAG", + "ROUTER", + "ROUTER_DATA", + "ROUTER_DATA_INCLUDE", + "ROUTE_NOT_FOUND", + "SESSION_STORAGE", + "SETTER_PREFIX", + "Bun", + "ColorMode", + "CompileContext", + "CompileVars", + "ComponentName", + "Config", + "CustomComponents", + "DefaultPage", + "DefaultPorts", + "Dirs", + "Endpoint", + "Env", + "EventTriggers", + "Expiration", + "Ext", + "GitIgnore", + "Hooks", + "Imports", + "LogLevel", + "MemoizationDisposition", + "MemoizationMode", + "Node", + "PackageJson", + "Page404", + "PageNames", + "Ping", + "PyprojectToml", + "ReactRouter", + "Reflex", + "RequirementsTxt", + "RouteArgType", + "RouteRegex", + "RouteVar", + "SocketEvent", + "StateManagerMode", + "Templates", +] diff --git a/packages/reflex-core/src/reflex_core/constants/base.py b/packages/reflex-core/src/reflex_core/constants/base.py new file mode 100644 index 00000000000..896dd95b5de --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/base.py @@ -0,0 +1,278 @@ +"""Base file for constants that don't fit any other categories.""" + +from __future__ import annotations + +import platform +from enum import Enum +from importlib import metadata +from pathlib import Path +from types import SimpleNamespace +from typing import Literal + +from platformdirs import PlatformDirs + +IS_WINDOWS = platform.system() == "Windows" +IS_MACOS = platform.system() == "Darwin" +IS_LINUX = platform.system() == "Linux" + + +class Dirs(SimpleNamespace): + """Various directories/paths used by Reflex.""" + + # The frontend directories in a project. + # The web folder where the frontend app is compiled to. + WEB = ".web" + # The directory where uploaded files are stored. + UPLOADED_FILES = "uploaded_files" + # The name of the assets directory. + APP_ASSETS = "assets" + # The name of the assets directory for external resources (a subfolder of APP_ASSETS). + EXTERNAL_APP_ASSETS = "external" + # The name of the utils file. + UTILS = "utils" + # The name of the state file. + STATE_PATH = UTILS + "/state" + # The name of the components file. + COMPONENTS_PATH = UTILS + "/components" + # The name of the contexts file. + CONTEXTS_PATH = UTILS + "/context" + # The name of the output directory. + BUILD_DIR = "build" + # The name of the static files directory. + STATIC = BUILD_DIR + "/client" + # The name of the public html directory served at "/" + PUBLIC = "public" + # The directory where styles are located. + STYLES = "styles" + # The name of the pages directory. + PAGES = "app" + # The name of the routes directory. + ROUTES = "routes" + # The name of the env json file. + ENV_JSON = "env.json" + # The name of the reflex json file. + REFLEX_JSON = "reflex.json" + # The name of the postcss config file. + POSTCSS_JS = "postcss.config.js" + # The name of the states directory. + STATES = ".states" + # Where compilation artifacts for the backend are stored. + BACKEND = "backend" + # JSON-encoded list of page routes that need to be evaluated on the backend. + STATEFUL_PAGES = "stateful_pages.json" + # Marker file indicating that upload component was used in the frontend. + UPLOAD_IS_USED = "upload_is_used" + + +def _reflex_version() -> str: + """Get the Reflex version. + + Returns: + The Reflex version. + """ + try: + return metadata.version("reflex") + except metadata.PackageNotFoundError: + return "unknown" + + +class Reflex(SimpleNamespace): + """Base constants concerning Reflex.""" + + # App names and versions. + # The name of the Reflex package. + MODULE_NAME = "reflex" + # The current version of Reflex. + VERSION = _reflex_version() + + # The reflex json file. + JSON = "reflex.json" + + # Files and directories used to init a new project. + # The directory to store reflex dependencies. + # on windows, we use C:/Users//AppData/Local/reflex. + # on macOS, we use ~/Library/Application Support/reflex. + # on linux, we use ~/.local/share/reflex. + # If user sets REFLEX_DIR envroment variable use that instead. + DIR = PlatformDirs(MODULE_NAME, False).user_data_path + + LOGS_DIR = DIR / "logs" + + # The root directory of the reflex library. + ROOT_DIR = Path(__file__).parents[1] + + RELEASES_URL = "https://api.github.com/repos/reflex-dev/templates/releases" + + # The reflex stylesheet language supported + STYLESHEETS_SUPPORTED = ["css", "sass", "scss"] + + +class ReflexHostingCLI(SimpleNamespace): + """Base constants concerning Reflex Hosting CLI.""" + + # The name of the Reflex Hosting CLI package. + MODULE_NAME = "reflex-hosting-cli" + + +class Templates(SimpleNamespace): + """Constants related to Templates.""" + + # The default template + DEFAULT = "blank" + + # The AI template + AI = "ai" + + # The option for the user to choose a remote template. + CHOOSE_TEMPLATES = "choose-templates" + + # The URL to find reflex templates. + REFLEX_TEMPLATES_URL = ( + "https://reflex.dev/docs/getting-started/open-source-templates/" + ) + + # The reflex.build frontend host + REFLEX_BUILD_FRONTEND = "https://build.reflex.dev" + + # The reflex.build frontend with referrer + REFLEX_BUILD_FRONTEND_WITH_REFERRER = ( + f"{REFLEX_BUILD_FRONTEND}/?utm_source=reflex_cli" + ) + + class Dirs(SimpleNamespace): + """Folders used by the template system of Reflex.""" + + # The template directory used during reflex init. + BASE = Reflex.ROOT_DIR / ".templates" + # The web subdirectory of the template directory. + WEB_TEMPLATE = BASE / "web" + # Where the code for the templates is stored. + CODE = "code" + + +class Javascript(SimpleNamespace): + """Constants related to Javascript.""" + + # The node modules directory. + NODE_MODULES = "node_modules" + + +class ReactRouter(Javascript): + """Constants related to React Router.""" + + # The react router config file + CONFIG_FILE = "react-router.config.js" + + # The associated Vite config file + VITE_CONFIG_FILE = "vite.config.js" + + # Regex to check for message displayed when frontend comes up + DEV_FRONTEND_LISTENING_REGEX = r"Local:[\s]+" + + # Regex to pattern the route path in the config file + # INFO Accepting connections at http://localhost:3000 + PROD_FRONTEND_LISTENING_REGEX = r"Accepting connections at[\s]+" + + FRONTEND_LISTENING_REGEX = ( + rf"(?:{DEV_FRONTEND_LISTENING_REGEX}|{PROD_FRONTEND_LISTENING_REGEX})(.*)" + ) + + SPA_FALLBACK = "__spa-fallback.html" + + +# Color mode variables +class ColorMode(SimpleNamespace): + """Constants related to ColorMode.""" + + NAME = "rawColorMode" + RESOLVED_NAME = "resolvedColorMode" + USE = "useColorMode" + TOGGLE = "toggleColorMode" + SET = "setColorMode" + + +LITERAL_ENV = Literal["dev", "prod"] + + +# Env modes +class Env(str, Enum): + """The environment modes.""" + + DEV = "dev" + PROD = "prod" + + +# Log levels +class LogLevel(str, Enum): + """The log levels.""" + + DEBUG = "debug" + DEFAULT = "default" + INFO = "info" + WARNING = "warning" + ERROR = "error" + CRITICAL = "critical" + + @classmethod + def from_string(cls, level: str | None) -> LogLevel | None: + """Convert a string to a log level. + + Args: + level: The log level as a string. + + Returns: + The log level. + """ + if not level: + return None + try: + return LogLevel[level.upper()] + except KeyError: + return None + + def __le__(self, other: LogLevel) -> bool: + """Compare log levels. + + Args: + other: The other log level. + + Returns: + True if the log level is less than or equal to the other log level. + """ + levels = list(LogLevel) + return levels.index(self) <= levels.index(other) + + def subprocess_level(self): + """Return the log level for the subprocess. + + Returns: + The log level for the subprocess + """ + return self if self != LogLevel.DEFAULT else LogLevel.WARNING + + +# Server socket configuration variables +POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000 + + +class Ping(SimpleNamespace): + """PING constants.""" + + # The 'ping' interval + INTERVAL = 25 + # The 'ping' timeout + TIMEOUT = 120 + + +# Keys in the client_side_storage dict +COOKIES = "cookies" +LOCAL_STORAGE = "local_storage" +SESSION_STORAGE = "session_storage" + +# Testing variables. +# Testing os env set by pytest when running a test case. +PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST" +APP_HARNESS_FLAG = "APP_HARNESS_FLAG" + +REFLEX_VAR_OPENING_TAG = "" +REFLEX_VAR_CLOSING_TAG = "" diff --git a/packages/reflex-core/src/reflex_core/constants/colors.py b/packages/reflex-core/src/reflex_core/constants/colors.py new file mode 100644 index 00000000000..d31b9cd72e3 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/colors.py @@ -0,0 +1,99 @@ +"""The colors used in Reflex are a wrapper around https://www.radix-ui.com/colors.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal, get_args + +if TYPE_CHECKING: + from reflex_core.vars import Var + +ColorType = Literal[ + "gray", + "mauve", + "slate", + "sage", + "olive", + "sand", + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "accent", + "black", + "white", +] + +COLORS = frozenset(get_args(ColorType)) + +ShadeType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] +MIN_SHADE_VALUE = 1 +MAX_SHADE_VALUE = 12 + + +def format_color( + color: ColorType | Var[str], shade: ShadeType | Var[int], alpha: bool | Var[bool] +) -> str: + """Format a color as a CSS color string. + + Args: + color: The color to use. + shade: The shade of the color to use. + alpha: Whether to use the alpha variant of the color. + + Returns: + The formatted color. + """ + if isinstance(alpha, bool): + return f"var(--{color}-{'a' if alpha else ''}{shade})" + + from reflex_components_core.core import cond + + alpha_var = cond(alpha, "a", "") + return f"var(--{color}-{alpha_var}{shade})" + + +@dataclass +class Color: + """A color in the Reflex color palette.""" + + # The color palette to use + color: ColorType | Var[str] + + # The shade of the color to use + shade: ShadeType | Var[int] = 7 + + # Whether to use the alpha variant of the color + alpha: bool | Var[bool] = False + + def __format__(self, format_spec: str) -> str: + """Format the color as a CSS color string. + + Args: + format_spec: The format specifier to use. + + Returns: + The formatted color. + """ + return format_color(self.color, self.shade, self.alpha) diff --git a/packages/reflex-core/src/reflex_core/constants/compiler.py b/packages/reflex-core/src/reflex_core/constants/compiler.py new file mode 100644 index 00000000000..74e90083ae0 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/compiler.py @@ -0,0 +1,210 @@ +"""Compiler variables.""" + +import dataclasses +import enum +from enum import Enum +from types import SimpleNamespace + +from reflex_core.constants import Dirs +from reflex_core.utils.imports import ImportVar + +# The prefix used to create setters for state vars. +SETTER_PREFIX = "set_" + +# The file used to specify no compilation. +NOCOMPILE_FILE = "nocompile" + + +class Ext(SimpleNamespace): + """Extension used in Reflex.""" + + # The extension for JS files. + JS = ".js" + # The extension for JSX files. + JSX = ".jsx" + # The extension for python files. + PY = ".py" + # The extension for css files. + CSS = ".css" + # The extension for zip files. + ZIP = ".zip" + # The extension for executable files on Windows. + EXE = ".exe" + # The extension for markdown files. + MD = ".md" + + +class CompileVars(SimpleNamespace): + """The variables used during compilation.""" + + # The expected variable name where the rx.App is stored. + APP = "app" + # The expected variable name where the API object is stored for deployment. + API = "api" + # The name of the router variable. + ROUTER = "router" + # The name of the socket variable. + SOCKET = "socket" + # The name of the variable to hold API results. + RESULT = "result" + # The name of the final variable. + FINAL = "final" + # The name of the process variable. + PROCESSING = "processing" + # The name of the state variable. + STATE = "state" + # The name of the events variable. + EVENTS = "events" + # The name of the initial hydrate event. + HYDRATE = "hydrate" + # The name of the is_hydrated variable. + IS_HYDRATED = "is_hydrated" + # The name of the function to add events to the queue. + ADD_EVENTS = "addEvents" + # The name of the function to apply event actions before invoking a target. + APPLY_EVENT_ACTIONS = "applyEventActions" + # The name of the var storing any connection error. + CONNECT_ERROR = "connectErrors" + # The name of the function for converting a dict to an event. + TO_EVENT = "ReflexEvent" + # The name of the internal on_load event. + ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal" + # The name of the internal event to update generic state vars. + UPDATE_VARS_INTERNAL = ( + "reflex___state____update_vars_internal_state.update_vars_internal" + ) + # The name of the frontend event exception state + FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state" + # The full name of the frontend exception state + FRONTEND_EXCEPTION_STATE_FULL = ( + f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}" + ) + + +class PageNames(SimpleNamespace): + """The name of basic pages deployed in the frontend.""" + + # The name of the index page. + INDEX_ROUTE = "index" + # The name of the app root page. + APP_ROOT = "root.jsx" + # The root stylesheet filename. + STYLESHEET_ROOT = "__reflex_global_styles" + # The name of the document root page. + DOCUMENT_ROOT = "_document.js" + # The name of the theme page. + THEME = "theme" + # The module containing components. + COMPONENTS = "components" + # The module containing shared stateful components + STATEFUL_COMPONENTS = "stateful_components" + + +class ComponentName(Enum): + """Component names.""" + + BACKEND = "Backend" + FRONTEND = "Frontend" + + def zip(self): + """Give the zip filename for the component. + + Returns: + The lower-case filename with zip extension. + """ + return self.value.lower() + Ext.ZIP + + +class CompileContext(str, Enum): + """The context in which the compiler is running.""" + + RUN = "run" + EXPORT = "export" + DEPLOY = "deploy" + UNDEFINED = "undefined" + + +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), + ImportVar(tag=CompileVars.APPLY_EVENT_ACTIONS), + ], + } + + +class Hooks(SimpleNamespace): + """Common sets of hook declarations.""" + + EVENTS = f"const [{CompileVars.ADD_EVENTS}, {CompileVars.CONNECT_ERROR}] = useContext(EventLoopContext);" + + class HookPosition(enum.Enum): + """The position of the hook in the component.""" + + INTERNAL = "internal" + PRE_TRIGGER = "pre_trigger" + POST_TRIGGER = "post_trigger" + + +class MemoizationDisposition(enum.Enum): + """The conditions under which a component should be memoized.""" + + # If the component uses state or events, it should be memoized. + STATEFUL = "stateful" + ALWAYS = "always" + NEVER = "never" + + +@dataclasses.dataclass(frozen=True) +class MemoizationMode: + """The mode for memoizing a Component.""" + + # The conditions under which the component should be memoized. + disposition: MemoizationDisposition = MemoizationDisposition.STATEFUL + + # Whether children of this component should be memoized first. + recursive: bool = True + + +DATA_UNDERSCORE = "data_" +DATA_DASH = "data-" +ARIA_UNDERSCORE = "aria_" +ARIA_DASH = "aria-" + +SPECIAL_ATTRS = ( + DATA_UNDERSCORE, + DATA_DASH, + ARIA_UNDERSCORE, + ARIA_DASH, +) + + +class SpecialAttributes(enum.Enum): + """Special attributes for components. + + These are placed in custom_attrs and rendered as-is rather than converting + to a style prop. + """ + + @classmethod + def is_special(cls, attr: str) -> bool: + """Check if the attribute is special. + + Args: + attr: the attribute to check + + Returns: + True if the attribute is special. + """ + return attr.startswith(SPECIAL_ATTRS) + + +class ResetStylesheet(SimpleNamespace): + """Constants for CSS reset stylesheet.""" + + # The filename of the CSS reset file. + FILENAME = "__reflex_style_reset.css" diff --git a/packages/reflex-core/src/reflex_core/constants/config.py b/packages/reflex-core/src/reflex_core/constants/config.py new file mode 100644 index 00000000000..d1fd39ab93c --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/config.py @@ -0,0 +1,76 @@ +"""Config constants.""" + +from pathlib import Path +from types import SimpleNamespace + +from reflex_core.constants.base import Dirs, Reflex + +from .compiler import Ext + +# Alembic migrations +ALEMBIC_CONFIG = "alembic.ini" + + +class Config(SimpleNamespace): + """Config constants.""" + + # The name of the reflex config module. + MODULE = "rxconfig" + # The python config file. + FILE = Path(f"{MODULE}{Ext.PY}") + + +class Expiration(SimpleNamespace): + """Expiration constants.""" + + # Token expiration time in seconds + TOKEN = 60 * 60 + # Maximum time in milliseconds that a state can be locked for exclusive access. + LOCK = 10000 + # The PING timeout + PING = 120 + # The maximum time in milliseconds to hold a lock before throwing a warning. + LOCK_WARNING_THRESHOLD = 1000 + + +class GitIgnore(SimpleNamespace): + """Gitignore constants.""" + + # The gitignore file. + FILE = Path(".gitignore") + # Files to gitignore. + DEFAULTS = { + Dirs.WEB, + Dirs.STATES, + "*.db", + "__pycache__/", + "*.py[cod]", + "assets/external/", + } + + +class PyprojectToml(SimpleNamespace): + """Pyproject.toml constants.""" + + # The pyproject.toml file. + FILE = "pyproject.toml" + + +class RequirementsTxt(SimpleNamespace): + """Requirements.txt constants.""" + + # The requirements.txt file. + FILE = "requirements.txt" + # The partial text used to form requirement that pins a reflex version + DEFAULTS_STUB = f"{Reflex.MODULE_NAME}==" + + +class DefaultPorts(SimpleNamespace): + """Default port constants.""" + + FRONTEND_PORT = 3000 + BACKEND_PORT = 8000 + + +# The deployment URL. +PRODUCTION_BACKEND_URL = "https://{username}-{app_name}.api.pynecone.app" diff --git a/packages/reflex-core/src/reflex_core/constants/custom_components.py b/packages/reflex-core/src/reflex_core/constants/custom_components.py new file mode 100644 index 00000000000..a499327b19d --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/custom_components.py @@ -0,0 +1,35 @@ +"""Constants for the custom components.""" + +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace + + +class CustomComponents(SimpleNamespace): + """Constants for the custom components.""" + + # The name of the custom components source directory. + SRC_DIR = Path("custom_components") + # The name of the custom components pyproject.toml file. + PYPROJECT_TOML = Path("pyproject.toml") + # The name of the custom components package README file. + PACKAGE_README = Path("README.md") + # The name of the custom components package .gitignore file. + PACKAGE_GITIGNORE = ".gitignore" + # The name of the distribution directory as result of a build. + DIST_DIR = "dist" + # The name of the init file. + INIT_FILE = "__init__.py" + # Suffixes for the distribution files. + DISTRIBUTION_FILE_SUFFIXES = [".tar.gz", ".whl"] + # The name to the URL of python package repositories. + REPO_URLS = { + # Note: the trailing slash is required for below URLs. + "pypi": "https://upload.pypi.org/legacy/", + "testpypi": "https://test.pypi.org/legacy/", + } + # The .gitignore file for the custom component project. + FILE = Path(".gitignore") + # Files to gitignore. + DEFAULTS = {"__pycache__/", "*.py[cod]", "*.egg-info/", "dist/"} diff --git a/packages/reflex-core/src/reflex_core/constants/event.py b/packages/reflex-core/src/reflex_core/constants/event.py new file mode 100644 index 00000000000..d69ccf246df --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/event.py @@ -0,0 +1,102 @@ +"""Event-related constants.""" + +from enum import Enum +from types import SimpleNamespace + + +class Endpoint(Enum): + """Endpoints for the reflex backend API.""" + + PING = "ping" + EVENT = "_event" + UPLOAD = "_upload" + AUTH_CODESPACE = "auth-codespace" + HEALTH = "_health" + ALL_ROUTES = "_all_routes" + + def __str__(self) -> str: + """Get the string representation of the endpoint. + + Returns: + The path for the endpoint. + """ + return f"/{self.value}" + + def get_url(self) -> str: + """Get the URL for the endpoint. + + Returns: + The full URL for the endpoint. + """ + # Import here to avoid circular imports. + from reflex_core.config import get_config + + # Get the API URL from the config. + config = get_config() + url = "".join([config.api_url, str(self)]) + + # The event endpoint is a websocket. + if self == Endpoint.EVENT: + # Replace the protocol with ws. + url = url.replace("https://", "wss://").replace("http://", "ws://") + + # Return the url. + return url + + +class SocketEvent(SimpleNamespace): + """Socket events sent by the reflex backend API.""" + + PING = "ping" + EVENT = "event" + + def __str__(self) -> str: + """Get the string representation of the event name. + + Returns: + The event name string. + """ + return str(self.value) + + +class EventTriggers(SimpleNamespace): + """All trigger names used in Reflex.""" + + ON_FOCUS = "on_focus" + ON_BLUR = "on_blur" + ON_CANCEL = "on_cancel" + ON_CLICK = "on_click" + ON_CHANGE = "on_change" + ON_CHANGE_END = "on_change_end" + ON_CHANGE_START = "on_change_start" + ON_COMPLETE = "on_complete" + ON_CONTEXT_MENU = "on_context_menu" + ON_DOUBLE_CLICK = "on_double_click" + ON_DROP = "on_drop" + ON_EDIT = "on_edit" + ON_KEY_DOWN = "on_key_down" + ON_KEY_UP = "on_key_up" + ON_MOUSE_DOWN = "on_mouse_down" + ON_MOUSE_ENTER = "on_mouse_enter" + ON_MOUSE_LEAVE = "on_mouse_leave" + ON_MOUSE_MOVE = "on_mouse_move" + ON_MOUSE_OUT = "on_mouse_out" + ON_MOUSE_OVER = "on_mouse_over" + ON_MOUSE_UP = "on_mouse_up" + ON_OPEN_CHANGE = "on_open_change" + ON_OPEN_AUTO_FOCUS = "on_open_auto_focus" + ON_CLOSE_AUTO_FOCUS = "on_close_auto_focus" + ON_FOCUS_OUTSIDE = "on_focus_outside" + ON_ESCAPE_KEY_DOWN = "on_escape_key_down" + ON_POINTER_DOWN_OUTSIDE = "on_pointer_down_outside" + ON_INTERACT_OUTSIDE = "on_interact_outside" + ON_SCROLL = "on_scroll" + ON_SCROLL_END = "on_scroll_end" + ON_SUBMIT = "on_submit" + ON_MOUNT = "on_mount" + ON_UNMOUNT = "on_unmount" + ON_CLEAR_SERVER_ERRORS = "on_clear_server_errors" + ON_VALUE_COMMIT = "on_value_commit" + ON_SELECT = "on_select" + ON_ANIMATION_START = "on_animation_start" + ON_ANIMATION_END = "on_animation_end" diff --git a/packages/reflex-core/src/reflex_core/constants/installer.py b/packages/reflex-core/src/reflex_core/constants/installer.py new file mode 100644 index 00000000000..c2f62047369 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/installer.py @@ -0,0 +1,165 @@ +"""File for constants related to the installation process. (Bun/Node).""" + +from __future__ import annotations + +import os +from types import SimpleNamespace + +from .base import IS_WINDOWS +from .utils import classproperty + + +# Bun config. +class Bun(SimpleNamespace): + """Bun constants.""" + + # The Bun version. + VERSION = "1.3.10" + + # Min Bun Version + MIN_VERSION = "1.3.0" + + # URL to bun install script. + INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh" + + # URL to windows install script. + WINDOWS_INSTALL_URL = ( + "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/install.ps1" + ) + + # Path of the bunfig file + CONFIG_PATH = "bunfig.toml" + + @classproperty + @classmethod + def ROOT_PATH(cls): + """The directory to store the bun. + + Returns: + The directory to store the bun. + """ + from reflex_core.environment import environment + + return environment.REFLEX_DIR.get() / "bun" + + @classproperty + @classmethod + def DEFAULT_PATH(cls): + """Default bun path. + + Returns: + The default bun path. + """ + return cls.ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe") + + DEFAULT_CONFIG = """ +[install] +registry = "{registry}" +""" + + +# Node / NPM config +class Node(SimpleNamespace): + """Node/ NPM constants.""" + + # The minimum required node version. + MIN_VERSION = "20.19.0" + + # Path of the node config file. + CONFIG_PATH = ".npmrc" + + DEFAULT_CONFIG = """ +registry={registry} +fetch-retries=0 +""" + + +def _determine_react_router_version() -> str: + default_version = "7.13.1" + if (version := os.getenv("REACT_ROUTER_VERSION")) and version != default_version: + from reflex_core.utils import console + + console.warn( + f"You have requested react-router@{version} but the supported version is {default_version}, abandon all hope ye who enter here." + ) + return version + return default_version + + +def _determine_react_version() -> str: + default_version = "19.2.4" + if (version := os.getenv("REACT_VERSION")) and version != default_version: + from reflex_core.utils import console + + console.warn( + f"You have requested react@{version} but the supported version is {default_version}, abandon all hope ye who enter here." + ) + return version + return default_version + + +class PackageJson(SimpleNamespace): + """Constants used to build the package.json file.""" + + class Commands(SimpleNamespace): + """The commands to define in package.json.""" + + DEV = "react-router dev --host" + EXPORT = "react-router build" + + @staticmethod + def get_prod_command(frontend_path: str = "") -> str: + """Get the prod command with the correct 404.html path for the given frontend_path. + + Args: + frontend_path: The frontend path prefix (e.g. "/app"). + + Returns: + The sirv command with the correct --single fallback path. + """ + stripped = frontend_path.strip("/") + fallback = f"{stripped}/404.html" if stripped else "404.html" + return f"sirv ./build/client --single {fallback} --host" + + PATH = "package.json" + + _react_version = _determine_react_version() + + _react_router_version = _determine_react_router_version() + + @classproperty + @classmethod + def DEPENDENCIES(cls) -> dict[str, str]: + """The dependencies to include in package.json. + + Returns: + A dictionary of dependencies with their versions. + """ + return { + "json5": "2.2.3", + "react-router": cls._react_router_version, + "react-router-dom": cls._react_router_version, + "@react-router/node": cls._react_router_version, + "sirv-cli": "3.0.1", + "react": cls._react_version, + "react-helmet": "6.1.0", + "react-dom": cls._react_version, + "isbot": "5.1.36", + "socket.io-client": "4.8.3", + "universal-cookie": "7.2.2", + } + + DEV_DEPENDENCIES = { + "@emotion/react": "11.14.0", + "autoprefixer": "10.4.27", + "postcss": "8.5.8", + "postcss-import": "16.1.1", + "@react-router/dev": _react_router_version, + "@react-router/fs-routes": _react_router_version, + "vite": "8.0.0", + } + OVERRIDES = { + # This should always match the `react` version in DEPENDENCIES for recharts compatibility. + "react-is": _react_version, + "cookie": "1.1.1", + } diff --git a/packages/reflex-core/src/reflex_core/constants/route.py b/packages/reflex-core/src/reflex_core/constants/route.py new file mode 100644 index 00000000000..30e7b32170e --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/route.py @@ -0,0 +1,94 @@ +"""Route constants.""" + +import re +from types import SimpleNamespace + + +class RouteArgType(SimpleNamespace): + """Type of dynamic route arg extracted from URI route.""" + + SINGLE = "arg_single" + LIST = "arg_list" + + +# the name of the backend var containing path and client information +ROUTER = "router" +ROUTER_DATA = "router_data" + + +class RouteVar(SimpleNamespace): + """Names of variables used in the router_data dict stored in State.""" + + CLIENT_IP = "ip" + CLIENT_TOKEN = "token" + HEADERS = "headers" + PATH = "pathname" + ORIGIN = "asPath" + SESSION_ID = "sid" + QUERY = "query" + COOKIE = "cookie" + + +# This subset of router_data is included in chained on_load events. +ROUTER_DATA_INCLUDE = {RouteVar.PATH, RouteVar.ORIGIN, RouteVar.QUERY} + + +class RouteRegex(SimpleNamespace): + """Regex used for extracting route args in route.""" + + _DOT_DOT_DOT = r"\.\.\." + _OPENING_BRACKET = r"\[" + _CLOSING_BRACKET = r"\]" + _ARG_NAME = r"[a-zA-Z_]\w*" + + # The regex for a valid arg name, e.g. "slug" in "[slug]" + _ARG_NAME_PATTERN = re.compile(_ARG_NAME) + + SLUG = re.compile(r"[a-zA-Z0-9_-]+") + # match a single arg (i.e. "[slug]"), returns the name of the arg + ARG = re.compile(rf"{_OPENING_BRACKET}({_ARG_NAME}){_CLOSING_BRACKET}") + # match a single optional arg (i.e. "[[slug]]"), returns the name of the arg + OPTIONAL_ARG = re.compile( + rf"{_OPENING_BRACKET * 2}({_ARG_NAME}){_CLOSING_BRACKET * 2}" + ) + + # match a single non-optional catch-all arg (i.e. "[...slug]"), returns the name of the arg + STRICT_CATCHALL = re.compile( + rf"{_OPENING_BRACKET}{_DOT_DOT_DOT}({_ARG_NAME}){_CLOSING_BRACKET}" + ) + + # match a single optional catch-all arg (i.e. "[[...slug]]"), returns the name of the arg + OPTIONAL_CATCHALL = re.compile( + rf"{_OPENING_BRACKET * 2}{_DOT_DOT_DOT}({_ARG_NAME}){_CLOSING_BRACKET * 2}" + ) + + SPLAT_CATCHALL = "[[...splat]]" + SINGLE_SEGMENT = "__SINGLE_SEGMENT__" + DOUBLE_SEGMENT = "__DOUBLE_SEGMENT__" + DOUBLE_CATCHALL_SEGMENT = "__DOUBLE_CATCHALL_SEGMENT__" + + +class DefaultPage(SimpleNamespace): + """Default page constants.""" + + # The default title to show for Reflex apps. + TITLE = "{} | {}" + # The default description to show for Reflex apps. + DESCRIPTION = "" + # The default image to show for Reflex apps. + IMAGE = "favicon.ico" + # The default meta list to show for Reflex apps. + META_LIST = [] + + +# 404 variables +class Page404(SimpleNamespace): + """Page 404 constants.""" + + SLUG = "404" + TITLE = "404 - Not Found" + IMAGE = "favicon.ico" + DESCRIPTION = "The page was not found" + + +ROUTE_NOT_FOUND = "routeNotFound" diff --git a/packages/reflex-core/src/reflex_core/constants/state.py b/packages/reflex-core/src/reflex_core/constants/state.py new file mode 100644 index 00000000000..3f6ebec2f17 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/state.py @@ -0,0 +1,19 @@ +"""State-related constants.""" + +from enum import Enum + + +class StateManagerMode(str, Enum): + """State manager constants.""" + + DISK = "disk" + MEMORY = "memory" + REDIS = "redis" + + +# Used for things like console_log, etc. +FRONTEND_EVENT_STATE = "__reflex_internal_frontend_event_state" + +FIELD_MARKER = "_rx_state_" +MEMO_MARKER = "_rx_memo_" +CAMEL_CASE_MEMO_MARKER = "RxMemo" diff --git a/packages/reflex-core/src/reflex_core/constants/utils.py b/packages/reflex-core/src/reflex_core/constants/utils.py new file mode 100644 index 00000000000..b07041582a9 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/constants/utils.py @@ -0,0 +1,31 @@ +"""Utility functions for constants.""" + +from collections.abc import Callable +from typing import Any, Generic, TypeVar + +T = TypeVar("T") +V = TypeVar("V") + + +class classproperty(Generic[T, V]): + """A class property decorator.""" + + def __init__(self, getter: Callable[[type[T]], V]) -> None: + """Initialize the class property. + + Args: + getter: The getter function. + """ + self.getter = getattr(getter, "__func__", getter) + + def __get__(self, instance: Any, owner: type[T]) -> V: + """Get the value of the class property. + + Args: + instance: The instance of the class. + owner: The class itself. + + Returns: + The value of the class property. + """ + return self.getter(owner) diff --git a/packages/reflex-core/src/reflex_core/environment.py b/packages/reflex-core/src/reflex_core/environment.py new file mode 100644 index 00000000000..a747cd21ba1 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/environment.py @@ -0,0 +1,877 @@ +"""Environment variable management.""" + +from __future__ import annotations + +import concurrent.futures +import dataclasses +import enum +import importlib +import multiprocessing +import os +import platform +from collections.abc import Callable, Sequence +from functools import lru_cache +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Generic, + Literal, + TypeVar, + get_args, + get_origin, + get_type_hints, +) + +from reflex_core import constants +from reflex_core.constants.base import LogLevel +from reflex_core.plugins import Plugin +from reflex_core.utils.exceptions import EnvironmentVarValueError +from reflex_core.utils.types import GenericType, is_union, value_inside_optional + + +def get_default_value_for_field(field: dataclasses.Field) -> Any: + """Get the default value for a field. + + Args: + field: The field. + + Returns: + The default value. + + Raises: + ValueError: If no default value is found. + """ + if field.default != dataclasses.MISSING: + return field.default + if field.default_factory != dataclasses.MISSING: + return field.default_factory() + msg = f"Missing value for environment variable {field.name} and no default value found" + raise ValueError(msg) + + +# TODO: Change all interpret_.* signatures to value: str, field: dataclasses.Field once we migrate rx.Config to dataclasses +def interpret_boolean_env(value: str, field_name: str) -> bool: + """Interpret a boolean environment variable value. + + Args: + value: The environment variable value. + field_name: The field name. + + Returns: + The interpreted value. + + Raises: + EnvironmentVarValueError: If the value is invalid. + """ + true_values = ["true", "1", "yes", "y"] + false_values = ["false", "0", "no", "n"] + + if value.lower() in true_values: + return True + if value.lower() in false_values: + return False + msg = f"Invalid boolean value: {value!r} for {field_name}" + raise EnvironmentVarValueError(msg) + + +def interpret_int_env(value: str, field_name: str) -> int: + """Interpret an integer environment variable value. + + Args: + value: The environment variable value. + field_name: The field name. + + Returns: + The interpreted value. + + Raises: + EnvironmentVarValueError: If the value is invalid. + """ + try: + return int(value) + except ValueError as ve: + msg = f"Invalid integer value: {value!r} for {field_name}" + raise EnvironmentVarValueError(msg) from ve + + +def interpret_float_env(value: str, field_name: str) -> float: + """Interpret a float environment variable value. + + Args: + value: The environment variable value. + field_name: The field name. + + Returns: + The interpreted value. + + Raises: + EnvironmentVarValueError: If the value is invalid. + """ + try: + return float(value) + except ValueError as ve: + msg = f"Invalid float value: {value!r} for {field_name}" + raise EnvironmentVarValueError(msg) from ve + + +def interpret_existing_path_env(value: str, field_name: str) -> ExistingPath: + """Interpret a path environment variable value as an existing path. + + Args: + value: The environment variable value. + field_name: The field name. + + Returns: + The interpreted value. + + Raises: + EnvironmentVarValueError: If the path does not exist. + """ + path = Path(value) + if not path.exists(): + msg = f"Path does not exist: {path!r} for {field_name}" + raise EnvironmentVarValueError(msg) + return path + + +def interpret_path_env(value: str, field_name: str) -> Path: + """Interpret a path environment variable value. + + Args: + value: The environment variable value. + field_name: The field name. + + Returns: + The interpreted value. + """ + return Path(value) + + +def interpret_plugin_class_env(value: str, field_name: str) -> type[Plugin]: + """Interpret an environment variable value as a Plugin subclass. + + Resolves a fully qualified import path to the Plugin subclass it refers to. + + Args: + value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin"). + field_name: The field name. + + Returns: + The Plugin subclass. + + Raises: + EnvironmentVarValueError: If the value is invalid. + """ + if "." not in value: + msg = f"Invalid plugin value: {value!r} for {field_name}. Plugin name must be in the format 'package.module.PluginName'." + raise EnvironmentVarValueError(msg) + + import_path, plugin_name = value.rsplit(".", 1) + + try: + module = importlib.import_module(import_path) + except ImportError as e: + msg = f"Failed to import module {import_path!r} for {field_name}: {e}" + raise EnvironmentVarValueError(msg) from e + + try: + plugin_class = getattr(module, plugin_name, None) + except Exception as e: + msg = f"Failed to get plugin class {plugin_name!r} from module {import_path!r} for {field_name}: {e}" + raise EnvironmentVarValueError(msg) from e + + if not isinstance(plugin_class, type) or not issubclass(plugin_class, Plugin): + msg = f"Invalid plugin class: {plugin_name!r} for {field_name}. Must be a subclass of Plugin." + raise EnvironmentVarValueError(msg) + + return plugin_class + + +def interpret_plugin_env(value: str, field_name: str) -> Plugin: + """Interpret a plugin environment variable value. + + Resolves a fully qualified import path and returns an instance of the Plugin. + + Args: + value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin"). + field_name: The field name. + + Returns: + An instance of the Plugin subclass. + + Raises: + EnvironmentVarValueError: If the value is invalid. + """ + plugin_class = interpret_plugin_class_env(value, field_name) + + try: + return plugin_class() + except Exception as e: + msg = f"Failed to instantiate plugin {plugin_class.__name__!r} for {field_name}: {e}" + raise EnvironmentVarValueError(msg) from e + + +def interpret_enum_env(value: str, field_type: GenericType, field_name: str) -> Any: + """Interpret an enum environment variable value. + + Args: + value: The environment variable value. + field_type: The field type. + field_name: The field name. + + Returns: + The interpreted value. + + Raises: + EnvironmentVarValueError: If the value is invalid. + """ + try: + return field_type(value) + except ValueError as ve: + msg = f"Invalid enum value: {value!r} for {field_name}" + raise EnvironmentVarValueError(msg) from ve + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class SequenceOptions: + """Options for interpreting Sequence environment variables.""" + + delimiter: str = ":" + strip: bool = False + + +DEFAULT_SEQUENCE_OPTIONS = SequenceOptions() + + +def interpret_env_var_value( + value: str, field_type: GenericType, field_name: str +) -> Any: + """Interpret an environment variable value based on the field type. + + Args: + value: The environment variable value. + field_type: The field type. + field_name: The field name. + + Returns: + The interpreted value. + + Raises: + ValueError: If the value is invalid. + EnvironmentVarValueError: If the value is invalid for the specific type. + """ + field_type = value_inside_optional(field_type) + + # Unwrap Annotated to get the base type for env var interpretation. + # Preserve SequenceOptions and PathExistsFlag markers. + annotated_metadata: tuple[Any, ...] = () + if get_origin(field_type) is Annotated: + annotated_args = get_args(field_type) + annotated_metadata = annotated_args[1:] + field_type = annotated_args[0] + + if is_union(field_type): + errors = [] + for arg in (union_types := get_args(field_type)): + try: + return interpret_env_var_value(value, arg, field_name) + except (ValueError, EnvironmentVarValueError) as e: # noqa: PERF203 + errors.append(e) + msg = f"Could not interpret {value!r} for {field_name} as any of {union_types}: {errors}" + raise EnvironmentVarValueError(msg) + + value = value.strip() + + if field_type is bool: + return interpret_boolean_env(value, field_name) + if field_type is str: + return value + if field_type is LogLevel: + loglevel = LogLevel.from_string(value) + if loglevel is None: + msg = f"Invalid log level value: {value} for {field_name}" + raise EnvironmentVarValueError(msg) + return loglevel + if field_type is int: + return interpret_int_env(value, field_name) + if field_type is float: + return interpret_float_env(value, field_name) + if field_type is Path: + if PathExistsFlag in annotated_metadata: + return interpret_existing_path_env(value, field_name) + return interpret_path_env(value, field_name) + if field_type is ExistingPath: + return interpret_existing_path_env(value, field_name) + if field_type is Plugin: + return interpret_plugin_env(value, field_name) + if get_origin(field_type) is type: + type_args = get_args(field_type) + if ( + type_args + and isinstance(type_args[0], type) + and issubclass(type_args[0], Plugin) + ): + return interpret_plugin_class_env(value, field_name) + if get_origin(field_type) is Literal: + literal_values = get_args(field_type) + for literal_value in literal_values: + if isinstance(literal_value, str) and literal_value == value: + return literal_value + if isinstance(literal_value, bool): + try: + interpreted_bool = interpret_boolean_env(value, field_name) + if interpreted_bool == literal_value: + return interpreted_bool + except EnvironmentVarValueError: + continue + if isinstance(literal_value, int): + try: + interpreted_int = interpret_int_env(value, field_name) + if interpreted_int == literal_value: + return interpreted_int + except EnvironmentVarValueError: + continue + msg = f"Invalid literal value: {value!r} for {field_name}, expected one of {literal_values}" + raise EnvironmentVarValueError(msg) + # If the field was Annotated with SequenceOptions, extract the options + sequence_options = DEFAULT_SEQUENCE_OPTIONS + for arg in annotated_metadata: + if isinstance(arg, SequenceOptions): + sequence_options = arg + break + if get_origin(field_type) in (list, Sequence): + items = value.split(sequence_options.delimiter) + if sequence_options.strip: + items = [item.strip() for item in items] + return [ + interpret_env_var_value( + v, + get_args(field_type)[0], + f"{field_name}[{i}]", + ) + for i, v in enumerate(items) + ] + if isinstance(field_type, type) and issubclass(field_type, enum.Enum): + return interpret_enum_env(value, field_type, field_name) + + msg = f"Invalid type for environment variable {field_name}: {field_type}. This is probably an issue in Reflex." + raise ValueError(msg) + + +T = TypeVar("T") + + +class EnvVar(Generic[T]): + """Environment variable.""" + + name: str + default: Any + type_: T + + def __init__(self, name: str, default: Any, type_: T) -> None: + """Initialize the environment variable. + + Args: + name: The environment variable name. + default: The default value. + type_: The type of the value. + """ + self.name = name + self.default = default + self.type_ = type_ + + def interpret(self, value: str) -> T: + """Interpret the environment variable value. + + Args: + value: The environment variable value. + + Returns: + The interpreted value. + """ + return interpret_env_var_value(value, self.type_, self.name) + + def getenv(self) -> T | None: + """Get the interpreted environment variable value. + + Returns: + The environment variable value. + """ + env_value = os.getenv(self.name, None) + if env_value and env_value.strip(): + return self.interpret(env_value) + return None + + def is_set(self) -> bool: + """Check if the environment variable is set. + + Returns: + True if the environment variable is set. + """ + return bool(os.getenv(self.name, "").strip()) + + def get(self) -> T: + """Get the interpreted environment variable value or the default value if not set. + + Returns: + The interpreted value. + """ + env_value = self.getenv() + if env_value is not None: + return env_value + return self.default + + def set(self, value: T | None) -> None: + """Set the environment variable. None unsets the variable. + + Args: + value: The value to set. + """ + if value is None: + _ = os.environ.pop(self.name, None) + else: + if isinstance(value, enum.Enum): + value = value.value + if isinstance(value, list): + str_value = ":".join(str(v) for v in value) + else: + str_value = str(value) + os.environ[self.name] = str_value + + +@lru_cache +def get_type_hints_environment(cls: type) -> dict[str, Any]: + """Get the type hints for the environment variables. + + Args: + cls: The class. + + Returns: + The type hints. + """ + return get_type_hints(cls) + + +class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] + """Descriptor for environment variables.""" + + name: str + default: Any + internal: bool = False + + def __init__(self, default: Any, internal: bool = False) -> None: + """Initialize the descriptor. + + Args: + default: The default value. + internal: Whether the environment variable is reflex internal. + """ + self.default = default + self.internal = internal + + def __set_name__(self, owner: Any, name: str): + """Set the name of the descriptor. + + Args: + owner: The owner class. + name: The name of the descriptor. + """ + self.name = name + + def __get__( + self, instance: EnvironmentVariables, owner: type[EnvironmentVariables] + ): + """Get the EnvVar instance. + + Args: + instance: The instance. + owner: The owner class. + + Returns: + The EnvVar instance. + """ + type_ = get_args(get_type_hints_environment(owner)[self.name])[0] + env_name = self.name + if self.internal: + env_name = f"__{env_name}" + return EnvVar(name=env_name, default=self.default, type_=type_) + + +if TYPE_CHECKING: + + def env_var(default: Any, internal: bool = False) -> EnvVar: + """Typing helper for the env_var descriptor. + + Args: + default: The default value. + internal: Whether the environment variable is reflex internal. + + Returns: + The EnvVar instance. + """ + return default + + +class PathExistsFlag: + """Flag to indicate that a path must exist.""" + + +ExistingPath = Annotated[Path, PathExistsFlag] + + +class PerformanceMode(enum.Enum): + """Performance mode for the app.""" + + WARN = "warn" + RAISE = "raise" + OFF = "off" + + +class ExecutorType(enum.Enum): + """Executor for compiling the frontend.""" + + THREAD = "thread" + PROCESS = "process" + MAIN_THREAD = "main_thread" + + @classmethod + def get_executor_from_environment(cls): + """Get the executor based on the environment variables. + + Returns: + The executor. + """ + from reflex_core.utils import console + + executor_type = environment.REFLEX_COMPILE_EXECUTOR.get() + + reflex_compile_processes = environment.REFLEX_COMPILE_PROCESSES.get() + reflex_compile_threads = environment.REFLEX_COMPILE_THREADS.get() + # By default, use the main thread. Unless the user has specified a different executor. + # Using a process pool is much faster, but not supported on all platforms. It's gated behind a flag. + if executor_type is None: + if ( + platform.system() not in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + console.warn("Multiprocessing is only supported on Linux and MacOS.") + + if ( + platform.system() in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + if reflex_compile_processes == 0: + console.warn( + "Number of processes must be greater than 0. If you want to use the default number of processes, set REFLEX_COMPILE_EXECUTOR to 'process'. Defaulting to None." + ) + reflex_compile_processes = None + elif reflex_compile_processes < 0: + console.warn( + "Number of processes must be greater than 0. Defaulting to None." + ) + reflex_compile_processes = None + executor_type = ExecutorType.PROCESS + elif reflex_compile_threads is not None: + if reflex_compile_threads == 0: + console.warn( + "Number of threads must be greater than 0. If you want to use the default number of threads, set REFLEX_COMPILE_EXECUTOR to 'thread'. Defaulting to None." + ) + reflex_compile_threads = None + elif reflex_compile_threads < 0: + console.warn( + "Number of threads must be greater than 0. Defaulting to None." + ) + reflex_compile_threads = None + executor_type = ExecutorType.THREAD + else: + executor_type = ExecutorType.MAIN_THREAD + + match executor_type: + case ExecutorType.PROCESS: + executor = concurrent.futures.ProcessPoolExecutor( + max_workers=reflex_compile_processes, + mp_context=multiprocessing.get_context("fork"), + ) + case ExecutorType.THREAD: + executor = concurrent.futures.ThreadPoolExecutor( + max_workers=reflex_compile_threads + ) + case ExecutorType.MAIN_THREAD: + FUTURE_RESULT_TYPE = TypeVar("FUTURE_RESULT_TYPE") + + class MainThreadExecutor: + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def submit( + self, fn: Callable[..., FUTURE_RESULT_TYPE], *args, **kwargs + ) -> concurrent.futures.Future[FUTURE_RESULT_TYPE]: + future_job = concurrent.futures.Future() + future_job.set_result(fn(*args, **kwargs)) + return future_job + + executor = MainThreadExecutor() + + return executor + + +class EnvironmentVariables: + """Environment variables class to instantiate environment variables.""" + + # Indicate the current command that was invoked in the reflex CLI. + REFLEX_COMPILE_CONTEXT: EnvVar[constants.CompileContext] = env_var( + constants.CompileContext.UNDEFINED, internal=True + ) + + # Whether to use npm over bun to install and run the frontend. + REFLEX_USE_NPM: EnvVar[bool] = env_var(False) + + # The npm registry to use. + NPM_CONFIG_REGISTRY: EnvVar[str | None] = env_var(None) + + # Whether to use Granian for the backend. By default, the backend uses Uvicorn if available. + REFLEX_USE_GRANIAN: EnvVar[bool] = env_var(False) + + # Whether to use the system installed bun. If set to false, bun will be bundled with the app. + REFLEX_USE_SYSTEM_BUN: EnvVar[bool] = env_var(False) + + # The working directory for the frontend directory. + REFLEX_WEB_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.WEB)) + + # The working directory for the states directory. + REFLEX_STATES_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.STATES)) + + # Path to the alembic config file + ALEMBIC_CONFIG: EnvVar[ExistingPath] = env_var(Path(constants.ALEMBIC_CONFIG)) + + # Include schemas in alembic migrations. + ALEMBIC_INCLUDE_SCHEMAS: EnvVar[bool] = env_var(False) + + # Disable SSL verification for HTTPX requests. + SSL_NO_VERIFY: EnvVar[bool] = env_var(False) + + # The directory to store uploaded files. + REFLEX_UPLOADED_FILES_DIR: EnvVar[Path] = env_var( + Path(constants.Dirs.UPLOADED_FILES) + ) + + REFLEX_COMPILE_EXECUTOR: EnvVar[ExecutorType | None] = env_var(None) + + # Whether to use separate processes to compile the frontend and how many. If not set, defaults to thread executor. + REFLEX_COMPILE_PROCESSES: EnvVar[int | None] = env_var(None) + + # Whether to use separate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`. + REFLEX_COMPILE_THREADS: EnvVar[int | None] = env_var(None) + + # The directory to store reflex dependencies. + REFLEX_DIR: EnvVar[Path] = env_var(constants.Reflex.DIR) + + # Whether to print the SQL queries if the log level is INFO or lower. + SQLALCHEMY_ECHO: EnvVar[bool] = env_var(False) + + # Whether to check db connections before using them. + SQLALCHEMY_POOL_PRE_PING: EnvVar[bool] = env_var(True) + + # The size of the database connection pool. + SQLALCHEMY_POOL_SIZE: EnvVar[int] = env_var(5) + + # The maximum overflow size of the database connection pool. + SQLALCHEMY_MAX_OVERFLOW: EnvVar[int] = env_var(10) + + # Recycle connections after this many seconds. + SQLALCHEMY_POOL_RECYCLE: EnvVar[int] = env_var(-1) + + # The timeout for acquiring a connection from the pool. + SQLALCHEMY_POOL_TIMEOUT: EnvVar[int] = env_var(30) + + # Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration. + REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False) + + # Whether to skip purging the web directory in dev mode. + REFLEX_PERSIST_WEB_DIR: EnvVar[bool] = env_var(False) + + # This env var stores the execution mode of the app + REFLEX_ENV_MODE: EnvVar[constants.Env] = env_var(constants.Env.DEV) + + # Whether to run the backend only. Exclusive with REFLEX_FRONTEND_ONLY. + REFLEX_BACKEND_ONLY: EnvVar[bool] = env_var(False) + + # Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY. + REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False) + + # The port to run the frontend on. + REFLEX_FRONTEND_PORT: EnvVar[int | None] = env_var(None) + + # The port to run the backend on. + REFLEX_BACKEND_PORT: EnvVar[int | None] = env_var(None) + + # If this env var is set to "yes", App.compile will be a no-op + REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True) + + # Whether to run app harness tests in headless mode. + APP_HARNESS_HEADLESS: EnvVar[bool] = env_var(False) + + # Which app harness driver to use. + APP_HARNESS_DRIVER: EnvVar[str] = env_var("Chrome") + + # Arguments to pass to the app harness driver. + APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("") + + # Whether to check for outdated package versions. + REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True) + + # In which performance mode to run the app. + REFLEX_PERF_MODE: EnvVar[PerformanceMode] = env_var(PerformanceMode.WARN) + + # The maximum size of the reflex state in kilobytes. + REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) + + # Whether to use the turbopack bundler. + REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(False) + + # Additional paths to include in the hot reload. Separated by a colon. + REFLEX_HOT_RELOAD_INCLUDE_PATHS: EnvVar[list[Path]] = env_var([]) + + # Paths to exclude from the hot reload. Takes precedence over include paths. Separated by a colon. + REFLEX_HOT_RELOAD_EXCLUDE_PATHS: EnvVar[list[Path]] = env_var([]) + + # Enables different behavior for when the backend would do a cold start if it was inactive. + REFLEX_DOES_BACKEND_COLD_START: EnvVar[bool] = env_var(False) + + # The timeout for the backend to do a cold start in seconds. + REFLEX_BACKEND_COLD_START_TIMEOUT: EnvVar[int] = env_var(10) + + # Used by flexgen to enumerate the pages. + REFLEX_ADD_ALL_ROUTES_ENDPOINT: EnvVar[bool] = env_var(False) + + # The address to bind the HTTP client to. You can set this to "::" to enable IPv6. + REFLEX_HTTP_CLIENT_BIND_ADDRESS: EnvVar[str | None] = env_var(None) + + # Maximum size of the message in the websocket server in bytes. + REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE: EnvVar[int] = env_var( + constants.POLLING_MAX_HTTP_BUFFER_SIZE + ) + + # The interval to send a ping to the websocket server in seconds. + REFLEX_SOCKET_INTERVAL: EnvVar[int] = env_var(constants.Ping.INTERVAL) + + # The timeout to wait for a pong from the websocket server in seconds. + REFLEX_SOCKET_TIMEOUT: EnvVar[int] = env_var(constants.Ping.TIMEOUT) + + # Whether to run Granian in a spawn process. This enables Reflex to pick up on environment variable changes between hot reloads. + REFLEX_STRICT_HOT_RELOAD: EnvVar[bool] = env_var(False) + + # The path to the reflex log file. If not set, the log file will be stored in the reflex user directory. + REFLEX_LOG_FILE: EnvVar[Path | None] = env_var(None) + + # Enable full logging of debug messages to reflex user directory. + REFLEX_ENABLE_FULL_LOGGING: EnvVar[bool] = env_var(False) + + # Whether to enable hot module replacement + VITE_HMR: EnvVar[bool] = env_var(True) + + # Whether to force a full reload on changes. + VITE_FORCE_FULL_RELOAD: EnvVar[bool] = env_var(False) + + # Whether to enable Rolldown's experimental HMR. + VITE_EXPERIMENTAL_HMR: EnvVar[bool] = env_var(False) + + # Whether to generate sourcemaps for the frontend. + VITE_SOURCEMAP: EnvVar[Literal[False, True, "inline", "hidden"]] = env_var(False) # noqa: RUF038 + + # Whether to enable SSR for the frontend. + REFLEX_SSR: EnvVar[bool] = env_var(True) + + # Whether to mount the compiled frontend app in the backend server in production. + REFLEX_MOUNT_FRONTEND_COMPILED_APP: EnvVar[bool] = env_var(False, internal=True) + + # How long to delay writing updated states to disk. (Higher values mean less writes, but more chance of lost data.) + REFLEX_STATE_MANAGER_DISK_DEBOUNCE_SECONDS: EnvVar[float] = env_var(2.0) + + # How long to wait between automatic reload on frontend error to avoid reload loops. + REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS: EnvVar[int] = env_var(10_000) + + # Whether to enable debug logging for the redis state manager. + REFLEX_STATE_MANAGER_REDIS_DEBUG: EnvVar[bool] = env_var(False) + + # Whether to opportunistically hold the redis lock to allow fast in-memory access while uncontended. + REFLEX_OPLOCK_ENABLED: EnvVar[bool] = env_var(False) + + # How long to opportunistically hold the redis lock in milliseconds (must be less than the token expiration). + REFLEX_OPLOCK_HOLD_TIME_MS: EnvVar[int] = env_var(0) + + +environment = EnvironmentVariables() + +try: + from dotenv import load_dotenv +except ImportError: + load_dotenv = None + + +def _paths_from_env_files(env_files: str) -> list[Path]: + """Convert a string of paths separated by os.pathsep into a list of Path objects. + + Args: + env_files: The string of paths. + + Returns: + A list of Path objects. + """ + # load env files in reverse order + return list( + reversed([ + Path(path) + for path_element in env_files.split(os.pathsep) + if (path := path_element.strip()) + ]) + ) + + +def _load_dotenv_from_files(files: list[Path]): + """Load environment variables from a list of files. + + Args: + files: A list of Path objects representing the environment variable files. + """ + from reflex_core.utils import console + + if not files: + return + + if load_dotenv is None: + console.error( + """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.1.0"`.""" + ) + return + + for env_file in files: + if env_file.exists(): + load_dotenv(env_file, override=True) + + +def _paths_from_environment() -> list[Path]: + """Get the paths from the REFLEX_ENV_FILE environment variable. + + Returns: + A list of Path objects. + """ + env_files = os.environ.get("REFLEX_ENV_FILE") + if not env_files: + return [] + + return _paths_from_env_files(env_files) + + +def _load_dotenv_from_env(): + """Load environment variables from paths specified in REFLEX_ENV_FILE.""" + _load_dotenv_from_files(_paths_from_environment()) + + +# Load the env files at import time if they are set in the ENV_FILE environment variable. +_load_dotenv_from_env() diff --git a/packages/reflex-core/src/reflex_core/event.py b/packages/reflex-core/src/reflex_core/event.py new file mode 100644 index 00000000000..71cc53baab5 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/event.py @@ -0,0 +1,2756 @@ +"""Define event classes to connect the frontend and backend.""" + +import dataclasses +import inspect +import sys +import types +import warnings +from base64 import b64encode +from collections.abc import Callable, Mapping, Sequence +from functools import lru_cache, partial +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Generic, + Literal, + NoReturn, + Protocol, + TypeVar, + get_args, + get_origin, + get_type_hints, + overload, +) + +from typing_extensions import Self, TypeAliasType, TypedDict, TypeVarTuple, Unpack + +from reflex_core import constants +from reflex_core.components.field import BaseField +from reflex_core.constants.compiler import CompileVars, Hooks, Imports +from reflex_core.constants.state import FRONTEND_EVENT_STATE +from reflex_core.utils import format +from reflex_core.utils.decorator import once +from reflex_core.utils.exceptions import ( + EventFnArgMismatchError, + EventHandlerArgTypeMismatchError, + MissingAnnotationError, +) +from reflex_core.utils.types import ( + ArgsSpec, + GenericType, + Unset, + safe_issubclass, + typehint_issubclass, +) +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.function import ( + ArgsFunctionOperation, + ArgsFunctionOperationBuilder, + BuilderFunctionVar, + FunctionArgs, + FunctionStringVar, + FunctionVar, + VarOperationCall, +) +from reflex_core.vars.object import ObjectVar + + +@dataclasses.dataclass( + init=True, + frozen=True, +) +class Event: + """An event that describes any state change in the app. + + Attributes: + token: The token to specify the client that the event is for. + name: The event name. + router_data: The routing data where event occurred. + payload: The event payload. + """ + + token: str + + name: str + + router_data: dict[str, Any] = dataclasses.field(default_factory=dict) + + payload: dict[str, Any] = dataclasses.field(default_factory=dict) + + @property + def substate_token(self) -> str: + """Get the substate token for the event. + + Returns: + The substate token. + """ + substate = self.name.rpartition(".")[0] + return f"{self.token}_{substate}" + + +_EVENT_FIELDS: set[str] = {f.name for f in dataclasses.fields(Event)} +_EMPTY_EVENTS = LiteralVar.create([]) +_EMPTY_EVENT_ACTIONS = LiteralVar.create({}) + +BACKGROUND_TASK_MARKER = "_reflex_background_task" +EVENT_ACTIONS_MARKER = "_rx_event_actions" +UPLOAD_FILES_CLIENT_HANDLER = "uploadFiles" + + +def _handler_name(handler: "EventHandler") -> str: + """Get a stable fully qualified handler name for errors. + + Args: + handler: The handler to name. + + Returns: + The fully qualified handler name. + """ + if handler.state_full_name: + return f"{handler.state_full_name}.{handler.fn.__name__}" + return handler.fn.__qualname__ + + +def resolve_upload_handler_param(handler: "EventHandler") -> tuple[str, Any]: + """Validate and resolve the UploadFile list parameter for a handler. + + Args: + handler: The event handler to inspect. + + Returns: + The parameter name and annotation for the upload file argument. + + Raises: + UploadTypeError: If the handler is a background task. + UploadValueError: If the handler does not accept ``list[rx.UploadFile]``. + """ + from reflex_components_core.core._upload import UploadFile + + from reflex_core.utils.exceptions import UploadTypeError, UploadValueError + + handler_name = _handler_name(handler) + if handler.is_background: + msg = ( + f"@rx.event(background=True) is not supported for upload handler " + f"`{handler_name}`." + ) + raise UploadTypeError(msg) + + func = handler.fn.func if isinstance(handler.fn, partial) else handler.fn + for name, annotation in get_type_hints(func).items(): + if name == "return" or get_origin(annotation) is not list: + continue + args = get_args(annotation) + if len(args) == 1 and typehint_issubclass(args[0], UploadFile): + return name, annotation + + msg = ( + f"`{handler_name}` handler should have a parameter annotated as " + "list[rx.UploadFile]" + ) + raise UploadValueError(msg) + + +def resolve_upload_chunk_handler_param(handler: "EventHandler") -> tuple[str, type]: + """Validate and resolve the UploadChunkIterator parameter for a handler. + + Args: + handler: The event handler to inspect. + + Returns: + The parameter name and annotation for the iterator argument. + + Raises: + UploadTypeError: If the handler is not a background task. + UploadValueError: If the handler does not accept an UploadChunkIterator. + """ + from reflex_components_core.core._upload import UploadChunkIterator + + from reflex_core.utils.exceptions import UploadTypeError, UploadValueError + + handler_name = _handler_name(handler) + if not handler.is_background: + msg = f"@rx.event(background=True) is required for upload_files_chunk handler `{handler_name}`." + raise UploadTypeError(msg) + + func = handler.fn.func if isinstance(handler.fn, partial) else handler.fn + for name, annotation in get_type_hints(func).items(): + if name == "return": + continue + if annotation is UploadChunkIterator: + return name, annotation + + msg = ( + f"`{handler_name}` handler should have a parameter annotated as " + "rx.UploadChunkIterator" + ) + raise UploadValueError(msg) + + +@dataclasses.dataclass( + init=True, + frozen=True, + kw_only=True, +) +class EventActionsMixin: + """Mixin for DOM event actions. + + Attributes: + event_actions: Whether to `preventDefault` or `stopPropagation` on the event. + """ + + event_actions: dict[str, bool | int] = dataclasses.field(default_factory=dict) + + @property + def stop_propagation(self) -> Self: + """Stop the event from bubbling up the DOM tree. + + Returns: + New EventHandler-like with stopPropagation set to True. + """ + return dataclasses.replace( + self, + event_actions={**self.event_actions, "stopPropagation": True}, + ) + + @property + def prevent_default(self) -> Self: + """Prevent the default behavior of the event. + + Returns: + New EventHandler-like with preventDefault set to True. + """ + return dataclasses.replace( + self, + event_actions={**self.event_actions, "preventDefault": True}, + ) + + def throttle(self, limit_ms: int) -> Self: + """Throttle the event handler. + + Args: + limit_ms: The time in milliseconds to throttle the event handler. + + Returns: + New EventHandler-like with throttle set to limit_ms. + """ + return dataclasses.replace( + self, + event_actions={**self.event_actions, "throttle": limit_ms}, + ) + + def debounce(self, delay_ms: int) -> Self: + """Debounce the event handler. + + Args: + delay_ms: The time in milliseconds to debounce the event handler. + + Returns: + New EventHandler-like with debounce set to delay_ms. + """ + return dataclasses.replace( + self, + event_actions={**self.event_actions, "debounce": delay_ms}, + ) + + @property + def temporal(self) -> Self: + """Do not queue the event if the backend is down. + + Returns: + New EventHandler-like with temporal set to True. + """ + return dataclasses.replace( + self, + event_actions={**self.event_actions, "temporal": True}, + ) + + +@dataclasses.dataclass( + init=True, + frozen=True, + kw_only=True, +) +class EventHandler(EventActionsMixin): + """An event handler responds to an event to update the state. + + Attributes: + fn: The function to call in response to the event. + state_full_name: The full name of the state class this event handler is attached to. Empty string means this event handler is a server side event. + """ + + fn: Any = dataclasses.field(default=None) + + state_full_name: str = dataclasses.field(default="") + + def __hash__(self): + """Get the hash of the event handler. + + Returns: + The hash of the event handler. + """ + return hash((tuple(self.event_actions.items()), self.fn, self.state_full_name)) + + def get_parameters(self) -> Mapping[str, inspect.Parameter]: + """Get the parameters of the function. + + Returns: + The parameters of the function. + """ + if self.fn is None: + return {} + return inspect.signature(self.fn).parameters + + @property + def _parameters(self) -> Mapping[str, inspect.Parameter]: + """Get the parameters of the function. + + Returns: + The parameters of the function. + """ + if (parameters := getattr(self, "__parameters", None)) is None: + parameters = {**self.get_parameters()} + object.__setattr__(self, "__parameters", parameters) + return parameters + + @classmethod + def __class_getitem__(cls, args_spec: str) -> Annotated: + """Get a typed EventHandler. + + Args: + args_spec: The args_spec of the EventHandler. + + Returns: + The EventHandler class item. + """ + return Annotated[cls, args_spec] + + @property + def is_background(self) -> bool: + """Whether the event handler is a background task. + + Returns: + True if the event handler is marked as a background task. + """ + return getattr(self.fn, BACKGROUND_TASK_MARKER, False) + + def __call__(self, *args: Any, **kwargs: Any) -> "EventSpec": + """Pass arguments to the handler to get an event spec. + + This method configures event handlers that take in arguments. + + Args: + *args: The arguments to pass to the handler. + **kwargs: The keyword arguments to pass to the handler. + + Returns: + The event spec, containing both the function and args. + + Raises: + EventHandlerTypeError: If the arguments are invalid. + """ + from reflex_core.utils.exceptions import EventHandlerTypeError + + # Get the function args. + fn_args = list(self._parameters)[1:] + + if not isinstance( + repeated_arg := next( + (kwarg for kwarg in kwargs if kwarg in fn_args[: len(args)]), Unset() + ), + Unset, + ): + msg = f"Event handler {self.fn.__name__} received repeated argument {repeated_arg}." + raise EventHandlerTypeError(msg) + + if not isinstance( + extra_arg := next( + (kwarg for kwarg in kwargs if kwarg not in fn_args), Unset() + ), + Unset, + ): + msg = ( + f"Event handler {self.fn.__name__} received extra argument {extra_arg}." + ) + raise EventHandlerTypeError(msg) + + fn_args = fn_args[: len(args)] + list(kwargs) + + fn_args = (Var(_js_expr=arg) for arg in fn_args) + + # Construct the payload. + values = [] + for arg in [*args, *kwargs.values()]: + # Special case for file uploads. + if isinstance(arg, (FileUpload, UploadFilesChunk)): + return arg.as_event_spec(handler=self) + + # Otherwise, convert to JSON. + try: + values.append(LiteralVar.create(arg)) + except TypeError as e: + msg = f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." + raise EventHandlerTypeError(msg) from e + payload = tuple(zip(fn_args, values, strict=False)) + + # Return the event spec. + return EventSpec( + handler=self, args=payload, event_actions=self.event_actions.copy() + ) + + +@dataclasses.dataclass( + init=True, + frozen=True, + kw_only=True, +) +class EventSpec(EventActionsMixin): + """An event specification. + + Whereas an Event object is passed during runtime, a spec is used + during compile time to outline the structure of an event. + + Attributes: + handler: The event handler. + client_handler_name: The handler on the client to process event. + args: The arguments to pass to the function. + """ + + handler: EventHandler = dataclasses.field(default=None) # pyright: ignore [reportAssignmentType] + + client_handler_name: str = dataclasses.field(default="") + + args: tuple[tuple[Var, Var], ...] = dataclasses.field(default_factory=tuple) + + def __init__( + self, + handler: EventHandler, + event_actions: dict[str, bool | int] | None = None, + client_handler_name: str = "", + args: tuple[tuple[Var, Var], ...] = (), + ): + """Initialize an EventSpec. + + Args: + event_actions: The event actions. + handler: The event handler. + client_handler_name: The client handler name. + args: The arguments to pass to the function. + """ + if event_actions is None: + event_actions = {} + object.__setattr__(self, "event_actions", event_actions) + object.__setattr__(self, "handler", handler) + object.__setattr__(self, "client_handler_name", client_handler_name) + object.__setattr__(self, "args", args or ()) + + def with_args(self, args: tuple[tuple[Var, Var], ...]) -> "EventSpec": + """Copy the event spec, with updated args. + + Args: + args: The new args to pass to the function. + + Returns: + A copy of the event spec, with the new args. + """ + return type(self)( + handler=self.handler, + client_handler_name=self.client_handler_name, + args=args, + event_actions=self.event_actions.copy(), + ) + + def add_args(self, *args: Var) -> "EventSpec": + """Add arguments to the event spec. + + Args: + *args: The arguments to add positionally. + + Returns: + The event spec with the new arguments. + + Raises: + EventHandlerTypeError: If the arguments are invalid. + """ + from reflex_core.utils.exceptions import EventHandlerTypeError + + # Get the remaining unfilled function args. + fn_args = list(self.handler._parameters)[1 + len(self.args) :] + fn_args = (Var(_js_expr=arg) for arg in fn_args) + + # Construct the payload. + values = [] + arg = None + try: + for arg in args: + values.append(LiteralVar.create(value=arg)) # noqa: PERF401, RUF100 + except TypeError as e: + msg = f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." + raise EventHandlerTypeError(msg) from e + new_payload = tuple(zip(fn_args, values, strict=False)) + return self.with_args(self.args + new_payload) + + +@dataclasses.dataclass( + frozen=True, +) +class CallableEventSpec(EventSpec): + """Decorate an EventSpec-returning function to act as both a EventSpec and a function. + + This is used as a compatibility shim for replacing EventSpec objects in the + API with functions that return a family of EventSpec. + """ + + fn: Callable[..., EventSpec] | None = None + + def __init__(self, fn: Callable[..., EventSpec] | None = None, **kwargs): + """Initialize a CallableEventSpec. + + Args: + fn: The function to decorate. + **kwargs: The kwargs to pass to the EventSpec constructor. + """ + if fn is not None: + default_event_spec = fn() + super().__init__( + event_actions=default_event_spec.event_actions, + client_handler_name=default_event_spec.client_handler_name, + args=default_event_spec.args, + handler=default_event_spec.handler, + **kwargs, + ) + object.__setattr__(self, "fn", fn) + else: + super().__init__(**kwargs) + + def __call__(self, *args, **kwargs) -> EventSpec: + """Call the decorated function. + + Args: + *args: The args to pass to the function. + **kwargs: The kwargs to pass to the function. + + Returns: + The EventSpec returned from calling the function. + + Raises: + EventHandlerTypeError: If the CallableEventSpec has no associated function. + """ + from reflex_core.utils.exceptions import EventHandlerTypeError + + if self.fn is None: + msg = "CallableEventSpec has no associated function." + raise EventHandlerTypeError(msg) + return self.fn(*args, **kwargs) + + +@dataclasses.dataclass( + init=True, + frozen=True, +) +class EventChain(EventActionsMixin): + """Container for a chain of events that will be executed in order.""" + + events: Sequence["EventSpec | EventVar | FunctionVar | EventCallback"] = ( + dataclasses.field(default_factory=list) + ) + + args_spec: Callable | Sequence[Callable] | None = dataclasses.field(default=None) + + invocation: Var | None = dataclasses.field(default=None) + + @classmethod + def create( + cls, + value: "EventType", + args_spec: ArgsSpec | Sequence[ArgsSpec], + key: str | None = None, + **event_chain_kwargs, + ) -> "EventChain | Var": + """Create an event chain from a variety of input types. + + Args: + value: The value to create the event chain from. + args_spec: The args_spec of the event trigger being bound. + key: The key of the event trigger being bound. + **event_chain_kwargs: Additional kwargs to pass to the EventChain constructor. + + Returns: + The event chain. + + Raises: + ValueError: If the value is not a valid event chain. + """ + # If it's an event chain var, return it. + if isinstance(value, Var): + # Only pass through literal/prebuilt chains. Other EventChainVar values may be + # FunctionVars cast with `.to(EventChain)` and still need wrapping so + # event_chain_kwargs can compose onto the resulting chain. + if isinstance(value, LiteralEventChainVar): + if event_chain_kwargs: + warnings.warn( + f"event_chain_kwargs {event_chain_kwargs!r} are ignored for " + "EventChainVar values.", + stacklevel=2, + ) + return value + if isinstance(value, (EventVar, FunctionVar)): + value = [value] + elif safe_issubclass(value._var_type, (EventChain, EventSpec)): + return cls.create( + value=value.guess_type(), + args_spec=args_spec, + key=key, + **event_chain_kwargs, + ) + else: + msg = f"Invalid event chain: {value!s} of type {value._var_type}" + raise ValueError(msg) + elif isinstance(value, EventChain): + # Trust that the caller knows what they're doing passing an EventChain directly + return value + + # If the input is a single event handler, wrap it in a list. + if isinstance(value, (EventHandler, EventSpec)): + value = [value] + + events: list[EventSpec | EventVar | FunctionVar] = [] + + # If the input is a list of event handlers, create an event chain. + if isinstance(value, list): + for v in value: + if isinstance(v, (EventHandler, EventSpec)): + # Call the event handler to get the event. + events.append(call_event_handler(v, args_spec, key=key)) + elif isinstance(v, (EventVar, EventChainVar)): + events.append(v) + elif isinstance(v, FunctionVar): + # Apply the args_spec transformations as partial arguments to the function. + events.append(v.partial(*parse_args_spec(args_spec)[0])) + elif isinstance(v, Callable): + # Call the lambda to get the event chain. + events.extend(call_event_fn(v, args_spec, key=key)) + else: + msg = f"Invalid event: {v}" + raise ValueError(msg) + + # If the input is a callable, create an event chain. + elif isinstance(value, Callable): + events.extend(call_event_fn(value, args_spec, key=key)) + + # Otherwise, raise an error. + else: + msg = f"Invalid event chain: {value}" + raise ValueError(msg) + + # Add args to the event specs if necessary. + events = [ + (e.with_args(get_handler_args(e)) if isinstance(e, EventSpec) else e) + for e in events + ] + + # Return the event chain. + return cls( + events=events, + args_spec=args_spec, + **event_chain_kwargs, + ) + + +@dataclasses.dataclass( + init=True, + frozen=True, +) +class JavascriptHTMLInputElement: + """Interface for a Javascript HTMLInputElement https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement.""" + + value: str = "" + checked: bool = False + + +@dataclasses.dataclass( + init=True, + frozen=True, +) +class JavascriptInputEvent: + """Interface for a Javascript InputEvent https://developer.mozilla.org/en-US/docs/Web/API/InputEvent.""" + + target: JavascriptHTMLInputElement = JavascriptHTMLInputElement() + + +@dataclasses.dataclass( + init=True, + frozen=True, +) +class JavascriptKeyboardEvent: + """Interface for a Javascript KeyboardEvent https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.""" + + key: str = "" + altKey: bool = False # noqa: N815 + ctrlKey: bool = False # noqa: N815 + metaKey: bool = False # noqa: N815 + shiftKey: bool = False # noqa: N815 + + +def input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[str]]: + """Get the value from an input event. + + Args: + e: The input event. + + Returns: + The value from the input event. + """ + return (e.target.value,) + + +def int_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[int]]: + """Get the value from an input event as an int. + + Args: + e: The input event. + + Returns: + The value from the input event as an int. + """ + return (Var("Number").to(FunctionVar).call(e.target.value).to(int),) + + +def float_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[float]]: + """Get the value from an input event as a float. + + Args: + e: The input event. + + Returns: + The value from the input event as a float. + """ + return (Var("Number").to(FunctionVar).call(e.target.value).to(float),) + + +def checked_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[bool]]: + """Get the checked state from an input event. + + Args: + e: The input event. + + Returns: + The checked state from the input event. + """ + return (e.target.checked,) + + +FORM_DATA = Var(_js_expr="form_data") + + +def on_submit_event() -> tuple[Var[dict[str, Any]]]: + """Event handler spec for the on_submit event. + + Returns: + The event handler spec. + """ + return (FORM_DATA,) + + +def on_submit_string_event() -> tuple[Var[dict[str, str]]]: + """Event handler spec for the on_submit event. + + Returns: + The event handler spec. + """ + return (FORM_DATA,) + + +class KeyInputInfo(TypedDict): + """Information about a key input event.""" + + alt_key: bool + ctrl_key: bool + meta_key: bool + shift_key: bool + + +def key_event( + e: ObjectVar[JavascriptKeyboardEvent], +) -> tuple[Var[str], Var[KeyInputInfo]]: + """Get the key from a keyboard event. + + Args: + e: The keyboard event. + + Returns: + The key from the keyboard event. + """ + return ( + e.key.to(str), + Var.create( + { + "alt_key": e.altKey, + "ctrl_key": e.ctrlKey, + "meta_key": e.metaKey, + "shift_key": e.shiftKey, + }, + ).to(KeyInputInfo), + ) + + +@dataclasses.dataclass( + init=True, + frozen=True, +) +class JavascriptMouseEvent: + """Interface for a Javascript MouseEvent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.""" + + button: int = 0 + buttons: list[int] = dataclasses.field(default_factory=list) + clientX: int = 0 # noqa: N815 + clientY: int = 0 # noqa: N815 + altKey: bool = False # noqa: N815 + ctrlKey: bool = False # noqa: N815 + metaKey: bool = False # noqa: N815 + shiftKey: bool = False # noqa: N815 + + +class JavascriptPointerEvent(JavascriptMouseEvent): + """Interface for a Javascript PointerEvent https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent. + + Inherits from JavascriptMouseEvent. + """ + + +class MouseEventInfo(TypedDict): + """Information about a mouse event.""" + + button: int + buttons: int + client_x: int + client_y: int + alt_key: bool + ctrl_key: bool + meta_key: bool + shift_key: bool + + +class PointerEventInfo(MouseEventInfo): + """Information about a pointer event.""" + + +def pointer_event_spec( + e: ObjectVar[JavascriptPointerEvent], +) -> tuple[Var[PointerEventInfo]]: + """Get the pointer event information. + + Args: + e: The pointer event. + + Returns: + The pointer event information. + """ + return ( + Var.create( + { + "button": e.button, + "buttons": e.buttons, + "client_x": e.clientX, + "client_y": e.clientY, + "alt_key": e.altKey, + "ctrl_key": e.ctrlKey, + "meta_key": e.metaKey, + "shift_key": e.shiftKey, + }, + ).to(PointerEventInfo), + ) + + +def no_args_event_spec() -> tuple[()]: + """Empty event handler. + + Returns: + An empty tuple. + """ + return () + + +T = TypeVar("T") +U = TypeVar("U") + + +class IdentityEventReturn(Generic[T], Protocol): + """Protocol for an identity event return.""" + + def __call__(self, *values: Var[T]) -> tuple[Var[T], ...]: + """Return the input values. + + Args: + *values: The values to return. + + Returns: + The input values. + """ + return values + + +@overload +def passthrough_event_spec( # pyright: ignore [reportOverlappingOverload] + event_type: type[T], / +) -> Callable[[Var[T]], tuple[Var[T]]]: ... + + +@overload +def passthrough_event_spec( + event_type_1: type[T], event_type2: type[U], / +) -> Callable[[Var[T], Var[U]], tuple[Var[T], Var[U]]]: ... + + +@overload +def passthrough_event_spec(*event_types: type[T]) -> IdentityEventReturn[T]: ... + + +def passthrough_event_spec(*event_types: type[T]) -> IdentityEventReturn[T]: # pyright: ignore [reportInconsistentOverload] + """A helper function that returns the input event as output. + + Args: + *event_types: The types of the events. + + Returns: + A function that returns the input event as output. + """ + + def inner(*values: Var[T]) -> tuple[Var[T], ...]: + return values + + inner_type = tuple(Var[event_type] for event_type in event_types) + return_annotation = tuple[inner_type] + + inner.__signature__ = inspect.signature(inner).replace( # pyright: ignore [reportFunctionMemberAccess] + parameters=[ + inspect.Parameter( + f"ev_{i}", + kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, + annotation=Var[event_type], + ) + for i, event_type in enumerate(event_types) + ], + return_annotation=return_annotation, + ) + for i, event_type in enumerate(event_types): + inner.__annotations__[f"ev_{i}"] = Var[event_type] + inner.__annotations__["return"] = return_annotation + + return inner + + +@dataclasses.dataclass( + init=True, + frozen=True, +) +class FileUpload: + """Class to represent a file upload.""" + + upload_id: str | None = None + on_upload_progress: EventHandler | Callable | None = None + extra_headers: dict[str, str] | None = None + + @staticmethod + def on_upload_progress_args_spec(_prog: Var[dict[str, int | float | bool]]): + """Args spec for on_upload_progress event handler. + + Returns: + The arg mapping passed to backend event handler + """ + return [_prog] + + def _as_event_spec( + self, + handler: EventHandler, + *, + client_handler_name: str, + upload_param_name: str, + ) -> EventSpec: + """Create an upload EventSpec. + + Args: + handler: The event handler. + client_handler_name: The client handler name. + upload_param_name: The upload argument name in the event handler. + + Returns: + The upload EventSpec. + + Raises: + ValueError: If the on_upload_progress is not a valid event handler. + """ + from reflex_components_core.core.upload import ( + DEFAULT_UPLOAD_ID, + upload_files_context_var_data, + ) + + upload_id = self.upload_id if self.upload_id is not None else DEFAULT_UPLOAD_ID + upload_files_var = Var( + _js_expr="filesById", + _var_type=dict[str, Any], + _var_data=VarData.merge(upload_files_context_var_data), + ).to(ObjectVar)[LiteralVar.create(upload_id)] + spec_args = [ + ( + Var(_js_expr="files"), + upload_files_var, + ), + ( + Var(_js_expr="upload_param_name"), + LiteralVar.create(upload_param_name), + ), + ( + Var(_js_expr="upload_id"), + LiteralVar.create(upload_id), + ), + ( + Var(_js_expr="extra_headers"), + LiteralVar.create( + self.extra_headers if self.extra_headers is not None else {} + ), + ), + ] + if upload_param_name != "files": + spec_args.insert( + 1, + ( + Var(_js_expr=upload_param_name), + upload_files_var, + ), + ) + if self.on_upload_progress is not None: + on_upload_progress = self.on_upload_progress + if isinstance(on_upload_progress, EventHandler): + events = [ + call_event_handler( + on_upload_progress, + self.on_upload_progress_args_spec, + ), + ] + elif isinstance(on_upload_progress, Callable): + # Call the lambda to get the event chain. + events = call_event_fn( + on_upload_progress, self.on_upload_progress_args_spec + ) + else: + msg = f"{on_upload_progress} is not a valid event handler." + raise ValueError(msg) + if isinstance(events, Var): + msg = f"{on_upload_progress} cannot return a var {events}." + raise ValueError(msg) + on_upload_progress_chain = EventChain( + events=[*events], + args_spec=self.on_upload_progress_args_spec, + ) + formatted_chain = str(format.format_prop(on_upload_progress_chain)) + spec_args.append( + ( + Var(_js_expr="on_upload_progress"), + FunctionStringVar( + formatted_chain.strip("{}"), + ).to(FunctionVar, EventChain), + ), + ) + return EventSpec( + handler=handler, + client_handler_name=client_handler_name, + args=tuple(spec_args), + event_actions=handler.event_actions.copy(), + ) + + def as_event_spec(self, handler: EventHandler) -> EventSpec: + """Get the EventSpec for the file upload. + + Args: + handler: The event handler. + + Returns: + The event spec for the handler. + """ + from reflex_core.utils.exceptions import UploadValueError + + try: + upload_param_name, _annotation = resolve_upload_handler_param(handler) + except UploadValueError: + upload_param_name = "files" + return self._as_event_spec( + handler, + client_handler_name=UPLOAD_FILES_CLIENT_HANDLER, + upload_param_name=upload_param_name, + ) + + +# Alias for rx.upload_files +upload_files = FileUpload + + +@dataclasses.dataclass( + init=True, + frozen=True, +) +class UploadFilesChunk(FileUpload): + """Class to represent a streaming file upload.""" + + def as_event_spec(self, handler: EventHandler) -> EventSpec: + """Get the EventSpec for the streaming file upload. + + Args: + handler: The event handler. + + Returns: + The event spec for the handler. + """ + upload_param_name, _annotation = resolve_upload_chunk_handler_param(handler) + return self._as_event_spec( + handler, + client_handler_name=UPLOAD_FILES_CLIENT_HANDLER, + upload_param_name=upload_param_name, + ) + + +# Alias for rx.upload_files_chunk +upload_files_chunk = UploadFilesChunk + + +# Special server-side events. +def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec: + """A server-side event. + + Args: + name: The name of the event. + sig: The function signature of the event. + **kwargs: The arguments to pass to the event. + + Returns: + An event spec for a server-side event. + """ + + def fn(): + return None + + fn.__qualname__ = name + fn.__signature__ = sig # pyright: ignore [reportFunctionMemberAccess] + return EventSpec( + handler=EventHandler(fn=fn, state_full_name=FRONTEND_EVENT_STATE), + args=tuple( + ( + Var(_js_expr=k), + LiteralVar.create(v), + ) + for k, v in kwargs.items() + ), + ) + + +@overload +def redirect( + path: str | Var[str], + *, + is_external: Literal[False] = False, + replace: bool = False, +) -> EventSpec: ... + + +@overload +def redirect( + path: str | Var[str], + *, + is_external: Literal[True], + popup: bool = False, +) -> EventSpec: ... + + +def redirect( + path: str | Var[str], + *, + is_external: bool = False, + popup: bool = False, + replace: bool = False, +) -> EventSpec: + """Redirect to a new path. + + Args: + path: The path to redirect to. + is_external: Whether to open in new tab or not. + popup: Whether to open in a new window 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=is_external, + popup=popup, + replace=replace, + ) + + +def console_log(message: str | Var[str]) -> EventSpec: + """Do a console.log on the browser. + + Args: + message: The message to log. + + Returns: + An event to log the message. + """ + return run_script(Var("console").to(dict).log.to(FunctionVar).call(message)) + + +@once +def noop() -> EventSpec: + """Do nothing. + + Returns: + An event to do nothing. + """ + return run_script(Var.create(None)) + + +def back() -> EventSpec: + """Do a history.back on the browser. + + Returns: + An event to go back one page. + """ + return run_script( + Var("window").to(dict).history.to(dict).back.to(FunctionVar).call() + ) + + +def window_alert(message: str | Var[str]) -> EventSpec: + """Create a window alert on the browser. + + Args: + message: The message to alert. + + Returns: + An event to alert the message. + """ + return run_script(Var("window").to(dict).alert.to(FunctionVar).call(message)) + + +def set_focus(ref: str) -> EventSpec: + """Set focus to specified ref. + + Args: + ref: The ref. + + Returns: + An event to set focus on the ref + """ + return server_side( + "_set_focus", + get_fn_signature(set_focus), + ref=LiteralVar.create(format.format_ref(ref)), + ) + + +def blur_focus(ref: str) -> EventSpec: + """Blur focus of specified ref. + + Args: + ref: The ref. + + Returns: + An event to blur focus on the ref + """ + return server_side( + "_blur_focus", + get_fn_signature(blur_focus), + ref=LiteralVar.create(format.format_ref(ref)), + ) + + +def scroll_to(elem_id: str, align_to_top: bool | Var[bool] = True) -> EventSpec: + """Select the id of a html element for scrolling into view. + + Args: + elem_id: The id of the element to scroll to. + align_to_top: Whether to scroll to the top (True) or bottom (False) of the element. + + Returns: + An EventSpec to scroll the page to the selected element. + """ + get_element_by_id = FunctionStringVar.create("document.getElementById") + + return run_script( + get_element_by_id + .call(elem_id) + .to(ObjectVar) + .scrollIntoView.to(FunctionVar) + .call(align_to_top), + ) + + +def set_value(ref: str, value: Any) -> EventSpec: + """Set the value of a ref. + + Args: + ref: The ref. + value: The value to set. + + Returns: + An event to set the ref. + """ + return server_side( + "_set_value", + get_fn_signature(set_value), + ref=LiteralVar.create(format.format_ref(ref)), + value=value, + ) + + +def remove_cookie(key: str, options: dict[str, Any] | None = None) -> EventSpec: + """Remove a cookie on the frontend. + + Args: + key: The key identifying the cookie to be removed. + options: Support all the cookie options from RFC 6265 + + Returns: + EventSpec: An event to remove a cookie. + """ + options = options or {} + options["path"] = options.get("path", "/") + return server_side( + "_remove_cookie", + get_fn_signature(remove_cookie), + key=key, + options=options, + ) + + +def clear_local_storage() -> EventSpec: + """Set a value in the local storage on the frontend. + + Returns: + EventSpec: An event to clear the local storage. + """ + return server_side( + "_clear_local_storage", + get_fn_signature(clear_local_storage), + ) + + +def remove_local_storage(key: str) -> EventSpec: + """Set a value in the local storage on the frontend. + + Args: + key: The key identifying the variable in the local storage to remove. + + Returns: + EventSpec: An event to remove an item based on the provided key in local storage. + """ + return server_side( + "_remove_local_storage", + get_fn_signature(remove_local_storage), + key=key, + ) + + +def clear_session_storage() -> EventSpec: + """Set a value in the session storage on the frontend. + + Returns: + EventSpec: An event to clear the session storage. + """ + return server_side( + "_clear_session_storage", + get_fn_signature(clear_session_storage), + ) + + +def remove_session_storage(key: str) -> EventSpec: + """Set a value in the session storage on the frontend. + + Args: + key: The key identifying the variable in the session storage to remove. + + Returns: + EventSpec: An event to remove an item based on the provided key in session storage. + """ + return server_side( + "_remove_session_storage", + get_fn_signature(remove_session_storage), + key=key, + ) + + +def set_clipboard(content: str | Var[str]) -> EventSpec: + """Set the text in content in the clipboard. + + Args: + content: The text to add to clipboard. + + Returns: + EventSpec: An event to set some content in the clipboard. + """ + return run_script( + Var("navigator") + .to(dict) + .clipboard.to(dict) + .writeText.to(FunctionVar) + .call(content) + ) + + +def download( + url: str | Var | None = None, + filename: str | Var | None = None, + data: str | bytes | Var | None = None, + mime_type: str | Var | None = None, +) -> EventSpec: + """Download the file at a given path or with the specified data. + + Args: + url: The URL to the file to download. + filename: The name that the file should be saved as after download. + data: The data to download. + mime_type: The mime type of the data to download. + + Returns: + EventSpec: An event to download the associated file. + + Raises: + ValueError: If the URL provided is invalid, both URL and data are provided, + or the data is not an expected type. + """ + from reflex_components_core.core.cond import cond + + if isinstance(url, str): + if not url.startswith("/"): + msg = "The URL argument should start with a /" + raise ValueError(msg) + + # if filename is not provided, infer it from url + if filename is None: + filename = url.rpartition("/")[-1] + + if filename is None: + filename = "" + + if data is not None: + if url is not None: + msg = "Cannot provide both URL and data to download." + raise ValueError(msg) + + if isinstance(data, str): + if mime_type is None: + mime_type = "text/plain" + # Caller provided a plain text string to download. + url = f"data:{mime_type};base64," + b64encode(data.encode("utf-8")).decode( + "utf-8" + ) + elif isinstance(data, Var): + if mime_type is None: + mime_type = "text/plain" + # Need to check on the frontend if the Var already looks like a data: URI. + + is_data_url = (data.js_type() == "string") & ( + data.to(str).startswith("data:") + ) + + # If it's a data: URI, use it as is, otherwise convert the Var to JSON in a data: URI. + url = cond( + is_data_url, + data.to(str), + f"data:{mime_type}," + data.to_string(), + ) + elif isinstance(data, bytes): + if mime_type is None: + mime_type = "application/octet-stream" + # Caller provided bytes, so base64 encode it as a data: URI. + b64_data = b64encode(data).decode("utf-8") + url = f"data:{mime_type};base64," + b64_data + else: + msg = f"Invalid data type {type(data)} for download. Use `str` or `bytes`." + raise ValueError(msg) + + return server_side( + "_download", + get_fn_signature(download), + url=url, + filename=filename, + ) + + +def call_script( + javascript_code: str | Var[str], + callback: "EventType[Any] | None" = None, +) -> EventSpec: + """Create an event handler that executes arbitrary javascript code. + + Args: + javascript_code: The code to execute. + callback: EventHandler that will receive the result of evaluating the javascript code. + + Returns: + EventSpec: An event that will execute the client side javascript. + """ + callback_kwargs = {"callback": None} + if callback is not None: + callback_kwargs = { + "callback": str( + format.format_queue_events( + callback, + args_spec=lambda result: [result], + ) + ), + } + if isinstance(javascript_code, str): + # When there is VarData, include it and eval the JS code inline on the client. + javascript_code, original_code = ( + LiteralVar.create(javascript_code), + javascript_code, + ) + if not javascript_code._get_all_var_data(): + # Without VarData, cast to string and eval the code in the event loop. + javascript_code = str(Var(_js_expr=original_code)) + + return server_side( + "_call_script", + get_fn_signature(call_script), + javascript_code=javascript_code, + **callback_kwargs, + ) + + +def call_function( + javascript_code: str | Var, + callback: "EventType[Any] | None" = None, +) -> EventSpec: + """Create an event handler that executes arbitrary javascript code. + + Args: + javascript_code: The code to execute. + callback: EventHandler that will receive the result of evaluating the javascript code. + + Returns: + EventSpec: An event that will execute the client side javascript. + """ + callback_kwargs = {"callback": None} + if callback is not None: + callback_kwargs = { + "callback": str( + format.format_queue_events( + callback, + args_spec=lambda result: [result], + ), + ) + } + + javascript_code = ( + Var(javascript_code) if isinstance(javascript_code, str) else javascript_code + ) + + return server_side( + "_call_function", + get_fn_signature(call_function), + function=javascript_code, + **callback_kwargs, + ) + + +def run_script( + javascript_code: str | Var, + callback: "EventType[Any] | None" = None, +) -> EventSpec: + """Create an event handler that executes arbitrary javascript code. + + Args: + javascript_code: The code to execute. + callback: EventHandler that will receive the result of evaluating the javascript code. + + Returns: + EventSpec: An event that will execute the client side javascript. + """ + javascript_code = ( + Var(javascript_code) if isinstance(javascript_code, str) else javascript_code + ) + + return call_function(ArgsFunctionOperation.create((), javascript_code), callback) + + +def get_event(state: "BaseState", event: str): + """Get the event from the given state. + + Args: + state: The state. + event: The event. + + Returns: + The event. + """ + return f"{state.get_name()}.{event}" + + +def get_hydrate_event(state: "BaseState") -> str: + """Get the name of the hydrate event for the state. + + Args: + state: The state. + + Returns: + The name of the hydrate event. + """ + return get_event(state, constants.CompileVars.HYDRATE) + + +def _values_returned_from_event(event_spec_annotations: list[Any]) -> list[Any]: + return [ + event_spec_return_type + for event_spec_annotation in event_spec_annotations + if (event_spec_return_type := event_spec_annotation.get("return")) is not None + and get_origin(event_spec_return_type) is tuple + ] + + +def _check_event_args_subclass_of_callback( + callback_params_names: list[str], + provided_event_types: list[Any], + callback_param_name_to_type: dict[str, Any], + callback_name: str = "", + key: str = "", +): + """Check if the event handler arguments are subclass of the callback. + + Args: + callback_params_names: The names of the callback parameters. + provided_event_types: The event types. + callback_param_name_to_type: The callback parameter name to type mapping. + callback_name: The name of the callback. + key: The key. + + Raises: + TypeError: If the event handler arguments are invalid. + EventHandlerArgTypeMismatchError: If the event handler arguments do not match the callback. + + # noqa: DAR401 delayed_exceptions[] + # noqa: DAR402 EventHandlerArgTypeMismatchError + """ + from reflex_core.utils import console + + type_match_found: dict[str, bool] = {} + delayed_exceptions: list[EventHandlerArgTypeMismatchError] = [] + + for event_spec_index, event_spec_return_type in enumerate(provided_event_types): + args = get_args(event_spec_return_type) + + args_types_without_vars = [ + arg if get_origin(arg) is not Var else get_args(arg)[0] for arg in args + ] + + # check that args of event handler are matching the spec if type hints are provided + for i, arg in enumerate(callback_params_names[: len(args_types_without_vars)]): + if arg not in callback_param_name_to_type: + continue + + type_match_found.setdefault(arg, False) + + try: + compare_result = typehint_issubclass( + args_types_without_vars[i], callback_param_name_to_type[arg] + ) + except TypeError as te: + callback_name_context = f" of {callback_name}" if callback_name else "" + key_context = f" for {key}" if key else "" + msg = f"Could not compare types {args_types_without_vars[i]} and {callback_param_name_to_type[arg]} for argument {arg}{callback_name_context}{key_context}." + raise TypeError(msg) from te + + if compare_result: + type_match_found[arg] = True + continue + type_match_found[arg] = False + as_annotated_in = ( + f" as annotated in {callback_name}" if callback_name else "" + ) + delayed_exceptions.append( + EventHandlerArgTypeMismatchError( + f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {callback_param_name_to_type[arg]}{as_annotated_in} instead." + ) + ) + + if all(type_match_found.values()): + delayed_exceptions.clear() + if event_spec_index: + args = get_args(provided_event_types[0]) + + args_types_without_vars = [ + arg if get_origin(arg) is not Var else get_args(arg)[0] + for arg in args + ] + + expect_string = ", ".join( + repr(arg) for arg in args_types_without_vars + ).replace("[", "\\[") + + given_string = ", ".join( + repr(callback_param_name_to_type.get(arg, Any)) + for arg in callback_params_names + ).replace("[", "\\[") + + as_annotated_in = ( + f" as annotated in {callback_name}" if callback_name else "" + ) + + console.warn( + f"Event handler {key} expects ({expect_string}) -> () but got ({given_string}) -> (){as_annotated_in} instead. " + f"This may lead to unexpected behavior but is intentionally ignored for {key}." + ) + break + + if delayed_exceptions: + raise delayed_exceptions[0] + + +def call_event_handler( + event_callback: EventHandler | EventSpec, + event_spec: ArgsSpec | Sequence[ArgsSpec], + key: str | None = None, +) -> EventSpec: + """Call an event handler to get the event spec. + + This function will inspect the function signature of the event handler. + If it takes in an arg, the arg will be passed to the event handler. + Otherwise, the event handler will be called with no args. + + Args: + event_callback: The event handler. + event_spec: The lambda that define the argument(s) to pass to the event handler. + key: The key to pass to the event handler. + + Returns: + The event spec from calling the event handler. + """ + event_spec_args, event_annotations = parse_args_spec(event_spec) + + event_spec_return_types = _values_returned_from_event(event_annotations) + + if isinstance(event_callback, EventSpec): + parameters = event_callback.handler._parameters + + check_fn_match_arg_spec( + event_callback.handler.fn, + parameters, + event_spec_args, + key, + bool(event_callback.handler.state_full_name) + len(event_callback.args), + event_callback.handler.fn.__qualname__, + ) + + event_callback_spec_args = list(parameters) + + try: + type_hints_of_provided_callback = get_type_hints(event_callback.handler.fn) + except NameError: + type_hints_of_provided_callback = {} + + argument_names = [str(arg) for arg, value in event_callback.args] + + _check_event_args_subclass_of_callback( + [ + arg + for arg in event_callback_spec_args[ + bool(event_callback.handler.state_full_name) : + ] + if arg not in argument_names + ], + event_spec_return_types, + type_hints_of_provided_callback, + event_callback.handler.fn.__qualname__, + key or "", + ) + + # Handle partial application of EventSpec args + return event_callback.add_args(*event_spec_args) + + parameters = event_callback._parameters + + check_fn_match_arg_spec( + event_callback.fn, + parameters, + event_spec_args, + key, + bool(event_callback.state_full_name), + event_callback.fn.__qualname__, + ) + + if event_spec_return_types: + event_callback_spec_args = list(parameters) + + try: + type_hints_of_provided_callback = get_type_hints(event_callback.fn) + except NameError: + type_hints_of_provided_callback = {} + + _check_event_args_subclass_of_callback( + event_callback_spec_args[1:], + event_spec_return_types, + type_hints_of_provided_callback, + event_callback.fn.__qualname__, + key or "", + ) + + return event_callback(*event_spec_args) + + +def unwrap_var_annotation(annotation: GenericType): + """Unwrap a Var annotation or return it as is if it's not Var[X]. + + Args: + annotation: The annotation to unwrap. + + Returns: + The unwrapped annotation. + """ + if get_origin(annotation) in (Var, ObjectVar) and (args := get_args(annotation)): + return args[0] + return annotation + + +def resolve_annotation(annotations: dict[str, Any], arg_name: str, spec: ArgsSpec): + """Resolve the annotation for the given argument name. + + Args: + annotations: The annotations. + arg_name: The argument name. + spec: The specs which the annotations come from. + + Returns: + The resolved annotation. + + Raises: + MissingAnnotationError: If the annotation is missing for non-lambda methods. + """ + annotation = annotations.get(arg_name) + if annotation is None: + if not isinstance(spec, types.LambdaType): + raise MissingAnnotationError(var_name=arg_name) + return dict[str, dict] + return annotation + + +@lru_cache +def parse_args_spec(arg_spec: ArgsSpec | Sequence[ArgsSpec]): + """Parse the args provided in the ArgsSpec of an event trigger. + + Args: + arg_spec: The spec of the args. + + Returns: + The parsed args. + """ + # if there's multiple, the first is the default + if isinstance(arg_spec, Sequence): + annotations = [get_type_hints(one_arg_spec) for one_arg_spec in arg_spec] + arg_spec = arg_spec[0] + else: + annotations = [get_type_hints(arg_spec)] + + spec = inspect.getfullargspec(arg_spec) + + return list( + arg_spec(*[ + Var(f"_{l_arg}").to( + unwrap_var_annotation( + resolve_annotation(annotations[0], l_arg, spec=arg_spec) + ) + ) + for l_arg in spec.args + ]) + ), annotations + + +def args_specs_from_fields( + fields_dict: Mapping[str, BaseField], +) -> dict[str, ArgsSpec | Sequence[ArgsSpec]]: + """Get the event triggers and arg specs from the given fields. + + Args: + fields_dict: The fields, keyed by name + + Returns: + The args spec for any field annotated as EventHandler. + """ + return { + name: ( + metadata[0] + if ( + (metadata := getattr(field.annotated_type, "__metadata__", None)) + is not None + ) + else no_args_event_spec + ) + for name, field in fields_dict.items() + if field.type_origin is EventHandler + } + + +def check_fn_match_arg_spec( + user_func: Callable, + user_func_parameters: Mapping[str, inspect.Parameter], + event_spec_args: Sequence[Var], + key: str | None = None, + number_of_bound_args: int = 0, + func_name: str | None = None, +): + """Ensures that the function signature matches the passed argument specification + or raises an EventFnArgMismatchError if they do not. + + Args: + user_func: The function to be validated. + user_func_parameters: The parameters of the function to be validated. + event_spec_args: The argument specification for the event trigger. + key: The key of the event trigger. + number_of_bound_args: The number of bound arguments to the function. + func_name: The name of the function to be validated. + + Raises: + EventFnArgMismatchError: Raised if the number of mandatory arguments do not match + """ + user_args = list(user_func_parameters) + # Drop the first argument if it's a bound method + if inspect.ismethod(user_func) and user_func.__self__ is not None: + user_args = user_args[1:] + + user_default_args = [ + p.default + for p in user_func_parameters.values() + if p.default is not inspect.Parameter.empty + ] + number_of_user_args = len(user_args) - number_of_bound_args + number_of_user_default_args = len(user_default_args) if user_default_args else 0 + + number_of_event_args = len(event_spec_args) + + if number_of_user_args - number_of_user_default_args > number_of_event_args: + msg = ( + f"Event {key} only provides {number_of_event_args} arguments, but " + f"{func_name or user_func} requires at least {number_of_user_args - number_of_user_default_args} " + "arguments to be passed to the event handler.\n" + "See https://reflex.dev/docs/events/event-arguments/" + ) + raise EventFnArgMismatchError(msg) + + +def call_event_fn( + fn: Callable, + arg_spec: ArgsSpec | Sequence[ArgsSpec], + key: str | None = None, +) -> list["EventSpec | FunctionVar | EventVar"]: + """Call a function to a list of event specs. + + The function should return a single event-like value or a heterogeneous + sequence of event-like values. + + Args: + fn: The function to call. + arg_spec: The argument spec for the event trigger. + key: The key to pass to the event handler. + + Returns: + The event-like values from calling the function. + + Raises: + EventHandlerValueError: If the lambda returns an unusable value. + """ + # Import here to avoid circular imports. + from reflex_core.event import EventHandler, EventSpec + from reflex_core.utils.exceptions import EventHandlerValueError + + parsed_args, _ = parse_args_spec(arg_spec) + + parameters = inspect.signature(fn).parameters + + # Check that fn signature matches arg_spec + check_fn_match_arg_spec(fn, parameters, parsed_args, key=key) + + number_of_fn_args = len(parameters) + + # Call the function with the parsed args. + out = fn(*[*parsed_args][:number_of_fn_args]) + + # Normalize common heterogeneous event collections into individual events + # while keeping other scalar values for validation below. + out = list(out) if isinstance(out, (list, tuple)) else [out] + + # Convert any event specs to event specs. + events = [] + for e in out: + if isinstance(e, EventHandler): + # An un-called EventHandler gets all of the args of the event trigger. + e = call_event_handler(e, arg_spec, key=key) + + if isinstance(e, EventChain): + # Nested EventChain is treated like a FunctionVar. + e = Var.create(e) + + # Make sure the event spec is valid. + if not isinstance(e, (EventSpec, FunctionVar, EventVar)): + hint = "" + if isinstance(e, VarOperationCall): + hint = " Hint: use `fn.partial(...)` instead of calling the FunctionVar directly." + msg = ( + f"Invalid event chain for {key}: {fn} -> {e}: A lambda inside an EventChain " + "list must return `EventSpec | EventHandler | EventChain | EventVar | FunctionVar` " + "or a heterogeneous sequence of these types. " + f"Got: {type(e)}.{hint}" + ) + raise EventHandlerValueError(msg) + + # Add the event spec to the chain. + events.append(e) + + # Return the events. + return events + + +def get_handler_args( + event_spec: EventSpec, +) -> tuple[tuple[Var, Var], ...]: + """Get the handler args for the given event spec. + + Args: + event_spec: The event spec. + + Returns: + The handler args. + """ + args = event_spec.handler._parameters + + return event_spec.args if len(args) > 1 else () + + +def fix_events( + events: list[EventSpec | EventHandler] | None, + token: str, + router_data: dict[str, Any] | None = None, +) -> list[Event]: + """Fix a list of events returned by an event handler. + + Args: + events: The events to fix. + token: The user token. + router_data: The optional router data to set in the event. + + Returns: + The fixed events. + + Raises: + ValueError: If the event type is not what was expected. + """ + # If the event handler returns nothing, return an empty list. + if events is None: + return [] + + # If the handler returns a single event, wrap it in a list. + if not isinstance(events, list): + events = [events] + + # Fix the events created by the handler. + out = [] + for e in events: + if callable(e) and getattr(e, "__name__", "") == "": + # A lambda was returned, assume the user wants to call it with no args. + e = e() + if isinstance(e, Event): + # If the event is already an event, append it to the list. + out.append(e) + continue + if not isinstance(e, (EventHandler, EventSpec)): + e = EventHandler(fn=e) + # Otherwise, create an event from the event spec. + if isinstance(e, EventHandler): + e = e() + if not isinstance(e, EventSpec): + msg = f"Unexpected event type, {type(e)}." + raise ValueError(msg) + name = format.format_event_handler(e.handler) + payload = {k._js_expr: v._decode() for k, v in e.args} + + # Filter router_data to reduce payload size + event_router_data = { + k: v + for k, v in (router_data or {}).items() + if k in constants.route.ROUTER_DATA_INCLUDE + } + # Create an event and append it to the list. + out.append( + Event( + token=token, + name=name, + payload=payload, + router_data=event_router_data, + ) + ) + + return out + + +def get_fn_signature(fn: Callable) -> inspect.Signature: + """Get the signature of a function. + + Args: + fn: The function. + + Returns: + The signature of the function. + """ + signature = inspect.signature(fn) + new_param = inspect.Parameter( + FRONTEND_EVENT_STATE, inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Any + ) + return signature.replace(parameters=(new_param, *signature.parameters.values())) + + +# These chains can be used for their side effects when no other events are desired. +stop_propagation = noop().stop_propagation +prevent_default = noop().prevent_default + + +class EventVar(ObjectVar, python_types=(EventSpec, EventHandler)): + """Base class for event vars.""" + + def bool(self) -> NoReturn: + """Get the boolean value of the var. + + Raises: + TypeError: EventVar cannot be converted to a boolean. + """ + msg = f"Cannot convert {self._js_expr} of type {type(self).__name__} to bool." + raise TypeError(msg) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralEventVar(VarOperationCall, LiteralVar, EventVar): + """A literal event var.""" + + _var_value: EventSpec = dataclasses.field(default=None) # pyright: ignore [reportAssignmentType] + + def __hash__(self) -> int: + """Get the hash of the var. + + Returns: + The hash of the var. + """ + return hash((type(self).__name__, self._js_expr)) + + @classmethod + def create( + cls, + value: EventSpec | EventHandler, + _var_data: VarData | None = None, + ) -> "LiteralEventVar": + """Create a new LiteralEventVar instance. + + Args: + value: The value of the var. + _var_data: The data of the var. + + Returns: + The created LiteralEventVar instance. + + Raises: + EventFnArgMismatchError: If the event handler takes arguments. + """ + if isinstance(value, EventHandler): + + def no_args(): + return () + + try: + value = call_event_handler(value, no_args) + except EventFnArgMismatchError: + msg = f"Event handler {value.fn.__qualname__} used inside of a rx.cond() must not take any arguments." + raise EventFnArgMismatchError(msg) from None + + return cls( + _js_expr="", + _var_type=EventSpec, + _var_data=_var_data, + _var_value=value, + _func=FunctionStringVar("ReflexEvent"), + _args=( + # event handler name + ".".join( + filter( + None, + format.get_event_handler_parts(value.handler), + ) + ), + # event handler args + {str(name): value for name, value in value.args}, + # event actions + value.event_actions, + # client handler name + *([value.client_handler_name] if value.client_handler_name else []), + ), + ) + + +class EventChainVar(BuilderFunctionVar, python_types=EventChain): + """Base class for event chain vars.""" + + def bool(self) -> NoReturn: + """Get the boolean value of the var. + + Raises: + TypeError: EventChainVar cannot be converted to a boolean. + """ + msg = f"Cannot convert {self._js_expr} of type {type(self).__name__} to bool." + raise TypeError(msg) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +# Note: LiteralVar is second in the inheritance list allowing it act like a +# CachedVarOperation (ArgsFunctionOperation) and get the _js_expr from the +# _cached_var_name property. +class LiteralEventChainVar(ArgsFunctionOperationBuilder, LiteralVar, EventChainVar): + """A literal event chain var.""" + + _var_value: EventChain = dataclasses.field(default=None) # pyright: ignore [reportAssignmentType] + + def __hash__(self) -> int: + """Get the hash of the var. + + Returns: + The hash of the var. + """ + return hash((type(self).__name__, self._js_expr)) + + @classmethod + def create( + cls, + value: EventChain, + _var_data: VarData | None = None, + ) -> "LiteralEventChainVar": + """Create a new LiteralEventChainVar instance. + + Args: + value: The value of the var. + _var_data: The data of the var. + + Returns: + The created LiteralEventChainVar instance. + + Raises: + ValueError: If the invocation is not a FunctionVar. + """ + arg_spec = ( + value.args_spec[0] + if isinstance(value.args_spec, Sequence) + else value.args_spec + ) + sig = inspect.signature(arg_spec) # pyright: ignore [reportArgumentType] + arg_vars = () + if sig.parameters: + arg_def = tuple(f"_{p}" for p in sig.parameters) + arg_vars = tuple(Var(_js_expr=arg) for arg in arg_def) + arg_def_expr = LiteralVar.create(list(arg_vars)) + else: + # add a default argument for addEvents if none were specified in value.args_spec + # used to trigger the preventDefault() on the event. + arg_def = ("...args",) + arg_def_expr = Var(_js_expr="args") + + if value.invocation is None: + invocation = FunctionStringVar.create( + CompileVars.ADD_EVENTS, + _var_data=VarData( + imports=Imports.EVENTS, + hooks={Hooks.EVENTS: None}, + ), + ) + else: + invocation = value.invocation + + if invocation is not None and not isinstance(invocation, FunctionVar): + msg = f"EventChain invocation must be a FunctionVar, got {invocation!s} of type {invocation._var_type!s}." + raise ValueError(msg) + assert invocation is not None + + call_args = arg_vars if sig.parameters else (Var(_js_expr="...args"),) + statements = [ + ( + event.call(*call_args) + if isinstance(event, FunctionVar) + else invocation.call( + LiteralVar.create([LiteralVar.create(event)]), + arg_def_expr, + _EMPTY_EVENT_ACTIONS, + ) + ) + for event in value.events + ] + + if not statements: + statements.append( + invocation.call( + _EMPTY_EVENTS, + arg_def_expr, + _EMPTY_EVENT_ACTIONS, + ) + ) + + if len(statements) == 1 and not value.event_actions: + return_expr = statements[0] + else: + statement_block = Var( + _js_expr=f"{{{''.join(f'{statement};' for statement in statements)}}}", + ) + if value.event_actions: + apply_event_actions = FunctionStringVar.create( + CompileVars.APPLY_EVENT_ACTIONS, + _var_data=VarData( + imports=Imports.EVENTS, + hooks={Hooks.EVENTS: None}, + ), + ) + return_expr = apply_event_actions.call( + ArgsFunctionOperation.create((), statement_block), + value.event_actions, + *call_args, + ) + else: + return_expr = statement_block + + return cls( + _js_expr="", + _var_type=EventChain, + _var_data=_var_data, + _args=FunctionArgs(arg_def), + _return_expr=return_expr, + _var_value=value, + ) + + +P = TypeVarTuple("P") +Q = TypeVarTuple("Q") +V = TypeVar("V") +V2 = TypeVar("V2") +V3 = TypeVar("V3") +V4 = TypeVar("V4") +V5 = TypeVar("V5") + + +class EventCallback(Generic[Unpack[P]], EventActionsMixin): + """A descriptor that wraps a function to be used as an event.""" + + def __init__(self, func: Callable[[Any, Unpack[P]], Any]): + """Initialize the descriptor with the function to be wrapped. + + Args: + func: The function to be wrapped. + """ + self.func = func + + @overload + def __call__( + self: "EventCallback[Unpack[Q]]", + ) -> "EventCallback[Unpack[Q]]": ... + + @overload + def __call__( + self: "EventCallback[V, Unpack[Q]]", value: V | Var[V] + ) -> "EventCallback[Unpack[Q]]": ... + + @overload + def __call__( + self: "EventCallback[V, V2, Unpack[Q]]", + value: V | Var[V], + value2: V2 | Var[V2], + ) -> "EventCallback[Unpack[Q]]": ... + + @overload + def __call__( + self: "EventCallback[V, V2, V3, Unpack[Q]]", + value: V | Var[V], + value2: V2 | Var[V2], + value3: V3 | Var[V3], + ) -> "EventCallback[Unpack[Q]]": ... + + @overload + def __call__( + self: "EventCallback[V, V2, V3, V4, Unpack[Q]]", + value: V | Var[V], + value2: V2 | Var[V2], + value3: V3 | Var[V3], + value4: V4 | Var[V4], + ) -> "EventCallback[Unpack[Q]]": ... + + def __call__(self, *values) -> "EventCallback": # pyright: ignore [reportInconsistentOverload] + """Call the function with the values. + + Args: + *values: The values to call the function with. + + Returns: + The function with the values. + """ + return self.func(*values) # pyright: ignore [reportArgumentType] + + @overload + def __get__( + self: "EventCallback[Unpack[P]]", instance: None, owner: Any + ) -> "EventCallback[Unpack[P]]": ... + + @overload + def __get__(self, instance: Any, owner: Any) -> "Callable[[Unpack[P]]]": ... + + def __get__(self, instance: Any, owner: Any) -> Callable: + """Get the function with the instance bound to it. + + Args: + instance: The instance to bind to the function. + owner: The owner of the function. + + Returns: + The function with the instance bound to it + """ + if instance is None: + return self.func + + return partial(self.func, instance) + + +class LambdaEventCallback(Protocol[Unpack[P]]): + """A protocol for a lambda event callback.""" + + __code__: types.CodeType + + @overload + def __call__(self: "LambdaEventCallback[()]") -> Any: ... + + @overload + def __call__(self: "LambdaEventCallback[V]", value: Var[V], /) -> Any: ... + + @overload + def __call__( + self: "LambdaEventCallback[V, V2]", value: Var[V], value2: Var[V2], / + ) -> Any: ... + + @overload + def __call__( + self: "LambdaEventCallback[V, V2, V3]", + value: Var[V], + value2: Var[V2], + value3: Var[V3], + /, + ) -> Any: ... + + def __call__(self, *args: Var) -> Any: + """Call the lambda with the args. + + Args: + *args: The args to call the lambda with. + """ + + +ARGS = TypeVarTuple("ARGS") + + +LAMBDA_OR_STATE = TypeAliasType( + "LAMBDA_OR_STATE", + LambdaEventCallback[Unpack[ARGS]] | EventCallback[Unpack[ARGS]], + type_params=(ARGS,), +) + +ItemOrList = V | list[V] + +BASIC_EVENT_TYPES = TypeAliasType( + "BASIC_EVENT_TYPES", EventSpec | EventHandler | Var[Any], type_params=() +) + +IndividualEventType = TypeAliasType( + "IndividualEventType", + LAMBDA_OR_STATE[Unpack[ARGS]] | BASIC_EVENT_TYPES, + type_params=(ARGS,), +) + +EventType = TypeAliasType( + "EventType", + ItemOrList[LAMBDA_OR_STATE[Unpack[ARGS]] | BASIC_EVENT_TYPES], + type_params=(ARGS,), +) + + +if TYPE_CHECKING: + from reflex.state import BaseState + + BASE_STATE = TypeVar("BASE_STATE", bound=BaseState) +else: + BASE_STATE = TypeVar("BASE_STATE") + + +class EventNamespace: + """A namespace for event related classes.""" + + # Core Event Classes + Event = Event + EventActionsMixin = EventActionsMixin + EventHandler = EventHandler + EventSpec = EventSpec + CallableEventSpec = CallableEventSpec + EventChain = EventChain + EventVar = EventVar + LiteralEventVar = LiteralEventVar + EventChainVar = EventChainVar + LiteralEventChainVar = LiteralEventChainVar + EventCallback = EventCallback + LambdaEventCallback = LambdaEventCallback + + # Javascript Event Classes + JavascriptHTMLInputElement = JavascriptHTMLInputElement + JavascriptInputEvent = JavascriptInputEvent + JavascriptKeyboardEvent = JavascriptKeyboardEvent + JavascriptMouseEvent = JavascriptMouseEvent + JavascriptPointerEvent = JavascriptPointerEvent + + # Type Info Classes + KeyInputInfo = KeyInputInfo + MouseEventInfo = MouseEventInfo + PointerEventInfo = PointerEventInfo + IdentityEventReturn = IdentityEventReturn + + # File Upload + FileUpload = FileUpload + UploadFilesChunk = UploadFilesChunk + + # Type Aliases + EventType = EventType + LAMBDA_OR_STATE = LAMBDA_OR_STATE + BASIC_EVENT_TYPES = BASIC_EVENT_TYPES + IndividualEventType = IndividualEventType + + # Constants + BACKGROUND_TASK_MARKER = BACKGROUND_TASK_MARKER + EVENT_ACTIONS_MARKER = EVENT_ACTIONS_MARKER + _EVENT_FIELDS = _EVENT_FIELDS + FORM_DATA = FORM_DATA + upload_files = upload_files + upload_files_chunk = upload_files_chunk + stop_propagation = stop_propagation + prevent_default = prevent_default + + # Private/Internal Functions + resolve_upload_handler_param = staticmethod(resolve_upload_handler_param) + resolve_upload_chunk_handler_param = staticmethod( + resolve_upload_chunk_handler_param + ) + _values_returned_from_event = staticmethod(_values_returned_from_event) + _check_event_args_subclass_of_callback = staticmethod( + _check_event_args_subclass_of_callback + ) + + @overload + def __new__( + cls, + func: None = None, + *, + background: bool | None = None, + stop_propagation: bool | None = None, + prevent_default: bool | None = None, + throttle: int | None = None, + debounce: int | None = None, + temporal: bool | None = None, + ) -> ( + "Callable[[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]]]" + ): ... + + @overload + def __new__( + cls, + func: Callable[[BASE_STATE, Unpack[P]], Any], + *, + background: bool | None = None, + stop_propagation: bool | None = None, + prevent_default: bool | None = None, + throttle: int | None = None, + debounce: int | None = None, + temporal: bool | None = None, + ) -> EventCallback[Unpack[P]]: ... + + def __new__( + cls, + func: Callable[[BASE_STATE, Unpack[P]], Any] | None = None, + *, + background: bool | None = None, + stop_propagation: bool | None = None, + prevent_default: bool | None = None, + throttle: int | None = None, + debounce: int | None = None, + temporal: bool | None = None, + ) -> ( + EventCallback[Unpack[P]] + | Callable[[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]]] + ): + """Wrap a function to be used as an event. + + Args: + func: The function to wrap. + background: Whether the event should be run in the background. Defaults to False. + stop_propagation: Whether to stop the event from bubbling up the DOM tree. + prevent_default: Whether to prevent the default behavior of the event. + throttle: Throttle the event handler to limit calls (in milliseconds). + debounce: Debounce the event handler to delay calls (in milliseconds). + temporal: Whether the event should be dropped when the backend is down. + + Returns: + The wrapped function. + + Raises: + TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402 + """ + + def _build_event_actions(): + """Build event_actions dict from decorator parameters. + + Returns: + Dict of event actions to apply, or empty dict if none specified. + """ + if not any([ + stop_propagation, + prevent_default, + throttle, + debounce, + temporal, + ]): + return {} + + event_actions = {} + if stop_propagation is not None: + event_actions["stopPropagation"] = stop_propagation + if prevent_default is not None: + event_actions["preventDefault"] = prevent_default + if throttle is not None: + event_actions["throttle"] = throttle + if debounce is not None: + event_actions["debounce"] = debounce + if temporal is not None: + event_actions["temporal"] = temporal + return event_actions + + def wrapper( + func: Callable[[BASE_STATE, Unpack[P]], T], + ) -> EventCallback[Unpack[P]]: + if background is True: + if not inspect.iscoroutinefunction( + func + ) and not inspect.isasyncgenfunction(func): + msg = "Background task must be async function or generator." + raise TypeError(msg) + setattr(func, BACKGROUND_TASK_MARKER, True) + if getattr(func, "__name__", "").startswith("_"): + msg = "Event handlers cannot be private." + raise ValueError(msg) + + qualname: str | None = getattr(func, "__qualname__", None) + + if qualname and ( + len(func_path := qualname.split(".")) == 1 + or func_path[-2] == "" + ): + from reflex.state import BaseState + + types = get_type_hints(func) + state_arg_name = next(iter(inspect.signature(func).parameters), None) + state_cls = state_arg_name and types.get(state_arg_name) + if state_cls and issubclass(state_cls, BaseState): + name = ( + (func.__module__ + "." + qualname) + .replace(".", "_") + .replace("", "_") + .removeprefix("_") + ) + object.__setattr__(func, "__name__", name) + object.__setattr__(func, "__qualname__", name) + state_cls._add_event_handler(name, func) + event_callback = getattr(state_cls, name) + + # Apply decorator event actions + event_actions = _build_event_actions() + if event_actions: + # Create new EventCallback with updated event_actions + event_callback = dataclasses.replace( + event_callback, event_actions=event_actions + ) + + return event_callback + + # Store decorator event actions on the function for later processing + event_actions = _build_event_actions() + if event_actions: + setattr(func, EVENT_ACTIONS_MARKER, event_actions) + return func # pyright: ignore [reportReturnType] + + if func is not None: + return wrapper(func) + return wrapper + + get_event = staticmethod(get_event) + get_hydrate_event = staticmethod(get_hydrate_event) + fix_events = staticmethod(fix_events) + call_event_handler = staticmethod(call_event_handler) + call_event_fn = staticmethod(call_event_fn) + get_handler_args = staticmethod(get_handler_args) + check_fn_match_arg_spec = staticmethod(check_fn_match_arg_spec) + resolve_annotation = staticmethod(resolve_annotation) + parse_args_spec = staticmethod(parse_args_spec) + args_specs_from_fields = staticmethod(args_specs_from_fields) + unwrap_var_annotation = staticmethod(unwrap_var_annotation) + get_fn_signature = staticmethod(get_fn_signature) + + # Event Spec Functions + passthrough_event_spec = staticmethod(passthrough_event_spec) + input_event = staticmethod(input_event) + int_input_event = staticmethod(int_input_event) + float_input_event = staticmethod(float_input_event) + checked_input_event = staticmethod(checked_input_event) + key_event = staticmethod(key_event) + pointer_event_spec = staticmethod(pointer_event_spec) + no_args_event_spec = staticmethod(no_args_event_spec) + on_submit_event = staticmethod(on_submit_event) + on_submit_string_event = staticmethod(on_submit_string_event) + + # Server Side Events + server_side = staticmethod(server_side) + redirect = staticmethod(redirect) + console_log = staticmethod(console_log) + noop = staticmethod(noop) + back = staticmethod(back) + window_alert = staticmethod(window_alert) + set_focus = staticmethod(set_focus) + blur_focus = staticmethod(blur_focus) + scroll_to = staticmethod(scroll_to) + set_value = staticmethod(set_value) + remove_cookie = staticmethod(remove_cookie) + clear_local_storage = staticmethod(clear_local_storage) + remove_local_storage = staticmethod(remove_local_storage) + clear_session_storage = staticmethod(clear_session_storage) + remove_session_storage = staticmethod(remove_session_storage) + set_clipboard = staticmethod(set_clipboard) + download = staticmethod(download) + call_script = staticmethod(call_script) + call_function = staticmethod(call_function) + run_script = staticmethod(run_script) + __file__ = __file__ + + +event = EventNamespace +event.event = event # pyright: ignore[reportAttributeAccessIssue] +sys.modules[__name__] = event # pyright: ignore[reportArgumentType] diff --git a/packages/reflex-core/src/reflex_core/plugins/__init__.py b/packages/reflex-core/src/reflex_core/plugins/__init__.py new file mode 100644 index 00000000000..754409046b8 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/plugins/__init__.py @@ -0,0 +1,17 @@ +"""Reflex Plugin System.""" + +from ._screenshot import ScreenshotPlugin as _ScreenshotPlugin +from .base import CommonContext, Plugin, PreCompileContext +from .sitemap import SitemapPlugin +from .tailwind_v3 import TailwindV3Plugin +from .tailwind_v4 import TailwindV4Plugin + +__all__ = [ + "CommonContext", + "Plugin", + "PreCompileContext", + "SitemapPlugin", + "TailwindV3Plugin", + "TailwindV4Plugin", + "_ScreenshotPlugin", +] diff --git a/packages/reflex-core/src/reflex_core/plugins/_screenshot.py b/packages/reflex-core/src/reflex_core/plugins/_screenshot.py new file mode 100644 index 00000000000..7b3b1d5e8b7 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/plugins/_screenshot.py @@ -0,0 +1,144 @@ +"""Plugin to enable screenshot functionality.""" + +from typing import TYPE_CHECKING + +from reflex_core.plugins.base import Plugin as BasePlugin + +if TYPE_CHECKING: + from starlette.requests import Request + from starlette.responses import Response + from typing_extensions import Unpack + + from reflex.app import App + from reflex.state import BaseState + from reflex_core.plugins.base import PostCompileContext + +ACTIVE_CONNECTIONS = "/_active_connections" +CLONE_STATE = "/_clone_state" + + +def _deep_copy(state: "BaseState") -> "BaseState": + """Create a deep copy of the state. + + Args: + state: The state to copy. + + Returns: + A deep copy of the state. + """ + import copy + + copy_of_state = copy.deepcopy(state) + + def copy_substate(substate: "BaseState") -> "BaseState": + substate_copy = _deep_copy(substate) + + substate_copy.parent_state = copy_of_state + + return substate_copy + + copy_of_state.substates = { + substate_name: copy_substate(substate) + for substate_name, substate in state.substates.items() + } + + return copy_of_state + + +class ScreenshotPlugin(BasePlugin): + """Plugin to handle screenshot functionality.""" + + def post_compile(self, **context: "Unpack[PostCompileContext]") -> None: + """Called after the compilation of the plugin. + + Args: + context: The context for the plugin. + """ + app = context["app"] + self._add_active_connections_endpoint(app) + self._add_clone_state_endpoint(app) + + @staticmethod + def _add_active_connections_endpoint(app: "App") -> None: + """Add an endpoint to the app that returns the active connections. + + Args: + app: The application instance to which the endpoint will be added. + """ + if not app._api: + return + + def active_connections(_request: "Request") -> "Response": + from starlette.responses import JSONResponse + + if not app.event_namespace: + return JSONResponse({}) + + return JSONResponse(app.event_namespace.token_to_sid) + + app._api.add_route( + ACTIVE_CONNECTIONS, + active_connections, + methods=["GET"], + ) + + @staticmethod + def _add_clone_state_endpoint(app: "App") -> None: + """Add an endpoint to the app that clones the current state. + + Args: + app: The application instance to which the endpoint will be added. + """ + if not app._api: + return + + async def clone_state(request: "Request") -> "Response": + import uuid + + from starlette.responses import JSONResponse + + from reflex.state import _substate_key + + if not app.event_namespace: + return JSONResponse({}) + + token_to_clone = await request.json() + + if not isinstance(token_to_clone, str): + return JSONResponse( + {"error": "Token to clone must be a string."}, status_code=400 + ) + + old_state = await app.state_manager.get_state(token_to_clone) + + new_state = _deep_copy(old_state) + + new_token = uuid.uuid4().hex + + all_states = [new_state] + + found_new = True + + while found_new: + found_new = False + + for state in list(all_states): + for substate in state.substates.values(): + substate._was_touched = True + + if substate not in all_states: + all_states.append(substate) + + found_new = True + + await app.state_manager.set_state( + _substate_key(new_token, new_state), new_state + ) + + return JSONResponse(new_token) + + app._api.add_route( + CLONE_STATE, + clone_state, + methods=["POST"], + ) diff --git a/packages/reflex-core/src/reflex_core/plugins/base.py b/packages/reflex-core/src/reflex_core/plugins/base.py new file mode 100644 index 00000000000..52dfa8d7805 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/plugins/base.py @@ -0,0 +1,126 @@ +"""Base class for all plugins.""" + +from collections.abc import Callable, Sequence +from pathlib import Path +from typing import TYPE_CHECKING, ParamSpec, Protocol, TypedDict + +from typing_extensions import Unpack + +if TYPE_CHECKING: + from reflex.app import App, UnevaluatedPage + + +class CommonContext(TypedDict): + """Common context for all plugins.""" + + +P = ParamSpec("P") + + +class AddTaskProtocol(Protocol): + """Protocol for adding a task to the pre-compile context.""" + + def __call__( + self, + task: Callable[P, list[tuple[str, str]] | tuple[str, str] | None], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> None: + """Add a task to the pre-compile context. + + Args: + task: The task to add. + args: The arguments to pass to the task + kwargs: The keyword arguments to pass to the task + """ + + +class PreCompileContext(CommonContext): + """Context for pre-compile hooks.""" + + add_save_task: AddTaskProtocol + add_modify_task: Callable[[str, Callable[[str], str]], None] + unevaluated_pages: Sequence["UnevaluatedPage"] + + +class PostCompileContext(CommonContext): + """Context for post-compile hooks.""" + + app: "App" + + +class Plugin: + """Base class for all plugins.""" + + def get_frontend_development_dependencies( + self, **context: Unpack[CommonContext] + ) -> list[str] | set[str] | tuple[str, ...]: + """Get the NPM packages required by the plugin for development. + + Args: + context: The context for the plugin. + + Returns: + A list of packages required by the plugin for development. + """ + return [] + + def get_frontend_dependencies( + self, **context: Unpack[CommonContext] + ) -> list[str] | set[str] | tuple[str, ...]: + """Get the NPM packages required by the plugin. + + Args: + context: The context for the plugin. + + Returns: + A list of packages required by the plugin. + """ + return [] + + def get_static_assets( + self, **context: Unpack[CommonContext] + ) -> Sequence[tuple[Path, str | bytes]]: + """Get the static assets required by the plugin. + + Args: + context: The context for the plugin. + + Returns: + A list of static assets required by the plugin. + """ + return [] + + def get_stylesheet_paths(self, **context: Unpack[CommonContext]) -> Sequence[str]: + """Get the paths to the stylesheets required by the plugin relative to the styles directory. + + Args: + context: The context for the plugin. + + Returns: + A list of paths to the stylesheets required by the plugin. + """ + return [] + + def pre_compile(self, **context: Unpack[PreCompileContext]) -> None: + """Called before the compilation of the plugin. + + Args: + context: The context for the plugin. + """ + + def post_compile(self, **context: Unpack[PostCompileContext]) -> None: + """Called after the compilation of the plugin. + + Args: + context: The context for the plugin. + """ + + def __repr__(self): + """Return a string representation of the plugin. + + Returns: + A string representation of the plugin. + """ + return f"{self.__class__.__name__}()" diff --git a/packages/reflex-core/src/reflex_core/plugins/shared_tailwind.py b/packages/reflex-core/src/reflex_core/plugins/shared_tailwind.py new file mode 100644 index 00000000000..ce4dfe24ab6 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/plugins/shared_tailwind.py @@ -0,0 +1,235 @@ +"""Tailwind CSS configuration types for Reflex plugins.""" + +import dataclasses +from collections.abc import Mapping +from copy import deepcopy +from typing import Any, Literal, TypedDict + +from typing_extensions import NotRequired, Unpack + +from .base import Plugin as PluginBase + +TailwindPluginImport = TypedDict( + "TailwindPluginImport", + { + "name": str, + "from": str, + }, +) + +TailwindPluginWithCallConfig = TypedDict( + "TailwindPluginWithCallConfig", + { + "name": str, + "import": NotRequired[TailwindPluginImport], + "call": str, + "args": NotRequired[dict[str, Any]], + }, +) + +TailwindPluginWithoutCallConfig = TypedDict( + "TailwindPluginWithoutCallConfig", + { + "name": str, + "import": NotRequired[TailwindPluginImport], + }, +) + +TailwindPluginConfig = ( + TailwindPluginWithCallConfig | TailwindPluginWithoutCallConfig | str +) + + +def remove_version_from_plugin(plugin: TailwindPluginConfig) -> TailwindPluginConfig: + """Remove the version from a plugin name. + + Args: + plugin: The plugin to remove the version from. + + Returns: + The plugin without the version. + """ + from reflex_core.utils.format import format_library_name + + if isinstance(plugin, str): + return format_library_name(plugin) + + if plugin_import := plugin.get("import"): + plugin_import["from"] = format_library_name(plugin_import["from"]) + + plugin["name"] = format_library_name(plugin["name"]) + + return plugin + + +class TailwindConfig(TypedDict): + """Tailwind CSS configuration options. + + See: https://tailwindcss.com/docs/configuration + """ + + content: NotRequired[list[str]] + important: NotRequired[str | bool] + prefix: NotRequired[str] + separator: NotRequired[str] + presets: NotRequired[list[str]] + darkMode: NotRequired[Literal["media", "class", "selector"]] + theme: NotRequired[dict[str, Any]] + corePlugins: NotRequired[list[str] | dict[str, bool]] + plugins: NotRequired[list[TailwindPluginConfig]] + + +def tailwind_config_js_template( + *, default_content: list[str], **kwargs: Unpack[TailwindConfig] +): + """Generate a Tailwind CSS configuration file in JavaScript format. + + Args: + default_content: The default content to use if none is provided. + **kwargs: The template variables. + + Returns: + The Tailwind config template. + """ + import json + + # Extract parameters + plugins = kwargs.get("plugins", []) + presets = kwargs.get("presets", []) + content = kwargs.get("content") + theme = kwargs.get("theme") + dark_mode = kwargs.get("darkMode") + core_plugins = kwargs.get("corePlugins") + important = kwargs.get("important") + prefix = kwargs.get("prefix") + separator = kwargs.get("separator") + + # Extract destructured imports from plugin dicts only + imports = [ + plugin["import"] + for plugin in plugins + if isinstance(plugin, Mapping) and "import" in plugin + ] + + # Generate import statements for destructured imports + import_lines = "\n".join([ + f"import {{ {imp['name']} }} from {json.dumps(imp['from'])};" for imp in imports + ]) + + # Generate plugin imports + plugin_imports = [] + for i, plugin in enumerate(plugins, 1): + if isinstance(plugin, Mapping) and "call" not in plugin: + plugin_imports.append( + f"import plugin{i} from {json.dumps(plugin['name'])};" + ) + elif not isinstance(plugin, Mapping): + plugin_imports.append(f"import plugin{i} from {json.dumps(plugin)};") + + plugin_imports_lines = "\n".join(plugin_imports) + + presets_imports_lines = "\n".join([ + f"import preset{i} from {json.dumps(preset)};" + for i, preset in enumerate(presets, 1) + ]) + + # Generate plugin array + plugin_list = [] + for i, plugin in enumerate(plugins, 1): + if isinstance(plugin, Mapping) and "call" in plugin: + args_part = "" + if "args" in plugin: + args_part = json.dumps(plugin["args"]) + plugin_list.append(f"{plugin['call']}({args_part})") + else: + plugin_list.append(f"plugin{i}") + + plugin_use_str = ",".join(plugin_list) + + return rf""" +{import_lines} + +{plugin_imports_lines} + +{presets_imports_lines} + +export default {{ + content: {json.dumps(content or default_content)}, + theme: {json.dumps(theme or {})}, + {f"darkMode: {json.dumps(dark_mode)}," if dark_mode is not None else ""} + {f"corePlugins: {json.dumps(core_plugins)}," if core_plugins is not None else ""} + {f"importants: {json.dumps(important)}," if important is not None else ""} + {f"prefix: {json.dumps(prefix)}," if prefix is not None else ""} + {f"separator: {json.dumps(separator)}," if separator is not None else ""} + {f"presets: [{', '.join(f'preset{i}' for i in range(1, len(presets) + 1))}]," if presets else ""} + plugins: [{plugin_use_str}] +}}; +""" + + +@dataclasses.dataclass +class TailwindPlugin(PluginBase): + """Plugin for Tailwind CSS.""" + + config: TailwindConfig = dataclasses.field( + default_factory=lambda: TailwindConfig( + plugins=[ + "@tailwindcss/typography@0.5.19", + ], + ) + ) + + def get_frontend_development_dependencies(self, **context) -> list[str]: + """Get the packages required by the plugin. + + Args: + **context: The context for the plugin. + + Returns: + A list of packages required by the plugin. + """ + config = self.get_config() + + return [ + plugin if isinstance(plugin, str) else plugin.get("name") + for plugin in config.get("plugins", []) + ] + config.get("presets", []) + + def get_config(self) -> TailwindConfig: + """Get the Tailwind CSS configuration. + + Returns: + The Tailwind CSS configuration. + """ + from reflex_core.config import get_config + + rxconfig_config = getattr(get_config(), "tailwind", None) + + if rxconfig_config is not None and rxconfig_config != self.config: + from reflex_core.utils import console + + console.warn( + "It seems you have provided a tailwind configuration in your call to `rx.Config`." + f" You should provide the configuration as an argument to `rx.plugins.{self.__class__.__name__}()` instead." + ) + return rxconfig_config + + return self.config + + def get_unversioned_config(self) -> TailwindConfig: + """Get the Tailwind CSS configuration without version-specific adjustments. + + Returns: + The Tailwind CSS configuration without version-specific adjustments. + """ + from reflex_core.utils.format import format_library_name + + config = deepcopy(self.get_config()) + if presets := config.get("presets"): + # Somehow, having an empty list of presets breaks Tailwind. + # So we only set the presets if there are any. + config["presets"] = [format_library_name(preset) for preset in presets] + config["plugins"] = [ + remove_version_from_plugin(plugin) for plugin in config.get("plugins", []) + ] + return config diff --git a/packages/reflex-core/src/reflex_core/plugins/sitemap.py b/packages/reflex-core/src/reflex_core/plugins/sitemap.py new file mode 100644 index 00000000000..85e526cbb05 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/plugins/sitemap.py @@ -0,0 +1,210 @@ +"""Sitemap plugin for Reflex.""" + +import datetime +from collections.abc import Sequence +from pathlib import Path +from types import SimpleNamespace +from typing import TYPE_CHECKING, Literal, TypedDict +from xml.etree.ElementTree import Element, SubElement, indent, tostring + +from typing_extensions import NotRequired + +from reflex_core import constants + +from .base import Plugin as PluginBase + +if TYPE_CHECKING: + from reflex.app import UnevaluatedPage + +Location = str +LastModified = datetime.datetime +ChangeFrequency = Literal[ + "always", "hourly", "daily", "weekly", "monthly", "yearly", "never" +] +Priority = float + + +class SitemapLink(TypedDict): + """A link in the sitemap.""" + + loc: Location + lastmod: NotRequired[LastModified] + changefreq: NotRequired[ChangeFrequency] + priority: NotRequired[Priority] + + +class SitemapLinkConfiguration(TypedDict): + """Configuration for a sitemap link.""" + + loc: NotRequired[Location] + lastmod: NotRequired[LastModified] + changefreq: NotRequired[ChangeFrequency] + priority: NotRequired[Priority] + + +class Constants(SimpleNamespace): + """Sitemap constants.""" + + FILE_PATH: Path = Path(constants.Dirs.PUBLIC) / "sitemap.xml" + + +def configuration_with_loc( + *, config: SitemapLinkConfiguration, deploy_url: str | None, loc: Location +) -> SitemapLink: + """Set the 'loc' field of the configuration. + + Args: + config: The configuration dictionary. + deploy_url: The deployment URL, if any. + loc: The location to set. + + Returns: + A SitemapLink dictionary with the 'loc' field set. + """ + if deploy_url and not loc.startswith("http://") and not loc.startswith("https://"): + loc = f"{deploy_url.rstrip('/')}/{loc.lstrip('/')}" + link: SitemapLink = {"loc": loc} + if (lastmod := config.get("lastmod")) is not None: + link["lastmod"] = lastmod + if (changefreq := config.get("changefreq")) is not None: + link["changefreq"] = changefreq + if (priority := config.get("priority")) is not None: + link["priority"] = min(1.0, max(0.0, priority)) + return link + + +def generate_xml(links: Sequence[SitemapLink]) -> str: + """Generate an XML sitemap from a list of links. + + Args: + links: A sequence of SitemapLink dictionaries. + + Returns: + A pretty-printed XML string representing the sitemap. + """ + urlset = Element("urlset", xmlns="https://www.sitemaps.org/schemas/sitemap/0.9") + + for link in links: + url = SubElement(urlset, "url") + + loc_element = SubElement(url, "loc") + loc_element.text = link["loc"] + + if (changefreq := link.get("changefreq")) is not None: + changefreq_element = SubElement(url, "changefreq") + changefreq_element.text = changefreq + + if (lastmod := link.get("lastmod")) is not None: + lastmod_element = SubElement(url, "lastmod") + if isinstance(lastmod, datetime.datetime): + lastmod = lastmod.isoformat() + lastmod_element.text = lastmod + + if (priority := link.get("priority")) is not None: + priority_element = SubElement(url, "priority") + priority_element.text = str(priority) + indent(urlset, " ") + return tostring(urlset, encoding="utf-8", xml_declaration=True).decode("utf-8") + + +def is_route_dynamic(route: str) -> bool: + """Check if a route is dynamic. + + Args: + route: The route to check. + + Returns: + True if the route is dynamic, False otherwise. + """ + return "[" in route and "]" in route + + +def generate_links_for_sitemap( + unevaluated_pages: Sequence["UnevaluatedPage"], +) -> list[SitemapLink]: + """Generate sitemap links from unevaluated pages. + + Args: + unevaluated_pages: Sequence of unevaluated pages. + + Returns: + A list of SitemapLink dictionaries. + """ + from reflex_core.config import get_config + from reflex_core.utils import console + + deploy_url = get_config().deploy_url + + links: list[SitemapLink] = [] + + for page in unevaluated_pages: + sitemap_config: SitemapLinkConfiguration | None = page.context.get( + "sitemap", {} + ) + if sitemap_config is None: + continue + + if is_route_dynamic(page.route) or page.route == "404": + if not sitemap_config: + continue + + if (loc := sitemap_config.get("loc")) is None: + route_message = ( + "Dynamic route" if is_route_dynamic(page.route) else "Route 404" + ) + console.warn( + route_message + + f" '{page.route}' does not have a 'loc' in sitemap configuration. Skipping." + ) + continue + + sitemap_link = configuration_with_loc( + config=sitemap_config, deploy_url=deploy_url, loc=loc + ) + + elif (loc := sitemap_config.get("loc")) is not None: + sitemap_link = configuration_with_loc( + config=sitemap_config, deploy_url=deploy_url, loc=loc + ) + + else: + loc = page.route if page.route != "index" else "/" + if not loc.startswith("/"): + loc = "/" + loc + sitemap_link = configuration_with_loc( + config=sitemap_config, deploy_url=deploy_url, loc=loc + ) + + links.append(sitemap_link) + return links + + +def sitemap_task(unevaluated_pages: Sequence["UnevaluatedPage"]) -> tuple[str, str]: + """Task to generate the sitemap XML file. + + Args: + unevaluated_pages: Sequence of unevaluated pages. + + Returns: + A tuple containing the file path and the generated XML content. + """ + return ( + str(Constants.FILE_PATH), + generate_xml(generate_links_for_sitemap(unevaluated_pages)), + ) + + +class SitemapPlugin(PluginBase): + """Sitemap plugin for Reflex.""" + + def pre_compile(self, **context): + """Generate the sitemap XML file before compilation. + + Args: + context: The context for the plugin. + """ + unevaluated_pages = context.get("unevaluated_pages", []) + context["add_save_task"](sitemap_task, unevaluated_pages) + + +Plugin = SitemapPlugin diff --git a/packages/reflex-core/src/reflex_core/plugins/tailwind_v3.py b/packages/reflex-core/src/reflex_core/plugins/tailwind_v3.py new file mode 100644 index 00000000000..c32c80835b6 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/plugins/tailwind_v3.py @@ -0,0 +1,170 @@ +"""Base class for all plugins.""" + +import dataclasses +from pathlib import Path +from types import SimpleNamespace + +from reflex_core.constants.base import Dirs +from reflex_core.constants.compiler import Ext, PageNames +from reflex_core.plugins.shared_tailwind import ( + TailwindConfig, + TailwindPlugin, + tailwind_config_js_template, +) + + +class Constants(SimpleNamespace): + """Tailwind constants.""" + + # The Tailwindcss version + VERSION = "tailwindcss@3.4.19" + # The Tailwind config. + CONFIG = "tailwind.config.js" + # Default Tailwind content paths + CONTENT = [f"./{Dirs.PAGES}/**/*.{{js,ts,jsx,tsx}}", "./utils/**/*.{js,ts,jsx,tsx}"] + # Relative tailwind style path to root stylesheet in Dirs.STYLES. + ROOT_STYLE_PATH = "./tailwind.css" + + # Content of the style content. + ROOT_STYLE_CONTENT = """ +@import "tailwindcss/base"; + +@import url('{radix_url}'); + +@tailwind components; +@tailwind utilities; +""" + + # The default tailwind css. + TAILWIND_CSS = "@import url('./tailwind.css');" + + +def compile_config(config: TailwindConfig): + """Compile the Tailwind config. + + Args: + config: The Tailwind config. + + Returns: + The compiled Tailwind config. + """ + return Constants.CONFIG, tailwind_config_js_template( + **config, + default_content=Constants.CONTENT, + ) + + +def compile_root_style(): + """Compile the Tailwind root style. + + Returns: + The compiled Tailwind root style. + """ + from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET + + return str( + Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH + ), Constants.ROOT_STYLE_CONTENT.format( + radix_url=RADIX_THEMES_STYLESHEET, + ) + + +def _index_of_element_that_has(haystack: list[str], needle: str) -> int | None: + return next( + (i for i, line in enumerate(haystack) if needle in line), + None, + ) + + +def add_tailwind_to_postcss_config(postcss_file_content: str) -> str: + """Add tailwind to the postcss config. + + Args: + postcss_file_content: The content of the postcss config file. + + Returns: + The modified postcss config file content. + """ + from reflex_core.constants import Dirs + + postcss_file_lines = postcss_file_content.splitlines() + + if _index_of_element_that_has(postcss_file_lines, "tailwindcss") is not None: + return postcss_file_content + + line_with_postcss_plugins = _index_of_element_that_has( + postcss_file_lines, "plugins" + ) + if not line_with_postcss_plugins: + print( # noqa: T201 + f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. " + "Please make sure the file exists and is valid." + ) + return postcss_file_content + + postcss_import_line = _index_of_element_that_has( + postcss_file_lines, '"postcss-import"' + ) + postcss_file_lines.insert( + (postcss_import_line or line_with_postcss_plugins) + 1, "tailwindcss: {}," + ) + + return "\n".join(postcss_file_lines) + + +def add_tailwind_to_css_file(css_file_content: str) -> str: + """Add tailwind to the css file. + + Args: + css_file_content: The content of the css file. + + Returns: + The modified css file content. + """ + from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET + + if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content: + return css_file_content + if RADIX_THEMES_STYLESHEET not in css_file_content: + print( # noqa: T201 + f"Could not find line with '{RADIX_THEMES_STYLESHEET}' in {Dirs.STYLES}. " + "Please make sure the file exists and is valid." + ) + return css_file_content + return css_file_content.replace( + f"@import url('{RADIX_THEMES_STYLESHEET}');", + Constants.TAILWIND_CSS, + ) + + +@dataclasses.dataclass +class TailwindV3Plugin(TailwindPlugin): + """Plugin for Tailwind CSS.""" + + def get_frontend_development_dependencies(self, **context) -> list[str]: + """Get the packages required by the plugin. + + Args: + **context: The context for the plugin. + + Returns: + A list of packages required by the plugin. + """ + return [ + *super().get_frontend_development_dependencies(**context), + Constants.VERSION, + ] + + def pre_compile(self, **context): + """Pre-compile the plugin. + + Args: + context: The context for the plugin. + """ + context["add_save_task"](compile_config, self.get_unversioned_config()) + context["add_save_task"](compile_root_style) + context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config) + context["add_modify_task"]( + str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)), + add_tailwind_to_css_file, + ) diff --git a/packages/reflex-core/src/reflex_core/plugins/tailwind_v4.py b/packages/reflex-core/src/reflex_core/plugins/tailwind_v4.py new file mode 100644 index 00000000000..8901c5220c9 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/plugins/tailwind_v4.py @@ -0,0 +1,174 @@ +"""Base class for all plugins.""" + +import dataclasses +from pathlib import Path +from types import SimpleNamespace + +from reflex_core.constants.base import Dirs +from reflex_core.constants.compiler import Ext, PageNames +from reflex_core.plugins.shared_tailwind import ( + TailwindConfig, + TailwindPlugin, + tailwind_config_js_template, +) + + +class Constants(SimpleNamespace): + """Tailwind constants.""" + + # The Tailwindcss version + VERSION = "tailwindcss@4.2.2" + # The Tailwind config. + CONFIG = "tailwind.config.js" + # Default Tailwind content paths + CONTENT = [f"./{Dirs.PAGES}/**/*.{{js,ts,jsx,tsx}}", "./utils/**/*.{js,ts,jsx,tsx}"] + # Relative tailwind style path to root stylesheet in Dirs.STYLES. + ROOT_STYLE_PATH = "./tailwind.css" + + # Content of the style content. + ROOT_STYLE_CONTENT = """@layer theme, base, components, utilities; +@import "tailwindcss/theme.css" layer(theme); +@import "tailwindcss/preflight.css" layer(base); +@import "{radix_url}" layer(components); +@import "tailwindcss/utilities.css" layer(utilities); +@config "../tailwind.config.js"; +""" + + # The default tailwind css. + TAILWIND_CSS = "@import url('./tailwind.css');" + + +def compile_config(config: TailwindConfig): + """Compile the Tailwind config. + + Args: + config: The Tailwind config. + + Returns: + The compiled Tailwind config. + """ + return Constants.CONFIG, tailwind_config_js_template( + **config, + default_content=Constants.CONTENT, + ) + + +def compile_root_style(): + """Compile the Tailwind root style. + + Returns: + The compiled Tailwind root style. + """ + from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET + + return str( + Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH + ), Constants.ROOT_STYLE_CONTENT.format( + radix_url=RADIX_THEMES_STYLESHEET, + ) + + +def _index_of_element_that_has(haystack: list[str], needle: str) -> int | None: + return next( + (i for i, line in enumerate(haystack) if needle in line), + None, + ) + + +def add_tailwind_to_postcss_config(postcss_file_content: str) -> str: + """Add tailwind to the postcss config. + + Args: + postcss_file_content: The content of the postcss config file. + + Returns: + The modified postcss config file content. + """ + from reflex_core.constants import Dirs + + postcss_file_lines = postcss_file_content.splitlines() + + line_with_postcss_plugins = _index_of_element_that_has( + postcss_file_lines, "plugins" + ) + if not line_with_postcss_plugins: + print( # noqa: T201 + f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. " + "Please make sure the file exists and is valid." + ) + return postcss_file_content + + plugins_to_remove = ['"postcss-import"', "tailwindcss", "autoprefixer"] + plugins_to_add = ['"@tailwindcss/postcss"'] + + for plugin in plugins_to_remove: + plugin_index = _index_of_element_that_has(postcss_file_lines, plugin) + if plugin_index is not None: + postcss_file_lines.pop(plugin_index) + + for plugin in plugins_to_add[::-1]: + if not _index_of_element_that_has(postcss_file_lines, plugin): + postcss_file_lines.insert( + line_with_postcss_plugins + 1, f" {plugin}: {{}}," + ) + + return "\n".join(postcss_file_lines) + + +def add_tailwind_to_css_file(css_file_content: str) -> str: + """Add tailwind to the css file. + + Args: + css_file_content: The content of the css file. + + Returns: + The modified css file content. + """ + from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET + + if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content: + return css_file_content + if RADIX_THEMES_STYLESHEET not in css_file_content: + print( # noqa: T201 + f"Could not find line with '{RADIX_THEMES_STYLESHEET}' in {Dirs.STYLES}. " + "Please make sure the file exists and is valid." + ) + return css_file_content + return css_file_content.replace( + f"@import url('{RADIX_THEMES_STYLESHEET}');", + Constants.TAILWIND_CSS, + ) + + +@dataclasses.dataclass +class TailwindV4Plugin(TailwindPlugin): + """Plugin for Tailwind CSS.""" + + def get_frontend_development_dependencies(self, **context) -> list[str]: + """Get the packages required by the plugin. + + Args: + **context: The context for the plugin. + + Returns: + A list of packages required by the plugin. + """ + return [ + *super().get_frontend_development_dependencies(**context), + Constants.VERSION, + "@tailwindcss/postcss@4.2.2", + ] + + def pre_compile(self, **context): + """Pre-compile the plugin. + + Args: + context: The context for the plugin. + """ + context["add_save_task"](compile_config, self.get_unversioned_config()) + context["add_save_task"](compile_root_style) + context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config) + context["add_modify_task"]( + str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)), + add_tailwind_to_css_file, + ) diff --git a/packages/reflex-core/src/reflex_core/style.py b/packages/reflex-core/src/reflex_core/style.py new file mode 100644 index 00000000000..2aa2dc090e3 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/style.py @@ -0,0 +1,416 @@ +"""Handle styling.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal + +from reflex_core import constants +from reflex_core.breakpoints import Breakpoints, breakpoints_values +from reflex_core.event import EventChain, EventHandler, EventSpec, run_script +from reflex_core.utils import format +from reflex_core.utils.exceptions import ReflexError +from reflex_core.utils.imports import ImportVar +from reflex_core.utils.types import typehint_issubclass +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.function import FunctionVar +from reflex_core.vars.object import ObjectVar + +SYSTEM_COLOR_MODE: str = "system" +LIGHT_COLOR_MODE: str = "light" +DARK_COLOR_MODE: str = "dark" +LiteralColorMode = Literal["system", "light", "dark"] + +# Reference the global ColorModeContext +color_mode_imports = { + f"$/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="ColorModeContext")], + "react": [ImportVar(tag="useContext")], +} + + +def _color_mode_var(_js_expr: str, _var_type: type = str) -> Var: + """Create a Var that destructs the _js_expr from ColorModeContext. + + Args: + _js_expr: The name of the variable to get from ColorModeContext. + _var_type: The type of the Var. + + Returns: + The Var that resolves to the color mode. + """ + return Var( + _js_expr=_js_expr, + _var_type=_var_type, + _var_data=VarData( + imports=color_mode_imports, + hooks={f"const {{ {_js_expr} }} = useContext(ColorModeContext)": None}, + ), + ).guess_type() + + +def set_color_mode( + new_color_mode: LiteralColorMode | Var[LiteralColorMode], +) -> EventSpec: + """Create an EventSpec Var that sets the color mode to a specific value. + + Note: `set_color_mode` is not a real event and cannot be triggered from a + backend event handler. + + Args: + new_color_mode: The color mode to set. + + Returns: + The EventSpec Var that can be passed to an event trigger. + """ + base_setter = _color_mode_var( + _js_expr=constants.ColorMode.SET, + ).to(FunctionVar) + + return run_script( + base_setter.call(new_color_mode), + ) + + +# Var resolves to the current color mode for the app ("light", "dark" or "system") +color_mode = _color_mode_var(_js_expr=constants.ColorMode.NAME) +# Var resolves to the resolved color mode for the app ("light" or "dark") +resolved_color_mode = _color_mode_var(_js_expr=constants.ColorMode.RESOLVED_NAME) +# Var resolves to a function invocation that toggles the color mode +toggle_color_mode = _color_mode_var( + _js_expr=constants.ColorMode.TOGGLE, + _var_type=EventChain, +) + +STYLE_PROP_SHORTHAND_MAPPING = { + "paddingX": ("paddingInlineStart", "paddingInlineEnd"), + "paddingY": ("paddingTop", "paddingBottom"), + "marginX": ("marginInlineStart", "marginInlineEnd"), + "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"), +} + + +def media_query(breakpoint_expr: str): + """Create a media query selector. + + Args: + breakpoint_expr: The CSS expression representing the breakpoint. + + Returns: + The media query selector used as a key in emotion css dict. + """ + return f"@media screen and (min-width: {breakpoint_expr})" + + +def convert_item( + style_item: int | str | Var, +) -> tuple[str | Var, VarData | None]: + """Format a single value in a style dictionary. + + Args: + style_item: The style item to format. + + Returns: + The formatted style item and any associated VarData. + + Raises: + ReflexError: If an EventHandler is used as a style value + """ + from reflex_core.components.component import BaseComponent + + if isinstance(style_item, (EventHandler, BaseComponent)): + msg = ( + f"{type(style_item)} cannot be used as style values. " + "Please use a Var or a literal value." + ) + raise ReflexError(msg) + + if isinstance(style_item, Var): + return style_item, style_item._get_all_var_data() + + # Otherwise, convert to Var to collapse VarData encoded in f-string. + new_var = LiteralVar.create(style_item) + var_data = new_var._get_all_var_data() if new_var is not None else None + return new_var, var_data + + +def convert_list( + responsive_list: list[str | dict | Var], +) -> tuple[list[str | dict[str, Var | list | dict]], VarData | None]: + """Format a responsive value list. + + Args: + responsive_list: The raw responsive value list (one value per breakpoint). + + Returns: + The recursively converted responsive value list and any associated VarData. + """ + converted_value = [] + item_var_datas = [] + for responsive_item in responsive_list: + if isinstance(responsive_item, dict): + # Recursively format nested style dictionaries. + item, item_var_data = convert(responsive_item) + else: + item, item_var_data = convert_item(responsive_item) + converted_value.append(item) + item_var_datas.append(item_var_data) + return converted_value, VarData.merge(*item_var_datas) + + +def convert( + style_dict: dict[str, Var | dict | list | str], +) -> tuple[dict[str, str | list | dict], VarData | None]: + """Format a style dictionary. + + Args: + style_dict: The style dictionary to format. + + Returns: + The formatted style dictionary. + """ + var_data = None # Track import/hook data from any Vars in the style dict. + out = {} + + def update_out_dict( + return_value: Var | dict | list | str, keys_to_update: tuple[str, ...] + ): + for k in keys_to_update: + out[k] = return_value + + for key, value in style_dict.items(): + keys = ( + format_style_key(key) + if not isinstance(value, (dict, ObjectVar, list)) + or ( + isinstance(value, Breakpoints) + and all(not isinstance(v, dict) for v in value.values()) + ) + or (isinstance(value, list) and all(not isinstance(v, dict) for v in value)) + or ( + isinstance(value, ObjectVar) + and not typehint_issubclass(value._var_type, Mapping) + ) + else (key,) + ) + + if isinstance(value, Var): + return_val = value + new_var_data = value._get_all_var_data() + update_out_dict(return_val, keys) + elif isinstance(value, dict): + # Recursively format nested style dictionaries. + return_val, new_var_data = convert(value) + update_out_dict(return_val, keys) + elif isinstance(value, list): + # Responsive value is a list of dict or value + return_val, new_var_data = convert_list(value) + update_out_dict(return_val, keys) + else: + return_val, new_var_data = convert_item(value) + update_out_dict(return_val, keys) + # Combine all the collected VarData instances. + var_data = VarData.merge(var_data, new_var_data) + + if isinstance(style_dict, Breakpoints): + out = Breakpoints(out).factorize() + + return out, var_data + + +def format_style_key(key: str) -> tuple[str, ...]: + """Convert style keys to camel case and convert shorthand + styles names to their corresponding css names. + + Args: + key: The style key to convert. + + Returns: + Tuple of css style names corresponding to the key provided. + """ + if key.startswith("--"): + return (key,) + key = format.to_camel_case(key) + return STYLE_PROP_SHORTHAND_MAPPING.get(key, (key,)) + + +EMPTY_VAR_DATA = VarData() + + +class Style(dict[str, Any]): + """A style dictionary.""" + + def __init__(self, style_dict: dict[str, Any] | None = None, **kwargs): + """Initialize the style. + + Args: + style_dict: The style dictionary. + kwargs: Other key value pairs to apply to the dict update. + """ + if style_dict: + style_dict.update(kwargs) + else: + style_dict = kwargs + if style_dict: + style_dict, self._var_data = convert(style_dict) + else: + self._var_data = EMPTY_VAR_DATA + super().__init__(style_dict) + + def update(self, style_dict: dict | None, **kwargs): + """Update the style. + + Args: + style_dict: The style dictionary. + kwargs: Other key value pairs to apply to the dict update. + """ + if not isinstance(style_dict, Style): + converted_dict = type(self)(style_dict) + else: + converted_dict = style_dict + if kwargs: + if converted_dict is None: + converted_dict = type(self)(kwargs) + else: + converted_dict.update(kwargs) + # Combine our VarData with that of any Vars in the style_dict that was passed. + self._var_data = VarData.merge(self._var_data, converted_dict._var_data) + super().update(converted_dict) + + def __setitem__(self, key: str, value: Any): + """Set an item in the style. + + Args: + key: The key to set. + value: The value to set. + """ + # Create a Var to collapse VarData encoded in f-string. + var = LiteralVar.create(value) + if var is not None: + # Carry the imports/hooks when setting a Var as a value. + self._var_data = VarData.merge( + getattr(self, "_var_data", None), var._get_all_var_data() + ) + super().__setitem__(key, value) + + def __or__(self, other: Style | dict) -> Style: + """Combine two styles. + + Args: + other: The other style to combine. + + Returns: + The combined style. + """ + other_var_data = None + if not isinstance(other, Style): + other_dict, other_var_data = convert(other) + else: + other_dict, other_var_data = other, other._var_data + + new_style = Style(super().__or__(other_dict)) + if self._var_data or other_var_data: + new_style._var_data = VarData.merge(self._var_data, other_var_data) + return new_style + + +def _format_emotion_style_pseudo_selector(key: str) -> str: + """Format a pseudo selector for emotion CSS-in-JS. + + Args: + key: Underscore-prefixed or colon-prefixed pseudo selector key (_hover/:hover). + + Returns: + A self-referential pseudo selector key (&:hover). + """ + prefix = None + if key.startswith("_"): + prefix = "&:" + key = key[1:] + if key.startswith(":"): + # Handle pseudo selectors and elements in native format. + prefix = "&" + if prefix is not None: + return prefix + format.to_kebab_case(key) + return key + + +def format_as_emotion(style_dict: dict[str, Any]) -> Style | None: + """Convert the style to an emotion-compatible CSS-in-JS dict. + + Args: + style_dict: The style dict to convert. + + Returns: + The emotion style dict. + """ + var_data = style_dict._var_data if isinstance(style_dict, Style) else None + + emotion_style = Style() + + for orig_key, value in style_dict.items(): + key = _format_emotion_style_pseudo_selector(orig_key) + if isinstance(value, (Breakpoints, list)): + if isinstance(value, Breakpoints): + mbps = { + media_query(bp): ( + bp_value if isinstance(bp_value, dict) else {key: bp_value} + ) + for bp, bp_value in value.items() + } + else: + # Apply media queries from responsive value list. + mbps = { + media_query([0, *breakpoints_values][bp]): ( + bp_value if isinstance(bp_value, dict) else {key: bp_value} + ) + for bp, bp_value in enumerate(value) + } + if key.startswith("&:"): + emotion_style[key] = mbps + else: + for mq, style_sub_dict in mbps.items(): + emotion_style.setdefault(mq, {}).update(style_sub_dict) + elif isinstance(value, dict): + # Recursively format nested style dictionaries. + emotion_style[key] = format_as_emotion(value) + else: + emotion_style[key] = value + if emotion_style: + if var_data is not None: + emotion_style._var_data = VarData.merge(emotion_style._var_data, var_data) + return emotion_style + return None + + +def convert_dict_to_style_and_format_emotion( + raw_dict: dict[str, Any], +) -> dict[str, Any] | None: + """Convert a dict to a style dict and then format as emotion. + + Args: + raw_dict: The dict to convert. + + Returns: + The emotion dict. + + """ + return format_as_emotion(Style(raw_dict)) + + +STACK_CHILDREN_FULL_WIDTH = { + "& :where(.rx-Stack)": { + "width": "100%", + }, + "& :where(.rx-Stack) > :where( " + "div:not(.rt-Box, .rx-Upload, .rx-Html)," + "input, select, textarea, table" + ")": { + "width": "100%", + "flex_shrink": "1", + }, +} diff --git a/packages/reflex-core/src/reflex_core/utils/__init__.py b/packages/reflex-core/src/reflex_core/utils/__init__.py new file mode 100644 index 00000000000..d8eb4c260ed --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/__init__.py @@ -0,0 +1 @@ +"""Reflex core utilities.""" diff --git a/packages/reflex-core/src/reflex_core/utils/compat.py b/packages/reflex-core/src/reflex_core/utils/compat.py new file mode 100644 index 00000000000..cb9f35485a3 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/compat.py @@ -0,0 +1,69 @@ +"""Compatibility hacks and helpers.""" + +import sys +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from pydantic.fields import FieldInfo + + +async def windows_hot_reload_lifespan_hack(): + """[REF-3164] A hack to fix hot reload on Windows. + + Uvicorn has an issue stopping itself on Windows after detecting changes in + the filesystem. + + This workaround repeatedly prints and flushes null characters to stderr, + which seems to allow the uvicorn server to exit when the CTRL-C signal is + sent from the reloader process. + + Don't ask me why this works, I discovered it by accident - masenf. + """ + import asyncio + import sys + + try: + while True: + sys.stderr.write("\0") + sys.stderr.flush() + await asyncio.sleep(0.5) + except asyncio.CancelledError: + pass + + +def annotations_from_namespace(namespace: Mapping[str, Any]) -> dict[str, Any]: + """Get the annotations from a class namespace. + + Args: + namespace: The class namespace. + + Returns: + The (forward-ref) annotations from the class namespace. + """ + if sys.version_info >= (3, 14) and "__annotations__" not in namespace: + from annotationlib import ( + Format, + call_annotate_function, + get_annotate_from_class_namespace, + ) + + if annotate := get_annotate_from_class_namespace(namespace): + return call_annotate_function(annotate, format=Format.FORWARDREF) + return namespace.get("__annotations__", {}) + + +def sqlmodel_field_has_primary_key(field_info: "FieldInfo") -> bool: + """Determines if a field is a primary. + + Args: + field_info: a rx.model field + + Returns: + If field_info is a primary key (Bool) + """ + if getattr(field_info, "primary_key", None) is True: + return True + if getattr(field_info, "sa_column", None) is None: + return False + return bool(getattr(field_info.sa_column, "primary_key", None)) # pyright: ignore[reportAttributeAccessIssue] diff --git a/packages/reflex-core/src/reflex_core/utils/console.py b/packages/reflex-core/src/reflex_core/utils/console.py new file mode 100644 index 00000000000..de7f61c6ac8 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/console.py @@ -0,0 +1,486 @@ +"""Functions to communicate to the user via console.""" + +from __future__ import annotations + +import contextlib +import datetime +import inspect +import os +import shutil +import sys +import time +from pathlib import Path +from types import FrameType, ModuleType + +from rich.console import Console +from rich.progress import MofNCompleteColumn, Progress, TaskID, TimeElapsedColumn +from rich.prompt import Prompt + +from reflex_core.constants import LogLevel +from reflex_core.constants.base import Reflex +from reflex_core.utils.decorator import once + +# Console for pretty printing. +_console = Console(highlight=False) +_console_stderr = Console(stderr=True, highlight=False) + +# The current log level. +_LOG_LEVEL = LogLevel.INFO + +# Deprecated features who's warning has been printed. +_EMITTED_DEPRECATION_WARNINGS = set() + +# Info messages which have been printed. +_EMITTED_INFO = set() + +# Warnings which have been printed. +_EMITTED_WARNINGS = set() + +# Errors which have been printed. +_EMITTED_ERRORS = set() + +# Success messages which have been printed. +_EMITTED_SUCCESS = set() + +# Debug messages which have been printed. +_EMITTED_DEBUG = set() + +# Logs which have been printed. +_EMITTED_LOGS = set() + +# Prints which have been printed. +_EMITTED_PRINTS = set() + + +def set_log_level(log_level: LogLevel | None): + """Set the log level. + + Args: + log_level: The log level to set. + + Raises: + TypeError: If the log level is a string. + """ + if log_level is None: + return + if not isinstance(log_level, LogLevel): + msg = f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead." + raise TypeError(msg) + global _LOG_LEVEL + if log_level != _LOG_LEVEL: + # Set the loglevel persistenly for subprocesses. + os.environ["REFLEX_LOGLEVEL"] = log_level.value + _LOG_LEVEL = log_level + + +def is_debug() -> bool: + """Check if the log level is debug. + + Returns: + True if the log level is debug. + """ + return _LOG_LEVEL <= LogLevel.DEBUG + + +def print(msg: str, *, dedupe: bool = False, **kwargs): + """Print a message. + + Args: + msg: The message to print. + dedupe: If True, suppress multiple console logs of print message. + kwargs: Keyword arguments to pass to the print function. + """ + if dedupe: + if msg in _EMITTED_PRINTS: + return + _EMITTED_PRINTS.add(msg) + _console.print(msg, **kwargs) + + +def _print_stderr(msg: str, *, dedupe: bool = False, **kwargs): + """Print a message to stderr. + + Args: + msg: The message to print. + dedupe: If True, suppress multiple console logs of print message. + kwargs: Keyword arguments to pass to the print function. + """ + if dedupe: + if msg in _EMITTED_PRINTS: + return + _EMITTED_PRINTS.add(msg) + _console_stderr.print(msg, **kwargs) + + +@once +def log_file_console(): + """Create a console that logs to a file. + + Returns: + A Console object that logs to a file. + """ + from reflex_core.environment import environment + + if not (env_log_file := environment.REFLEX_LOG_FILE.get()): + subseconds = int((time.time() % 1) * 1000) + timestamp = time.strftime("%Y-%m-%d_%H-%M-%S") + f"_{subseconds:03d}" + log_file = Reflex.DIR / "logs" / (timestamp + ".log") + log_file.parent.mkdir(parents=True, exist_ok=True) + else: + log_file = env_log_file + if log_file.exists(): + log_file.unlink() + log_file.touch() + return Console(file=log_file.open("a", encoding="utf-8")) + + +@once +def should_use_log_file_console() -> bool: + """Check if the log file console should be used. + + Returns: + True if the log file console should be used, False otherwise. + """ + from reflex_core.environment import environment + + return environment.REFLEX_ENABLE_FULL_LOGGING.get() + + +def print_to_log_file(msg: str, *, dedupe: bool = False, **kwargs): + """Print a message to the log file. + + Args: + msg: The message to print. + dedupe: If True, suppress multiple console logs of print message. + kwargs: Keyword arguments to pass to the print function. + """ + log_file_console().print(f"[{datetime.datetime.now()}] {msg}", **kwargs) + + +def debug(msg: str, *, dedupe: bool = False, **kwargs): + """Print a debug message. + + Args: + msg: The debug message. + dedupe: If True, suppress multiple console logs of debug message. + kwargs: Keyword arguments to pass to the print function. + """ + if is_debug(): + msg_ = f"[purple]Debug: {msg}[/purple]" + if dedupe: + if msg_ in _EMITTED_DEBUG: + return + _EMITTED_DEBUG.add(msg_) + if progress := kwargs.pop("progress", None): + progress.console.print(msg_, **kwargs) + else: + print(msg_, **kwargs) + if should_use_log_file_console() and kwargs.pop("progress", None) is None: + print_to_log_file(f"[purple]Debug: {msg}[/purple]", **kwargs) + + +def info(msg: str, *, dedupe: bool = False, **kwargs): + """Print an info message. + + Args: + msg: The info message. + dedupe: If True, suppress multiple console logs of info message. + kwargs: Keyword arguments to pass to the print function. + """ + if _LOG_LEVEL <= LogLevel.INFO: + if dedupe: + if msg in _EMITTED_INFO: + return + _EMITTED_INFO.add(msg) + print(f"[cyan]Info: {msg}[/cyan]", **kwargs) + if should_use_log_file_console(): + print_to_log_file(f"[cyan]Info: {msg}[/cyan]", **kwargs) + + +def success(msg: str, *, dedupe: bool = False, **kwargs): + """Print a success message. + + Args: + msg: The success message. + dedupe: If True, suppress multiple console logs of success message. + kwargs: Keyword arguments to pass to the print function. + """ + if _LOG_LEVEL <= LogLevel.INFO: + if dedupe: + if msg in _EMITTED_SUCCESS: + return + _EMITTED_SUCCESS.add(msg) + print(f"[green]Success: {msg}[/green]", **kwargs) + if should_use_log_file_console(): + print_to_log_file(f"[green]Success: {msg}[/green]", **kwargs) + + +def log(msg: str, *, dedupe: bool = False, **kwargs): + """Takes a string and logs it to the console. + + Args: + msg: The message to log. + dedupe: If True, suppress multiple console logs of log message. + kwargs: Keyword arguments to pass to the print function. + """ + if _LOG_LEVEL <= LogLevel.INFO: + if dedupe: + if msg in _EMITTED_LOGS: + return + _EMITTED_LOGS.add(msg) + _console.log(msg, **kwargs) + if should_use_log_file_console(): + print_to_log_file(msg, **kwargs) + + +def rule(title: str, **kwargs): + """Prints a horizontal rule with a title. + + Args: + title: The title of the rule. + kwargs: Keyword arguments to pass to the print function. + """ + _console.rule(title, **kwargs) + + +def warn(msg: str, *, dedupe: bool = False, **kwargs): + """Print a warning message. + + Args: + msg: The warning message. + dedupe: If True, suppress multiple console logs of warning message. + kwargs: Keyword arguments to pass to the print function. + """ + if _LOG_LEVEL <= LogLevel.WARNING: + if dedupe: + if msg in _EMITTED_WARNINGS: + return + _EMITTED_WARNINGS.add(msg) + print(f"[orange1]Warning: {msg}[/orange1]", **kwargs) + if should_use_log_file_console(): + print_to_log_file(f"[orange1]Warning: {msg}[/orange1]", **kwargs) + + +@once +def _exclude_paths_from_frame_info() -> list[Path]: + import importlib.util + + import click + import granian + import socketio + import typing_extensions + + import reflex_core + + try: + import reflex as rx + except ImportError: + rx = None + + # Exclude utility modules that should never be the source of deprecated reflex usage. + exclude_modules: list[ModuleType | None] = [ + click, + rx, + typing_extensions, + socketio, + granian, + reflex_core, + ] + + modules_paths = [file for m in exclude_modules if m and (file := m.__file__)] + [ + spec.origin + for m in [*sys.builtin_module_names, *sys.stdlib_module_names] + if (spec := importlib.util.find_spec(m)) and spec.origin + ] + exclude_roots = [ + p.parent.resolve() if (p := Path(file)).name == "__init__.py" else p.resolve() + for file in modules_paths + ] + # Specifically exclude the reflex cli module. + if reflex_bin := shutil.which(b"reflex"): + exclude_roots.append(Path(reflex_bin.decode())) + + return exclude_roots + + +def _get_first_non_framework_frame() -> FrameType | None: + exclude_roots = _exclude_paths_from_frame_info() + + frame = inspect.currentframe() + while frame := frame and frame.f_back: + frame_path = Path(inspect.getfile(frame)).resolve() + if not any(frame_path.is_relative_to(root) for root in exclude_roots): + break + return frame + + +def deprecate( + *, + feature_name: str, + reason: str, + deprecation_version: str, + removal_version: str, + dedupe: bool = True, + **kwargs, +): + """Print a deprecation warning. + + Args: + feature_name: The feature to deprecate. + reason: The reason for deprecation. + deprecation_version: The version the feature was deprecated + removal_version: The version the deprecated feature will be removed + dedupe: If True, suppress multiple console logs of deprecation message. + kwargs: Keyword arguments to pass to the print function. + """ + dedupe_key = feature_name + loc = "" + + # See if we can find where the deprecation exists in "user code" + origin_frame = _get_first_non_framework_frame() + if origin_frame is not None: + filename = Path(origin_frame.f_code.co_filename) + if filename.is_relative_to(Path.cwd()): + filename = filename.relative_to(Path.cwd()) + loc = f" ({filename}:{origin_frame.f_lineno})" + dedupe_key = f"{dedupe_key} {loc}" + + if dedupe_key not in _EMITTED_DEPRECATION_WARNINGS: + msg = ( + f"{feature_name} has been deprecated in version {deprecation_version}. {reason.rstrip('.').lstrip('. ')}. It will be completely " + f"removed in {removal_version}.{loc}" + ) + if _LOG_LEVEL <= LogLevel.WARNING: + print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs) + if should_use_log_file_console(): + print_to_log_file(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs) + if dedupe: + _EMITTED_DEPRECATION_WARNINGS.add(dedupe_key) + + +def error(msg: str, *, dedupe: bool = False, **kwargs): + """Print an error message. + + Args: + msg: The error message. + dedupe: If True, suppress multiple console logs of error message. + kwargs: Keyword arguments to pass to the print function. + """ + if _LOG_LEVEL <= LogLevel.ERROR: + if dedupe: + if msg in _EMITTED_ERRORS: + return + _EMITTED_ERRORS.add(msg) + _print_stderr(f"[red]{msg}[/red]", **kwargs) + if should_use_log_file_console(): + print_to_log_file(f"[red]{msg}[/red]", **kwargs) + + +def ask( + question: str, + choices: list[str] | None = None, + default: str | None = None, + show_choices: bool = True, +) -> str | None: + """Takes a prompt question and optionally a list of choices + and returns the user input. + + Args: + question: The question to ask the user. + choices: A list of choices to select from. + default: The default option selected. + show_choices: Whether to show the choices. + + Returns: + A string with the user input. + """ + return Prompt.ask( + question, choices=choices, default=default, show_choices=show_choices + ) + + +def progress(): + """Create a new progress bar. + + Returns: + A new progress bar. + """ + return Progress( + *Progress.get_default_columns()[:-1], + MofNCompleteColumn(), + TimeElapsedColumn(), + ) + + +def status(*args, **kwargs): + """Create a status with a spinner. + + Args: + *args: Args to pass to the status. + **kwargs: Kwargs to pass to the status. + + Returns: + A new status. + """ + return _console.status(*args, **kwargs) + + +@contextlib.contextmanager +def timing(msg: str): + """Create a context manager to time a block of code. + + Args: + msg: The message to display. + + Yields: + None. + """ + start = time.time() + try: + yield + finally: + debug(f"[white]\\[timing] {msg}: {time.time() - start:.2f}s[/white]") + + +class PoorProgress: + """A poor man's progress bar.""" + + def __init__(self): + """Initialize the progress bar.""" + super().__init__() + self.tasks = {} + self.progress = 0 + self.total = 0 + + def add_task(self, task: str, total: int): + """Add a task to the progress bar. + + Args: + task: The task name. + total: The total number of steps for the task. + + Returns: + The task ID. + """ + self.total += total + task_id = TaskID(len(self.tasks)) + self.tasks[task_id] = {"total": total, "current": 0} + return task_id + + def advance(self, task: TaskID, advance: int = 1): + """Advance the progress of a task. + + Args: + task: The task ID. + advance: The number of steps to advance. + """ + if task in self.tasks: + self.tasks[task]["current"] += advance + self.progress += advance + _console.print(f"Progress: {self.progress}/{self.total}") + + def start(self): + """Start the progress bar.""" + + def stop(self): + """Stop the progress bar.""" diff --git a/packages/reflex-core/src/reflex_core/utils/decorator.py b/packages/reflex-core/src/reflex_core/utils/decorator.py new file mode 100644 index 00000000000..4129b18a2ee --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/decorator.py @@ -0,0 +1,148 @@ +"""Decorator utilities.""" + +import functools +from collections.abc import Callable +from pathlib import Path +from typing import ParamSpec, TypeVar, cast + +T = TypeVar("T") + + +def once(f: Callable[[], T]) -> Callable[[], T]: + """A decorator that calls the function once and caches the result. + + Args: + f: The function to call. + + Returns: + A function that calls the function once and caches the result. + """ + unset = object() + value: object | T = unset + + @functools.wraps(f) + def wrapper() -> T: + nonlocal value + value = f() if value is unset else value + return value # pyright: ignore[reportReturnType] + + return wrapper + + +def once_unless_none(f: Callable[[], T | None]) -> Callable[[], T | None]: + """A decorator that calls the function once and caches the result unless it is None. + + Args: + f: The function to call. + + Returns: + A function that calls the function once and caches the result unless it is None. + """ + value: T | None = None + + @functools.wraps(f) + def wrapper() -> T | None: + nonlocal value + value = f() if value is None else value + return value + + return wrapper + + +P = ParamSpec("P") + + +def debug(f: Callable[P, T]) -> Callable[P, T]: + """A decorator that prints the function name, arguments, and result. + + Args: + f: The function to call. + + Returns: + A function that prints the function name, arguments, and result. + """ + + @functools.wraps(f) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + result = f(*args, **kwargs) + print( # noqa: T201 + f"Calling {f.__name__} with args: {args} and kwargs: {kwargs}, result: {result}" + ) + return result + + return wrapper + + +def _write_cached_procedure_file(payload: str, cache_file: Path, value: object): + import pickle + + cache_file.parent.mkdir(parents=True, exist_ok=True) + cache_file.write_bytes(pickle.dumps((payload, value))) + + +def _read_cached_procedure_file(cache_file: Path) -> tuple[str | None, object]: + import pickle + + if cache_file.exists(): + with cache_file.open("rb") as f: + return pickle.loads(f.read()) + + return None, None + + +P = ParamSpec("P") +Picklable = TypeVar("Picklable") + + +def cached_procedure( + cache_file_path: Callable[[], Path], + payload_fn: Callable[P, str], +) -> Callable[[Callable[P, Picklable]], Callable[P, Picklable]]: + """Decorator to cache the result of a function based on its arguments. + + Args: + cache_file_path: Function that computes the cache file path. + payload_fn: Function that computes cache payload from function args. + + Returns: + The decorated function. + """ + + def _inner_decorator(func: Callable[P, Picklable]) -> Callable[P, Picklable]: + def _inner(*args: P.args, **kwargs: P.kwargs) -> Picklable: + cache_file = cache_file_path() + + payload, value = _read_cached_procedure_file(cache_file) + new_payload = payload_fn(*args, **kwargs) + + if payload != new_payload: + new_value = func(*args, **kwargs) + _write_cached_procedure_file(new_payload, cache_file, new_value) + return new_value + + from reflex_core.utils import console + + console.debug( + f"Using cached value for {func.__name__} with payload: {new_payload}" + ) + return cast("Picklable", value) + + return _inner + + return _inner_decorator + + +def cache_result_in_disk( + cache_file_path: Callable[[], Path], +) -> Callable[[Callable[[], Picklable]], Callable[[], Picklable]]: + """Decorator to cache the result of a function on disk. + + Args: + cache_file_path: Function that computes the cache file path. + + Returns: + The decorated function. + """ + return cached_procedure( + cache_file_path=cache_file_path, payload_fn=lambda: "constant" + ) diff --git a/packages/reflex-core/src/reflex_core/utils/exceptions.py b/packages/reflex-core/src/reflex_core/utils/exceptions.py new file mode 100644 index 00000000000..4617560e45e --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/exceptions.py @@ -0,0 +1,286 @@ +"""Custom Exceptions.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from reflex_core.vars import Var + + +class ReflexError(Exception): + """Base exception for all Reflex exceptions.""" + + +class ConfigError(ReflexError): + """Custom exception for config related errors.""" + + +class InvalidStateManagerModeError(ReflexError, ValueError): + """Raised when an invalid state manager mode is provided.""" + + +class ReflexRuntimeError(ReflexError, RuntimeError): + """Custom RuntimeError for Reflex.""" + + +class UploadTypeError(ReflexError, TypeError): + """Custom TypeError for upload related errors.""" + + +class EnvVarValueError(ReflexError, ValueError): + """Custom ValueError raised when unable to convert env var to expected type.""" + + +class ComponentTypeError(ReflexError, TypeError): + """Custom TypeError for component related errors.""" + + +class ChildrenTypeError(ComponentTypeError): + """Raised when the children prop of a component is not a valid type.""" + + def __init__(self, component: str, child: Any): + """Initialize the exception. + + Args: + component: The name of the component. + child: The child that caused the error. + """ + super().__init__( + f"Component {component} received child {child} of type {type(child)}. " + "Accepted types are other components, state vars, or primitive Python types (dict excluded)." + ) + + +class EventHandlerTypeError(ReflexError, TypeError): + """Custom TypeError for event handler related errors.""" + + +class EventHandlerValueError(ReflexError, ValueError): + """Custom ValueError for event handler related errors.""" + + +class StateValueError(ReflexError, ValueError): + """Custom ValueError for state related errors.""" + + +class VarNameError(ReflexError, NameError): + """Custom NameError for when a state var has been shadowed by a substate var.""" + + +class VarTypeError(ReflexError, TypeError): + """Custom TypeError for var related errors.""" + + +class VarValueError(ReflexError, ValueError): + """Custom ValueError for var related errors.""" + + +class VarAttributeError(ReflexError, AttributeError): + """Custom AttributeError for var related errors.""" + + +class UntypedVarError(ReflexError, TypeError): + """Custom TypeError for untyped var errors.""" + + def __init__(self, var: Var, action: str, doc_link: str = ""): + """Create an UntypedVarError from a var. + + Args: + var: The var. + action: The action that caused the error. + doc_link: The link to the documentation. + """ + var_data = var._get_all_var_data() + is_state_var = ( + var_data + and var_data.state + and var_data.field_name + and var_data.state + "." + var_data.field_name == str(var) + ) + super().__init__( + f"Cannot {action} on untyped var '{var!s}' of type '{var._var_type!s}'." + + ( + " Please add a type annotation to the var in the state class." + if is_state_var + else " You can call the var's .to(desired_type) method to convert it to the desired type." + ) + + (f" See {doc_link}" if doc_link else "") + ) + + +class UntypedComputedVarError(ReflexError, TypeError): + """Custom TypeError for untyped computed var errors.""" + + def __init__(self, var_name: str): + """Initialize the UntypedComputedVarError. + + Args: + var_name: The name of the computed var. + """ + super().__init__(f"Computed var '{var_name}' must have a type annotation.") + + +class ComputedVarSignatureError(ReflexError, TypeError): + """Custom TypeError for computed var signature errors.""" + + def __init__(self, var_name: str, signature: str): + """Initialize the ComputedVarSignatureError. + + Args: + var_name: The name of the var. + signature: The invalid signature. + """ + super().__init__(f"Computed var `{var_name}{signature}` cannot take arguments.") + + +class MissingAnnotationError(ReflexError, TypeError): + """Custom TypeError for missing annotations.""" + + def __init__(self, var_name: str): + """Initialize the MissingAnnotationError. + + Args: + var_name: The name of the var. + """ + super().__init__(f"Var '{var_name}' must have a type annotation.") + + +class UploadValueError(ReflexError, ValueError): + """Custom ValueError for upload related errors.""" + + +class PageValueError(ReflexError, ValueError): + """Custom ValueError for page related errors.""" + + +class RouteValueError(ReflexError, ValueError): + """Custom ValueError for route related errors.""" + + +class VarOperationTypeError(ReflexError, TypeError): + """Custom TypeError for when unsupported operations are performed on vars.""" + + +class VarDependencyError(ReflexError, ValueError): + """Custom ValueError for when a var depends on a non-existent var.""" + + +class InvalidStylePropError(ReflexError, TypeError): + """Custom Type Error when style props have invalid values.""" + + +class ImmutableStateError(ReflexError): + """Raised when a background task attempts to modify state outside of context.""" + + +class LockExpiredError(ReflexError): + """Raised when the state lock expires while an event is being processed.""" + + +class MatchTypeError(ReflexError, TypeError): + """Raised when the return types of match cases are different.""" + + +class EventHandlerArgTypeMismatchError(ReflexError, TypeError): + """Raised when the annotations of args accepted by an EventHandler differs from the spec of the event trigger.""" + + +class EventFnArgMismatchError(ReflexError, TypeError): + """Raised when the number of args required by an event handler is more than provided by the event trigger.""" + + +class DynamicRouteArgShadowsStateVarError(ReflexError, NameError): + """Raised when a dynamic route arg shadows a state var.""" + + +class ComputedVarShadowsStateVarError(ReflexError, NameError): + """Raised when a computed var shadows a state var.""" + + +class ComputedVarShadowsBaseVarsError(ReflexError, NameError): + """Raised when a computed var shadows a base var.""" + + +class EventHandlerShadowsBuiltInStateMethodError(ReflexError, NameError): + """Raised when an event handler shadows a built-in state method.""" + + +class GeneratedCodeHasNoFunctionDefsError(ReflexError): + """Raised when refactored code generated with flexgen has no functions defined.""" + + +class PrimitiveUnserializableToJSONError(ReflexError, ValueError): + """Raised when a primitive type is unserializable to JSON. Usually with NaN and Infinity.""" + + +class InvalidLifespanTaskTypeError(ReflexError, TypeError): + """Raised when an invalid task type is registered as a lifespan task.""" + + +class DynamicComponentMissingLibraryError(ReflexError, ValueError): + """Raised when a dynamic component is missing a library.""" + + +class SetUndefinedStateVarError(ReflexError, AttributeError): + """Raised when setting the value of a var without first declaring it.""" + + +class StateSchemaMismatchError(ReflexError, TypeError): + """Raised when the serialized schema of a state class does not match the current schema.""" + + +class EnvironmentVarValueError(ReflexError, ValueError): + """Raised when an environment variable is set to an invalid value.""" + + +class DynamicComponentInvalidSignatureError(ReflexError, TypeError): + """Raised when a dynamic component has an invalid signature.""" + + +class InvalidPropValueError(ReflexError): + """Raised when a prop value is invalid.""" + + +class StateTooLargeError(ReflexError): + """Raised when the state is too large to be serialized.""" + + +class StateSerializationError(ReflexError): + """Raised when the state cannot be serialized.""" + + +class StateMismatchError(ReflexError, ValueError): + """Raised when the state retrieved does not match the expected state.""" + + +class SystemPackageMissingError(ReflexError): + """Raised when a system package is missing.""" + + def __init__(self, package: str): + """Initialize the SystemPackageMissingError. + + Args: + package: The missing package. + """ + from reflex_core.constants.base import IS_MACOS + + extra = ( + f" You can do so by running 'brew install {package}'." if IS_MACOS else "" + ) + super().__init__( + f"System package '{package}' is missing." + f" Please install it through your system package manager.{extra}" + ) + + +class EventDeserializationError(ReflexError, ValueError): + """Raised when an event cannot be deserialized.""" + + +class InvalidLockWarningThresholdError(ReflexError): + """Raised when an invalid lock warning threshold is provided.""" + + +class UnretrievableVarValueError(ReflexError): + """Raised when the value of a var is not retrievable.""" diff --git a/packages/reflex-core/src/reflex_core/utils/format.py b/packages/reflex-core/src/reflex_core/utils/format.py new file mode 100644 index 00000000000..4e673e79af9 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/format.py @@ -0,0 +1,780 @@ +"""Formatting operations.""" + +from __future__ import annotations + +import inspect +import json +import os +import re +from typing import TYPE_CHECKING, Any + +from reflex_core import constants +from reflex_core.constants.state import FRONTEND_EVENT_STATE +from reflex_core.utils import exceptions + +if TYPE_CHECKING: + from reflex_core.components.component import ComponentStyle + from reflex_core.event import ( + ArgsSpec, + EventChain, + EventHandler, + EventSpec, + EventType, + ) + +WRAP_MAP = { + "{": "}", + "(": ")", + "[": "]", + "<": ">", + '"': '"', + "'": "'", + "`": "`", +} + + +def length_of_largest_common_substring(str1: str, str2: str) -> int: + """Find the length of the largest common substring between two strings. + + Args: + str1: The first string. + str2: The second string. + + Returns: + The length of the largest common substring. + """ + if not str1 or not str2: + return 0 + + # Create a matrix of size (len(str1) + 1) x (len(str2) + 1) + dp = [[0] * (len(str2) + 1) for _ in range(len(str1) + 1)] + + # Variables to keep track of maximum length and ending position + max_length = 0 + + # Fill the dp matrix + for i in range(1, len(str1) + 1): + for j in range(1, len(str2) + 1): + if str1[i - 1] == str2[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + if dp[i][j] > max_length: + max_length = dp[i][j] + + return max_length + + +def get_close_char(open: str, close: str | None = None) -> str: + """Check if the given character is a valid brace. + + Args: + open: The open character. + close: The close character if provided. + + Returns: + The close character. + + Raises: + ValueError: If the open character is not a valid brace. + """ + if close is not None: + return close + if open not in WRAP_MAP: + msg = f"Invalid wrap open: {open}, must be one of {WRAP_MAP.keys()}" + raise ValueError(msg) + return WRAP_MAP[open] + + +def is_wrapped(text: str, open: str, close: str | None = None) -> bool: + """Check if the given text is wrapped in the given open and close characters. + + "(a) + (b)" --> False + "((abc))" --> True + "(abc)" --> True + + Args: + text: The text to check. + open: The open character. + close: The close character. + + Returns: + Whether the text is wrapped. + """ + close = get_close_char(open, close) + if not (text.startswith(open) and text.endswith(close)): + return False + + depth = 0 + for ch in text[:-1]: + if ch == open: + depth += 1 + if ch == close: + depth -= 1 + if depth == 0: # it shouldn't close before the end + return False + return True + + +def wrap( + text: str, + open: str, + close: str | None = None, + check_first: bool = True, + num: int = 1, +) -> str: + """Wrap the given text in the given open and close characters. + + Args: + text: The text to wrap. + open: The open character. + close: The close character. + check_first: Whether to check if the text is already wrapped. + num: The number of times to wrap the text. + + Returns: + The wrapped text. + """ + close = get_close_char(open, close) + + # If desired, check if the text is already wrapped in braces. + if check_first and is_wrapped(text=text, open=open, close=close): + return text + + # Wrap the text in braces. + return f"{open * num}{text}{close * num}" + + +def indent(text: str, indent_level: int = 2) -> str: + """Indent the given text by the given indent level. + + Args: + text: The text to indent. + indent_level: The indent level. + + Returns: + The indented text. + """ + lines = text.splitlines() + if len(lines) < 2: + return text + return os.linesep.join(f"{' ' * indent_level}{line}" for line in lines) + os.linesep + + +def to_snake_case(text: str) -> str: + """Convert a string to snake case. + + The words in the text are converted to lowercase and + separated by underscores. + + Args: + text: The string to convert. + + Returns: + The snake case string. + """ + s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", text) + return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower().replace("-", "_") + + +def to_camel_case(text: str, treat_hyphens_as_underscores: bool = True) -> str: + """Convert a string to camel case. + + The first word in the text is converted to lowercase and + the rest of the words are converted to title case, removing underscores. + + Args: + text: The string to convert. + treat_hyphens_as_underscores: Whether to allow hyphens in the string. + + Returns: + The camel case string. + """ + if treat_hyphens_as_underscores: + text = text.replace("-", "_") + words = text.split("_") + # Capitalize the first letter of each word except the first one + if len(words) == 1: + return words[0] + return words[0] + "".join([w.capitalize() for w in words[1:]]) + + +def to_title_case(text: str, sep: str = "") -> str: + """Convert a string from snake case to title case. + + Args: + text: The string to convert. + sep: The separator to use to join the words. + + Returns: + The title case string. + """ + return sep.join(word.title() for word in text.split("_")) + + +def to_kebab_case(text: str) -> str: + """Convert a string to kebab case. + + The words in the text are converted to lowercase and + separated by hyphens. + + Args: + text: The string to convert. + + Returns: + The title case string. + """ + return to_snake_case(text).replace("_", "-") + + +def make_default_page_title(app_name: str, route: str) -> str: + """Make a default page title from a route. + + Args: + app_name: The name of the app owning the page. + route: The route to make the title from. + + Returns: + The default page title. + """ + route_parts = [ + part + for part in route.split("/") + if part and not (part.startswith("[") and part.endswith("]")) + ] + + title = constants.DefaultPage.TITLE.format( + app_name, route_parts[-1] if route_parts else constants.PageNames.INDEX_ROUTE + ) + return to_title_case(title) + + +def _escape_js_string(string: str) -> str: + """Escape the string for use as a JS string literal. + + Args: + string: The string to escape. + + Returns: + The escaped string. + """ + + # TODO: we may need to re-visit this logic after new Var API is implemented. + def escape_outside_segments(segment: str): + """Escape backticks in segments outside of `${}`. + + Args: + segment: The part of the string to escape. + + Returns: + The escaped or unescaped segment. + """ + if segment.startswith("${") and segment.endswith("}"): + # Return the `${}` segment unchanged + return segment + # Escape backticks in the segment + return segment.replace(r"\`", "`").replace("`", r"\`") + + # Split the string into parts, keeping the `${}` segments + parts = re.split(r"(\$\{.*?\})", string) + escaped_parts = [escape_outside_segments(part) for part in parts] + return "".join(escaped_parts) + + +def _wrap_js_string(string: str) -> str: + """Wrap string so it looks like {`string`}. + + Args: + string: The string to wrap. + + Returns: + The wrapped string. + """ + string = wrap(string, "`") + return wrap(string, "{") + + +def format_string(string: str) -> str: + """Format the given string as a JS string literal.. + + Args: + string: The string to format. + + Returns: + The formatted string. + """ + return _wrap_js_string(_escape_js_string(string)) + + +def format_var(var: Var) -> str: + """Format the given Var as a javascript value. + + Args: + var: The Var to format. + + Returns: + The formatted Var. + """ + return str(var) + + +def format_route(route: str) -> str: + """Format the given route. + + Args: + route: The route to format. + + Returns: + The formatted route. + """ + route = route.strip("/") + + # If the route is empty, return the index route. + if route == "": + return constants.PageNames.INDEX_ROUTE + + return route + + +def format_match( + cond: str | Var, + match_cases: list[tuple[list[Var], Var]], + default: Var, +) -> str: + """Format a match expression whose return type is a Var. + + Args: + cond: The condition. + match_cases: The list of cases to match. + default: The default case. + + Returns: + The formatted match expression + + """ + switch_code = f"(() => {{ switch (JSON.stringify({cond})) {{" + + for case in match_cases: + conditions, return_value = case + + case_conditions = " ".join([ + f"case JSON.stringify({condition!s}):" for condition in conditions + ]) + case_code = f"{case_conditions} return ({return_value!s}); break;" + switch_code += case_code + + switch_code += f"default: return ({default!s}); break;" + switch_code += "};})()" + + return switch_code + + +def format_prop( + prop: Var | EventChain | ComponentStyle | str, +) -> int | float | str: + """Format a prop. + + Args: + prop: The prop to format. + + Returns: + The formatted prop to display within a tag. + + Raises: + exceptions.InvalidStylePropError: If the style prop value is not a valid type. + TypeError: If the prop is not valid. + ValueError: If the prop is not a string. + """ + # import here to avoid circular import. + from reflex_core.event import EventChain + from reflex_core.utils import serializers + from reflex_core.vars import Var + + try: + # Handle var props. + if isinstance(prop, Var): + return str(prop) + + # Handle event props. + if isinstance(prop, EventChain): + return str(Var.create(prop)) + + # Handle other types. + if isinstance(prop, str): + if is_wrapped(prop, "{"): + return prop + return json_dumps(prop) + + # For dictionaries, convert any properties to strings. + if isinstance(prop, dict): + prop = serializers.serialize_dict(prop) # pyright: ignore [reportAttributeAccessIssue] + + else: + # Dump the prop as JSON. + prop = json_dumps(prop) + except exceptions.InvalidStylePropError: + raise + except TypeError as e: + msg = f"Could not format prop: {prop} of type {type(prop)}" + raise TypeError(msg) from e + + # Wrap the variable in braces. + if not isinstance(prop, str): + msg = f"Invalid prop: {prop}. Expected a string." + raise ValueError(msg) + return wrap(prop, "{", check_first=False) + + +def format_props(*single_props, **key_value_props) -> list[str]: + """Format the tag's props. + + Args: + single_props: Props that are not key-value pairs. + key_value_props: Props that are key-value pairs. + + Returns: + The formatted props list. + """ + # Format all the props. + from reflex_core.vars import LiteralStringVar, LiteralVar, Var + + return [ + (str(LiteralStringVar.create(name)) if "-" in name else name) + + ":" + + str(format_prop(prop if isinstance(prop, Var) else LiteralVar.create(prop))) + for name, prop in sorted(key_value_props.items()) + if prop is not None + ] + [(f"...{LiteralVar.create(prop)!s}") for prop in single_props] + + +def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: + """Get the state and function name of an event handler. + + Args: + handler: The event handler to get the parts of. + + Returns: + The state and function name. + """ + # Get the class that defines the event handler. + parts = handler.fn.__qualname__.split(".") + + # Get the state full name + state_full_name = handler.state_full_name + + # If there's no enclosing class, just return the function name. + if not state_full_name: + return ("", parts[-1]) + + # Get the function name + name = parts[-1] + + from reflex.state import State + + if state_full_name == FRONTEND_EVENT_STATE and name not in State.__dict__: + return ("", to_snake_case(handler.fn.__qualname__)) + + return (state_full_name, name) + + +def format_event_handler(handler: EventHandler) -> str: + """Format an event handler. + + Args: + handler: The event handler to format. + + Returns: + The formatted function. + """ + state, name = get_event_handler_parts(handler) + if state == "": + return name + return f"{state}.{name}" + + +def format_event(event_spec: EventSpec) -> str: + """Format an event. + + Args: + event_spec: The event to format. + + Returns: + The compiled event. + """ + args = ",".join([ + ":".join(( + name._js_expr, + ( + wrap( + json.dumps(val._js_expr).strip('"').replace("`", "\\`"), + "`", + ) + if val._var_is_string + else str(val) + ), + )) + for name, val in event_spec.args + ]) + event_args = [ + wrap(format_event_handler(event_spec.handler), '"'), + ] + event_args.append(wrap(args, "{")) + + if event_spec.client_handler_name: + event_args.append(wrap(event_spec.client_handler_name, '"')) + return f"ReflexEvent({', '.join(event_args)})" + + +if TYPE_CHECKING: + from reflex_core.vars import Var + + +def format_queue_events( + events: EventType[Any] | None = None, + args_spec: ArgsSpec | None = 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. + + Raises: + ValueError: If a lambda function is given which returns a Var. + """ + from reflex_core.event import ( + EventChain, + EventHandler, + EventSpec, + call_event_fn, + call_event_handler, + ) + from reflex_core.vars import FunctionVar, Var + + if not events: + return Var("(() => null)").to(FunctionVar, 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) + 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) # pyright: ignore [reportAssignmentType, reportArgumentType] + if isinstance(specs, Var): + msg = f"Invalid event spec: {specs}. Expected a list of EventSpecs." + raise ValueError(msg) + 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( + f"{arg_def} => {{queueEvents([{','.join(payloads)}], {constants.CompileVars.SOCKET}, false, navigate, params);" + f"processEvent({constants.CompileVars.SOCKET}, navigate, params);}}", + ).to(FunctionVar, EventChain) + + +def format_query_params(router_data: dict[str, Any]) -> dict[str, str]: + """Convert back query params name to python-friendly case. + + Args: + router_data: the router_data dict containing the query params + + Returns: + The reformatted query params + """ + params = router_data[constants.RouteVar.QUERY] + return {k.replace("-", "_"): v for k, v in params.items()} + + +def format_state_name(state_name: str) -> str: + """Format a state name, replacing dots with double underscore. + + This allows individual substates to be accessed independently as javascript vars + without using dot notation. + + Args: + state_name: The state name to format. + + Returns: + The formatted state name. + """ + return state_name.replace(".", "__") + + +def format_ref(ref: str) -> str: + """Format a ref. + + Args: + ref: The ref to format. + + Returns: + The formatted ref. + """ + # Replace all non-word characters with underscores. + clean_ref = re.sub(r"[^\w]+", "_", ref) + return f"ref_{clean_ref}" + + +def format_library_name(library_fullname: str | dict[str, Any]) -> str: + """Format the name of a library. + + Args: + library_fullname: The library reference, either as a string or a dictionary with a 'name' key. + + Returns: + The name without the @version if it was part of the name + + Raises: + KeyError: If library_fullname is a dictionary without a 'name' key. + TypeError: If library_fullname or its 'name' value is not a string. + """ + # If input is a dictionary, extract the 'name' key + if isinstance(library_fullname, dict): + if "name" not in library_fullname: + msg = "Dictionary input must contain a 'name' key" + raise KeyError(msg) + library_fullname = library_fullname["name"] + + # Process the library name as a string + if not isinstance(library_fullname, str): + msg = "Library name must be a string" + raise TypeError(msg) + + if library_fullname.startswith("https://"): + return library_fullname + + lib, at, version = library_fullname.rpartition("@") + if not lib: + lib = at + version + + return lib + + +def json_dumps(obj: Any, **kwargs) -> str: + """Takes an object and returns a jsonified string. + + Args: + obj: The object to be serialized. + kwargs: Additional keyword arguments to pass to json.dumps. + + Returns: + A string + """ + from reflex_core.utils import serializers + + kwargs.setdefault("ensure_ascii", False) + kwargs.setdefault("default", serializers.serialize) + + return json.dumps(obj, **kwargs) + + +def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]: + """Collapse keys with consecutive suffixes into a single list value. + + Separators dash and underscore are removed, unless this would overwrite an existing key. + + Args: + form_dict: The dict to collapse. + + Returns: + The collapsed dict. + """ + ending_digit_regex = re.compile(r"^(.*?)[_-]?(\d+)$") + collapsed = {} + for k in sorted(form_dict): + m = ending_digit_regex.match(k) + if m: + collapsed.setdefault(m.group(1), []).append(form_dict[k]) + # collapsing never overwrites valid data from the form_dict + collapsed.update(form_dict) + return collapsed + + +def format_array_ref(refs: str, idx: Var | None) -> str: + """Format a ref accessed by array. + + Args: + refs : The ref array to access. + idx : The index of the ref in the array. + + Returns: + The formatted ref. + """ + clean_ref = re.sub(r"[^\w]+", "_", refs) + if idx is not None: + return f"refs_{clean_ref}[{idx!s}]" + return f"refs_{clean_ref}" + + +def format_data_editor_column(col: str | dict): + """Format a given column into the proper format. + + Args: + col: The column. + + Returns: + The formatted column. + + Raises: + ValueError: invalid type provided for column. + """ + from reflex_core.vars import Var + + if isinstance(col, str): + return {"title": col, "id": col.lower(), "type": "str"} + + if isinstance(col, (dict,)): + if "id" not in col: + col["id"] = col["title"].lower() + if "type" not in col: + col["type"] = "str" + if "overlayIcon" not in col: + col["overlayIcon"] = None + return col + + if isinstance(col, Var): + return col + + msg = f"unexpected type ({(type(col).__name__)}: {col}) for column header in data_editor" + raise ValueError(msg) + + +def format_data_editor_cell(cell: Any): + """Format a given data into a renderable cell for data_editor. + + Args: + cell: The data to format. + + Returns: + The formatted cell. + """ + from reflex_core.vars.base import Var + + return { + "kind": Var(_js_expr="GridCellKind.Text"), + "data": cell, + } diff --git a/packages/reflex-core/src/reflex_core/utils/imports.py b/packages/reflex-core/src/reflex_core/utils/imports.py new file mode 100644 index 00000000000..e4ec1935739 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/imports.py @@ -0,0 +1,150 @@ +"""Import operations.""" + +from __future__ import annotations + +import dataclasses +from collections import defaultdict +from collections.abc import Mapping, Sequence + + +def merge_parsed_imports( + *imports: ImmutableParsedImportDict, +) -> ParsedImportDict: + """Merge multiple parsed import dicts together. + + Args: + *imports: The list of import dicts to merge. + + Returns: + The merged import dicts. + """ + all_imports: defaultdict[str, list[ImportVar]] = defaultdict(list) + for import_dict in imports: + for lib, fields in import_dict.items(): + all_imports[lib].extend(fields) + return all_imports + + +def merge_imports( + *imports: ImportDict | ParsedImportDict | ParsedImportTuple, +) -> ParsedImportDict: + """Merge multiple import dicts together. + + Args: + *imports: The list of import dicts to merge. + + Returns: + The merged import dicts. + """ + all_imports: defaultdict[str, list[ImportVar]] = defaultdict(list) + for import_dict in imports: + for lib, fields in ( + import_dict if isinstance(import_dict, tuple) else import_dict.items() + ): + # If the lib is an absolute path, we need to prefix it with a $ + lib = ( + "$" + lib + if lib.startswith(("/utils/", "/components/", "/styles/", "/public/")) + else lib + ) + if isinstance(fields, (list, tuple, set)): + all_imports[lib].extend( + ImportVar(field) if isinstance(field, str) else field + for field in fields + ) + else: + all_imports[lib].append( + ImportVar(fields) if isinstance(fields, str) else fields + ) + return all_imports + + +def parse_imports( + imports: ImmutableImportDict | ImmutableParsedImportDict, +) -> ParsedImportDict: + """Parse the import dict into a standard format. + + Args: + imports: The import dict to parse. + + Returns: + The parsed import dict. + """ + return { + package: [maybe_tags] + if isinstance(maybe_tags, ImportVar) + else [ImportVar(tag=maybe_tags)] + if isinstance(maybe_tags, str) + else [ImportVar(tag=tag) if isinstance(tag, str) else tag for tag in maybe_tags] + for package, maybe_tags in imports.items() + } + + +def collapse_imports( + imports: ParsedImportDict | ParsedImportTuple, +) -> ParsedImportDict: + """Remove all duplicate ImportVar within an ImportDict. + + Args: + imports: The import dict to collapse. + + Returns: + The collapsed import dict. + """ + return { + lib: ( + list(set(import_vars)) + if isinstance(import_vars, list) + else list(import_vars) + ) + for lib, import_vars in ( + imports if isinstance(imports, tuple) else imports.items() + ) + } + + +@dataclasses.dataclass(frozen=True) +class ImportVar: + """An import var.""" + + # The name of the import tag. + tag: str | None + + # whether the import is default or named. + is_default: bool | None = False + + # The tag alias. + alias: str | None = None + + # Whether this import need to install the associated lib + install: bool | None = True + + # whether this import should be rendered or not + render: bool | None = True + + # The path of the package to import from. + package_path: str = "/" + + @property + def name(self) -> str: + """The name of the import. + + Returns: + The name(tag name with alias) of tag. + """ + if self.alias: + return ( + self.alias + if self.is_default and self.tag != "*" + else (self.tag + " as " + self.alias if self.tag else self.alias) + ) + return self.tag or "" + + +ImportTypes = str | ImportVar | list[str | ImportVar] | list[ImportVar] +ImmutableImportTypes = str | ImportVar | Sequence[str | ImportVar] +ImportDict = dict[str, ImportTypes] +ImmutableImportDict = Mapping[str, ImmutableImportTypes] +ParsedImportDict = dict[str, list[ImportVar]] +ImmutableParsedImportDict = Mapping[str, Sequence[ImportVar]] +ParsedImportTuple = tuple[tuple[str, tuple[ImportVar, ...]], ...] diff --git a/packages/reflex-core/src/reflex_core/utils/lazy_loader.py b/packages/reflex-core/src/reflex_core/utils/lazy_loader.py new file mode 100644 index 00000000000..3b1dd42798e --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/lazy_loader.py @@ -0,0 +1,100 @@ +"""Module to implement lazy loading in reflex. + +BSD 3-Clause License + +Copyright (c) 2022--2023, Scientific Python project All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +from __future__ import annotations + +import copy +import importlib +import os +import sys + + +def attach( + package_name: str, + submodules: set[str] | None = None, + submod_attrs: dict[str, list[str]] | None = None, + **extra_mappings: str, +): + """Replaces a package's __getattr__, __dir__, and __all__ attributes using lazy.attach. + The lazy loader __getattr__ doesn't support tuples as list values. We needed to add + this functionality (tuples) in Reflex to support 'import as _' statements. This function + reformats the submod_attrs dictionary to flatten the module list before passing it to + lazy_loader. + + Args: + package_name: name of the package. + submodules : List of submodules to attach. + submod_attrs : Dictionary of submodule -> list of attributes / functions. + These attributes are imported as they are used. + extra_mappings: Additional mappings to resolve lazily. + + Returns: + __getattr__, __dir__, __all__ + """ + submod_attrs = copy.deepcopy(submod_attrs) + if submod_attrs: + for k, v in submod_attrs.items(): + # when flattening the list, only keep the alias in the tuple(mod[1]) + submod_attrs[k] = [ + mod if not isinstance(mod, tuple) else mod[1] for mod in v + ] + + if submod_attrs is None: + submod_attrs = {} + + submodules = set(submodules) if submodules is not None else set() + + attr_to_modules = { + attr: mod for mod, attrs in submod_attrs.items() for attr in attrs + } + + __all__ = sorted([*(submodules | attr_to_modules.keys()), *(extra_mappings or [])]) + + def __getattr__(name: str): # noqa: N807 + if name in extra_mappings: + path = extra_mappings[name] + if "." not in path: + return importlib.import_module(path) + submod_path, attr = path.rsplit(".", 1) + submod = importlib.import_module(submod_path) + return getattr(submod, attr) + if name in submodules: + return importlib.import_module(f"{package_name}.{name}") + if name in attr_to_modules: + submod_path = f"{package_name}.{attr_to_modules[name]}" + submod = importlib.import_module(submod_path) + attr = getattr(submod, name) + + # If the attribute lives in a file (module) with the same + # name as the attribute, ensure that the attribute and *not* + # the module is accessible on the package. + if name == attr_to_modules[name]: + pkg = sys.modules[package_name] + pkg.__dict__[name] = attr + + return attr + msg = f"No {package_name} attribute {name}" + raise AttributeError(msg) + + def __dir__(): # noqa: N807 + return __all__ + + if os.environ.get("EAGER_IMPORT", ""): + for attr in set(attr_to_modules.keys()) | submodules: + __getattr__(attr) + + return __getattr__, __dir__, list(__all__) diff --git a/packages/reflex-core/src/reflex_core/utils/pyi_generator.py b/packages/reflex-core/src/reflex_core/utils/pyi_generator.py new file mode 100644 index 00000000000..ef54acd4ab9 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/pyi_generator.py @@ -0,0 +1,1656 @@ +"""The pyi generator module.""" + +from __future__ import annotations + +import ast +import contextlib +import importlib +import inspect +import json +import logging +import multiprocessing +import re +import subprocess +import sys +import typing +from collections.abc import Callable, Iterable, Mapping, Sequence +from concurrent.futures import ProcessPoolExecutor +from functools import cache +from hashlib import md5 +from inspect import getfullargspec +from itertools import chain +from pathlib import Path +from types import MappingProxyType, ModuleType, SimpleNamespace, UnionType +from typing import Any, get_args, get_origin + +from reflex_core.components.component import Component +from reflex_core.vars.base import Var + + +def _is_union(cls: Any) -> bool: + origin = getattr(cls, "__origin__", None) + if origin is typing.Union: + return True + return origin is None and isinstance(cls, UnionType) + + +def _is_optional(cls: Any) -> bool: + return ( + cls is None + or cls is type(None) + or (_is_union(cls) and type(None) in get_args(cls)) + ) + + +def _is_literal(cls: Any) -> bool: + return getattr(cls, "__origin__", None) is typing.Literal + + +def _safe_issubclass(cls: Any, cls_check: Any | tuple[Any, ...]) -> bool: + try: + return issubclass(cls, cls_check) + except TypeError: + return False + + +logger = logging.getLogger("pyi_generator") + +PWD = Path.cwd() + +PYI_HASHES = "pyi_hashes.json" + +EXCLUDED_FILES = [ + "app.py", + "component.py", + "bare.py", + "foreach.py", + "cond.py", + "match.py", + "multiselect.py", + "literals.py", +] + +# These props exist on the base component, but should not be exposed in create methods. +EXCLUDED_PROPS = [ + "alias", + "children", + "event_triggers", + "library", + "lib_dependencies", + "tag", + "is_default", + "special_props", + "_is_tag_in_global_scope", + "_invalid_children", + "_memoization_mode", + "_rename_props", + "_valid_children", + "_valid_parents", + "State", +] + +OVERWRITE_TYPES = { + "style": "Sequence[Mapping[str, Any]] | Mapping[str, Any] | Var[Mapping[str, Any]] | Breakpoints | None", +} + +DEFAULT_TYPING_IMPORTS = { + "Any", + "Callable", + "Dict", + # "List", + "Sequence", + "Mapping", + "Literal", + "Optional", + "Union", + "Annotated", +} + +# TODO: fix import ordering and unused imports with ruff later +DEFAULT_IMPORTS = { + "typing": sorted(DEFAULT_TYPING_IMPORTS), + "reflex_components_core.core.breakpoints": ["Breakpoints"], + "reflex_core.event": [ + "EventChain", + "EventHandler", + "EventSpec", + "EventType", + "KeyInputInfo", + "PointerEventInfo", + ], + "reflex_core.style": ["Style"], + "reflex_core.vars.base": ["Var"], +} + + +def _walk_files(path: str | Path): + """Walk all files in a path. + This can be replaced with Path.walk() in python3.12. + + Args: + path: The path to walk. + + Yields: + The next file in the path. + """ + for p in Path(path).iterdir(): + if p.is_dir(): + yield from _walk_files(p) + continue + yield p.resolve() + + +def _relative_to_pwd(path: Path) -> Path: + """Get the relative path of a path to the current working directory. + + Args: + path: The path to get the relative path for. + + Returns: + The relative path. + """ + if path.is_absolute(): + return path.relative_to(PWD) + return path + + +def _get_type_hint( + value: Any, type_hint_globals: dict, is_optional: bool = True +) -> str: + """Resolve the type hint for value. + + Args: + value: The type annotation as a str or actual types/aliases. + type_hint_globals: The globals to use to resolving a type hint str. + is_optional: Whether the type hint should be wrapped in Optional. + + Returns: + The resolved type hint as a str. + + Raises: + TypeError: If the value name is not visible in the type hint globals. + """ + res = "" + args = get_args(value) + + if value is type(None) or value is None: + return "None" + + if _is_union(value): + if type(None) in value.__args__: + res_args = [ + _get_type_hint(arg, type_hint_globals, _is_optional(arg)) + for arg in value.__args__ + if arg is not type(None) + ] + res_args.sort() + if len(res_args) == 1: + return f"{res_args[0]} | None" + res = f"{' | '.join(res_args)}" + return f"{res} | None" + + res_args = [ + _get_type_hint(arg, type_hint_globals, _is_optional(arg)) + for arg in value.__args__ + ] + res_args.sort() + return f"{' | '.join(res_args)}" + + if args: + inner_container_type_args = ( + sorted(repr(arg) for arg in args) + if _is_literal(value) + else [ + _get_type_hint(arg, type_hint_globals, is_optional=False) + for arg in args + if arg is not type(None) + ] + ) + + if ( + value.__module__ not in ["builtins", "__builtins__"] + and value.__name__ not in type_hint_globals + ): + msg = ( + f"{value.__module__ + '.' + value.__name__} is not a default import, " + "add it to DEFAULT_IMPORTS in pyi_generator.py" + ) + raise TypeError(msg) + + res = f"{value.__name__}[{', '.join(inner_container_type_args)}]" + + if value.__name__ == "Var": + args = list( + chain.from_iterable([ + get_args(arg) if _is_union(arg) else [arg] for arg in args + ]) + ) + + # For Var types, Union with the inner args so they can be passed directly. + types = [res] + [ + _get_type_hint(arg, type_hint_globals, is_optional=False) + for arg in args + if arg is not type(None) + ] + if len(types) > 1: + res = " | ".join(sorted(types)) + + elif isinstance(value, str): + ev = eval(value, type_hint_globals) + if _is_optional(ev): + return _get_type_hint(ev, type_hint_globals, is_optional=False) + + if _is_union(ev): + res = [ + _get_type_hint(arg, type_hint_globals, _is_optional(arg)) + for arg in ev.__args__ + ] + return f"{' | '.join(res)}" + res = ( + _get_type_hint(ev, type_hint_globals, is_optional=False) + if ev.__name__ == "Var" + else value + ) + elif isinstance(value, list): + res = [ + _get_type_hint(arg, type_hint_globals, _is_optional(arg)) for arg in value + ] + return f"[{', '.join(res)}]" + else: + res = value.__name__ + if is_optional and not res.startswith("Optional") and not res.endswith("| None"): + res = f"{res} | None" + return res + + +@cache +def _get_source(obj: Any) -> str: + """Get and cache the source for a Python object. + + Args: + obj: The object whose source should be retrieved. + + Returns: + The source code for the object. + """ + return inspect.getsource(obj) + + +@cache +def _get_class_prop_comments(clz: type[Component]) -> Mapping[str, tuple[str, ...]]: + """Parse and cache prop comments for a component class. + + Args: + clz: The class to extract prop comments from. + + Returns: + An immutable mapping of prop name to comment lines. + """ + props_comments: dict[str, tuple[str, ...]] = {} + comments = [] + for line in _get_source(clz).splitlines(): + reached_functions = re.search(r"def ", line) + if reached_functions: + # We've reached the functions, so stop. + break + + if line == "": + # We hit a blank line, so clear comments to avoid commented out prop appearing in next prop docs. + comments.clear() + continue + + # Get comments for prop + if line.strip().startswith("#"): + # Remove noqa from the comments. + line = line.partition(" # noqa")[0] + comments.append(line) + continue + + # Check if this line has a prop. + match = re.search(r"\w+:", line) + if match is None: + # This line doesn't have a var, so continue. + continue + + # Get the prop. + prop = match.group(0).strip(":") + if comments: + props_comments[prop] = tuple( + comment.strip().strip("#") for comment in comments + ) + comments.clear() + + return MappingProxyType(props_comments) + + +@cache +def _get_full_argspec(func: Callable) -> inspect.FullArgSpec: + """Get and cache the full argspec for a callable. + + Args: + func: The callable to inspect. + + Returns: + The full argument specification. + """ + return getfullargspec(func) + + +@cache +def _get_signature_return_annotation(func: Callable) -> Any: + """Get and cache a callable's return annotation. + + Args: + func: The callable to inspect. + + Returns: + The callable's return annotation. + """ + return inspect.signature(func).return_annotation + + +@cache +def _get_module_star_imports(module_name: str) -> Mapping[str, Any]: + """Resolve names imported by `from module import *`. + + Args: + module_name: The module to inspect. + + Returns: + An immutable mapping of imported names to values. + """ + module = importlib.import_module(module_name) + exported_names = getattr(module, "__all__", None) + if exported_names is not None: + return MappingProxyType({ + name: getattr(module, name) for name in exported_names + }) + return MappingProxyType({ + name: value for name, value in vars(module).items() if not name.startswith("_") + }) + + +@cache +def _get_module_selected_imports( + module_name: str, imported_names: tuple[str, ...] +) -> Mapping[str, Any]: + """Resolve a set of imported names from a module. + + Args: + module_name: The module to import from. + imported_names: The names to resolve. + + Returns: + An immutable mapping of imported names to values. + """ + module = importlib.import_module(module_name) + return MappingProxyType({name: getattr(module, name) for name in imported_names}) + + +@cache +def _get_class_annotation_globals(target_class: type) -> Mapping[str, Any]: + """Get globals needed to resolve class annotations. + + Args: + target_class: The class whose annotation globals should be resolved. + + Returns: + An immutable mapping of globals for the class MRO. + """ + available_vars: dict[str, Any] = {} + for module_name in {cls.__module__ for cls in target_class.__mro__}: + available_vars.update(sys.modules[module_name].__dict__) + return MappingProxyType(available_vars) + + +@cache +def _get_class_event_triggers(target_class: type) -> frozenset[str]: + """Get and cache event trigger names for a class. + + Args: + target_class: The class to inspect. + + Returns: + The event trigger names defined on the class. + """ + return frozenset(target_class.get_event_triggers()) + + +def _generate_imports( + typing_imports: Iterable[str], +) -> list[ast.ImportFrom | ast.Import]: + """Generate the import statements for the stub file. + + Args: + typing_imports: The typing imports to include. + + Returns: + The list of import statements. + """ + return [ + *[ + ast.ImportFrom(module=name, names=[ast.alias(name=val) for val in values]) # pyright: ignore [reportCallIssue] + for name, values in DEFAULT_IMPORTS.items() + ], + ast.Import([ast.alias("reflex")]), + ] + + +def _generate_docstrings(clzs: list[type[Component]], props: list[str]) -> str: + """Generate the docstrings for the create method. + + Args: + clzs: The classes to generate docstrings for. + props: The props to generate docstrings for. + + Returns: + The docstring for the create method. + """ + props_comments = {} + for clz in clzs: + for prop, comment_lines in _get_class_prop_comments(clz).items(): + if prop in props: + props_comments[prop] = list(comment_lines) + clz = clzs[0] + new_docstring = [] + for line in (clz.create.__doc__ or "").splitlines(): + if "**" in line: + indent = line.split("**")[0] + new_docstring.extend([ + f"{indent}{n}:{' '.join(c)}" for n, c in props_comments.items() + ]) + new_docstring.append(line) + return "\n".join(new_docstring) + + +def _extract_func_kwargs_as_ast_nodes( + func: Callable, + type_hint_globals: dict[str, Any], +) -> list[tuple[ast.arg, ast.Constant | None]]: + """Get the kwargs already defined on the function. + + Args: + func: The function to extract kwargs from. + type_hint_globals: The globals to use to resolving a type hint str. + + Returns: + The list of kwargs as ast arg nodes. + """ + spec = _get_full_argspec(func) + kwargs = [] + + for kwarg in spec.kwonlyargs: + arg = ast.arg(arg=kwarg) + if kwarg in spec.annotations: + arg.annotation = ast.Name( + id=_get_type_hint(spec.annotations[kwarg], type_hint_globals) + ) + default = None + if spec.kwonlydefaults is not None and kwarg in spec.kwonlydefaults: + default = ast.Constant(value=spec.kwonlydefaults[kwarg]) + kwargs.append((arg, default)) + return kwargs + + +def _extract_class_props_as_ast_nodes( + func: Callable, + clzs: list[type], + type_hint_globals: dict[str, Any], + extract_real_default: bool = False, +) -> list[tuple[ast.arg, ast.Constant | None]]: + """Get the props defined on the class and all parents. + + Args: + func: The function that kwargs will be added to. + clzs: The classes to extract props from. + type_hint_globals: The globals to use to resolving a type hint str. + extract_real_default: Whether to extract the real default value from the + pydantic field definition. + + Returns: + The list of props as ast arg nodes + """ + spec = _get_full_argspec(func) + func_kwonlyargs = set(spec.kwonlyargs) + all_props: set[str] = set() + kwargs = [] + for target_class in clzs: + event_triggers = _get_class_event_triggers(target_class) + # Import from the target class to ensure type hints are resolvable. + type_hint_globals.update(_get_module_star_imports(target_class.__module__)) + annotation_globals = { + **type_hint_globals, + **_get_class_annotation_globals(target_class), + } + for name, value in target_class.__annotations__.items(): + if ( + name in func_kwonlyargs + or name in EXCLUDED_PROPS + or name in all_props + or name in event_triggers + or (isinstance(value, str) and "ClassVar" in value) + ): + continue + all_props.add(name) + + default = None + if extract_real_default: + # TODO: This is not currently working since the default is not type compatible + # with the annotation in some cases. + with contextlib.suppress(AttributeError, KeyError): + # Try to get default from pydantic field definition. + default = target_class.__fields__[name].default + if isinstance(default, Var): + default = default._decode() + + kwargs.append(( + ast.arg( + arg=name, + annotation=ast.Name( + id=OVERWRITE_TYPES.get( + name, + _get_type_hint( + value, + annotation_globals, + ), + ) + ), + ), + ast.Constant(value=default), # pyright: ignore [reportArgumentType] + )) + return kwargs + + +def _get_visible_type_name( + typ: Any, type_hint_globals: Mapping[str, Any] | None +) -> str | None: + """Get a visible identifier for a type in the current module. + + Args: + typ: The type annotation to resolve. + type_hint_globals: The globals visible in the current module. + + Returns: + The visible identifier if one exists, otherwise None. + """ + if type_hint_globals is None: + return None + + type_name = getattr(typ, "__name__", None) + if ( + type_name is not None + and type_name in type_hint_globals + and type_hint_globals[type_name] is typ + ): + return type_name + + for name, value in type_hint_globals.items(): + if name.isidentifier() and value is typ: + return name + + return None + + +def type_to_ast( + typ: Any, + cls: type, + type_hint_globals: Mapping[str, Any] | None = None, +) -> ast.expr: + """Converts any type annotation into its AST representation. + Handles nested generic types, unions, etc. + + Args: + typ: The type annotation to convert. + cls: The class where the type annotation is used. + type_hint_globals: The globals visible where the annotation is used. + + Returns: + The AST representation of the type annotation. + """ + if typ is type(None) or typ is None: + return ast.Name(id="None") + + origin = get_origin(typ) + if origin is typing.Literal: + return ast.Subscript( + value=ast.Name(id="Literal"), + slice=ast.Tuple( + elts=[ast.Constant(value=val) for val in get_args(typ)], ctx=ast.Load() + ), + ctx=ast.Load(), + ) + if origin is UnionType: + origin = typing.Union + + # Handle plain types (int, str, custom classes, etc.) + if origin is None: + if hasattr(typ, "__name__"): + if typ.__module__.startswith("reflex."): + typ_parts = typ.__module__.split(".") + cls_parts = cls.__module__.split(".") + + zipped = list(zip(typ_parts, cls_parts, strict=False)) + + if all(a == b for a, b in zipped) and len(typ_parts) == len(cls_parts): + return ast.Name(id=typ.__name__) + if visible_name := _get_visible_type_name(typ, type_hint_globals): + return ast.Name(id=visible_name) + if ( + typ.__module__ in DEFAULT_IMPORTS + and typ.__name__ in DEFAULT_IMPORTS[typ.__module__] + ): + return ast.Name(id=typ.__name__) + return ast.Name(id=typ.__module__ + "." + typ.__name__) + return ast.Name(id=typ.__name__) + if hasattr(typ, "_name"): + return ast.Name(id=typ._name) + return ast.Name(id=str(typ)) + + # Get the base type name (List, Dict, Optional, etc.) + base_name = getattr(origin, "_name", origin.__name__) + + # Get type arguments + args = get_args(typ) + + # Handle empty type arguments + if not args: + return ast.Name(id=base_name) + + # Convert all type arguments recursively + arg_nodes = [type_to_ast(arg, cls, type_hint_globals) for arg in args] + + # Special case for single-argument types (like list[T] or Optional[T]) + if len(arg_nodes) == 1: + slice_value = arg_nodes[0] + else: + slice_value = ast.Tuple(elts=arg_nodes, ctx=ast.Load()) + + return ast.Subscript( + value=ast.Name(id=base_name), + slice=slice_value, + ctx=ast.Load(), + ) + + +@cache +def _get_parent_imports(func: Callable) -> Mapping[str, tuple[str, ...]]: + """Get parent imports needed to resolve forwarded type hints. + + Args: + func: The callable whose annotations are being analyzed. + + Returns: + An immutable mapping of module names to imported symbol names. + """ + imports_: dict[str, set[str]] = {"reflex_core.vars": {"Var"}} + module_dir = set(dir(importlib.import_module(func.__module__))) + for type_hint in inspect.get_annotations(func).values(): + try: + match = re.match(r"\w+\[([\w\d]+)\]", type_hint) + except TypeError: + continue + if match: + type_hint = match.group(1) + if type_hint in module_dir: + imports_.setdefault(func.__module__, set()).add(type_hint) + return MappingProxyType({ + module_name: tuple(sorted(imported_names)) + for module_name, imported_names in imports_.items() + }) + + +def _generate_component_create_functiondef( + clz: type[Component], + type_hint_globals: dict[str, Any], + lineno: int, + decorator_list: Sequence[ast.expr] = (ast.Name(id="classmethod"),), +) -> ast.FunctionDef: + """Generate the create function definition for a Component. + + Args: + clz: The Component class to generate the create functiondef for. + type_hint_globals: The globals to use to resolving a type hint str. + lineno: The line number to use for the ast nodes. + decorator_list: The list of decorators to apply to the create functiondef. + + Returns: + The create functiondef node for the ast. + + Raises: + TypeError: If clz is not a subclass of Component. + """ + if not issubclass(clz, Component): + msg = f"clz must be a subclass of Component, not {clz!r}" + raise TypeError(msg) + + # add the imports needed by get_type_hint later + type_hint_globals.update({ + name: getattr(typing, name) for name in DEFAULT_TYPING_IMPORTS + }) + + if clz.__module__ != clz.create.__module__: + imports_ = _get_parent_imports(clz.create) + for name, values in imports_.items(): + type_hint_globals.update(_get_module_selected_imports(name, values)) + + kwargs = _extract_func_kwargs_as_ast_nodes(clz.create, type_hint_globals) + + # kwargs associated with props defined in the class and its parents + all_classes = [c for c in clz.__mro__ if issubclass(c, Component)] + prop_kwargs = _extract_class_props_as_ast_nodes( + clz.create, all_classes, type_hint_globals + ) + all_props = [arg[0].arg for arg in prop_kwargs] + kwargs.extend(prop_kwargs) + + def figure_out_return_type(annotation: Any): + if isinstance(annotation, type) and issubclass(annotation, inspect._empty): + return ast.Name(id="EventType[Any]") + + if not isinstance(annotation, str) and get_origin(annotation) is tuple: + arguments = get_args(annotation) + + arguments_without_var = [ + get_args(argument)[0] if get_origin(argument) == Var else argument + for argument in arguments + ] + + # Convert each argument type to its AST representation + type_args = [ + type_to_ast(arg, cls=clz, type_hint_globals=type_hint_globals) + for arg in arguments_without_var + ] + + # Get all prefixes of the type arguments + all_count_args_type = [ + ast.Name( + f"EventType[{', '.join([ast.unparse(arg) for arg in type_args[:i]])}]" + ) + if i > 0 + else ast.Name("EventType[()]") + for i in range(len(type_args) + 1) + ] + + # Create EventType using the joined string + return ast.Name(id=f"{' | '.join(map(ast.unparse, all_count_args_type))}") + + if isinstance(annotation, str) and annotation.lower().startswith("tuple["): + inside_of_tuple = ( + annotation + .removeprefix("tuple[") + .removeprefix("Tuple[") + .removesuffix("]") + ) + + if inside_of_tuple == "()": + return ast.Name(id="EventType[()]") + + arguments = [""] + + bracket_count = 0 + + for char in inside_of_tuple: + if char == "[": + bracket_count += 1 + elif char == "]": + bracket_count -= 1 + + if char == "," and bracket_count == 0: + arguments.append("") + else: + arguments[-1] += char + + arguments = [argument.strip() for argument in arguments] + + arguments_without_var = [ + argument.removeprefix("Var[").removesuffix("]") + if argument.startswith("Var[") + else argument + for argument in arguments + ] + + all_count_args_type = [ + ast.Name(f"EventType[{', '.join(arguments_without_var[:i])}]") + if i > 0 + else ast.Name("EventType[()]") + for i in range(len(arguments) + 1) + ] + + return ast.Name(id=f"{' | '.join(map(ast.unparse, all_count_args_type))}") + return ast.Name(id="EventType[Any]") + + event_triggers = clz.get_event_triggers() + + # event handler kwargs + kwargs.extend( + ( + ast.arg( + arg=trigger, + annotation=ast.Subscript( + ast.Name("Optional"), + ast.Name( + id=ast.unparse( + figure_out_return_type( + _get_signature_return_annotation(event_specs) + ) + if not isinstance( + event_specs := event_triggers[trigger], Sequence + ) + else ast.Subscript( + ast.Name("Union"), + ast.Tuple([ + figure_out_return_type( + _get_signature_return_annotation(event_spec) + ) + for event_spec in event_specs + ]), + ) + ) + ), + ), + ), + ast.Constant(value=None), + ) + for trigger in sorted(event_triggers) + ) + + logger.debug(f"Generated {clz.__name__}.create method with {len(kwargs)} kwargs") + create_args = ast.arguments( + args=[ast.arg(arg="cls")], + posonlyargs=[], + vararg=ast.arg(arg="children"), + kwonlyargs=[arg[0] for arg in kwargs], + kw_defaults=[arg[1] for arg in kwargs], + kwarg=ast.arg(arg="props"), + defaults=[], + ) + + return ast.FunctionDef( # pyright: ignore [reportCallIssue] + name="create", + args=create_args, + body=[ + ast.Expr( + value=ast.Constant( + value=_generate_docstrings( + all_classes, [*all_props, *event_triggers] + ) + ), + ), + ast.Expr( + value=ast.Constant(value=Ellipsis), + ), + ], + decorator_list=list(decorator_list), + lineno=lineno, + returns=ast.Constant(value=clz.__name__), + ) + + +def _generate_staticmethod_call_functiondef( + node: ast.ClassDef, + clz: type[Component] | type[SimpleNamespace], + type_hint_globals: dict[str, Any], +) -> ast.FunctionDef | None: + fullspec = _get_full_argspec(clz.__call__) + + call_args = ast.arguments( + args=[ + ast.arg( + name, + annotation=ast.Name( + id=_get_type_hint( + anno := fullspec.annotations[name], + type_hint_globals, + is_optional=_is_optional(anno), + ) + ), + ) + for name in fullspec.args + ], + posonlyargs=[], + kwonlyargs=[], + kw_defaults=[], + kwarg=ast.arg(arg="props"), + defaults=( + [ast.Constant(value=default) for default in fullspec.defaults] + if fullspec.defaults + else [] + ), + ) + return ast.FunctionDef( # pyright: ignore [reportCallIssue] + name="__call__", + args=call_args, + body=[ + ast.Expr(value=ast.Constant(value=clz.__call__.__doc__)), + ast.Expr( + value=ast.Constant(...), + ), + ], + decorator_list=[ast.Name(id="staticmethod")], + lineno=node.lineno, + returns=ast.Constant( + value=_get_type_hint( + typing.get_type_hints(clz.__call__).get("return", None), + type_hint_globals, + is_optional=False, + ) + ), + ) + + +def _generate_namespace_call_functiondef( + node: ast.ClassDef, + clz_name: str, + classes: dict[str, type[Component] | type[SimpleNamespace]], + type_hint_globals: dict[str, Any], +) -> ast.FunctionDef | None: + """Generate the __call__ function definition for a SimpleNamespace. + + Args: + node: The existing __call__ classdef parent node from the ast + clz_name: The name of the SimpleNamespace class to generate the __call__ functiondef for. + classes: Map name to actual class definition. + type_hint_globals: The globals to use to resolving a type hint str. + + Returns: + The create functiondef node for the ast. + """ + # add the imports needed by get_type_hint later + type_hint_globals.update({ + name: getattr(typing, name) for name in DEFAULT_TYPING_IMPORTS + }) + + clz = classes[clz_name] + + if not hasattr(clz.__call__, "__self__"): + return _generate_staticmethod_call_functiondef(node, clz, type_hint_globals) + + # Determine which class is wrapped by the namespace __call__ method + component_clz = clz.__call__.__self__ + + if clz.__call__.__func__.__name__ != "create": # pyright: ignore [reportFunctionMemberAccess] + return None + + if not issubclass(component_clz, Component): + return None + + definition = _generate_component_create_functiondef( + clz=component_clz, + type_hint_globals=type_hint_globals, + lineno=node.lineno, + decorator_list=[], + ) + definition.name = "__call__" + + # Turn the definition into a staticmethod + del definition.args.args[0] # remove `cls` arg + definition.decorator_list = [ast.Name(id="staticmethod")] + + return definition + + +class StubGenerator(ast.NodeTransformer): + """A node transformer that will generate the stubs for a given module.""" + + def __init__( + self, + module: ModuleType, + classes: dict[str, type[Component | SimpleNamespace]], + ): + """Initialize the stub generator. + + Args: + module: The actual module object module to generate stubs for. + classes: The actual Component class objects to generate stubs for. + """ + super().__init__() + # Dict mapping class name to actual class object. + self.classes = classes + # Track the last class node that was visited. + self.current_class = None + # These imports will be included in the AST of stub files. + self.typing_imports = DEFAULT_TYPING_IMPORTS.copy() + # Whether those typing imports have been inserted yet. + self.inserted_imports = False + # This dict is used when evaluating type hints. + self.type_hint_globals = module.__dict__.copy() + + @staticmethod + def _remove_docstring( + node: ast.Module | ast.ClassDef | ast.FunctionDef, + ) -> ast.Module | ast.ClassDef | ast.FunctionDef: + """Removes any docstring in place. + + Args: + node: The node to remove the docstring from. + + Returns: + The modified node. + """ + if ( + node.body + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Constant) + ): + node.body.pop(0) + return node + + def _current_class_is_component(self) -> type[Component] | None: + """Check if the current class is a Component. + + Returns: + Whether the current class is a Component. + """ + if ( + self.current_class is not None + and self.current_class in self.classes + and issubclass((clz := self.classes[self.current_class]), Component) + ): + return clz + return None + + def visit_Module(self, node: ast.Module) -> ast.Module: + """Visit a Module node and remove docstring from body. + + Args: + node: The Module node to visit. + + Returns: + The modified Module node. + """ + self.generic_visit(node) + return self._remove_docstring(node) # pyright: ignore [reportReturnType] + + def visit_Import( + self, node: ast.Import | ast.ImportFrom + ) -> ast.Import | ast.ImportFrom | list[ast.Import | ast.ImportFrom]: + """Collect import statements from the module. + + If this is the first import statement, insert the typing imports before it. + + Args: + node: The import node to visit. + + Returns: + The modified import node(s). + """ + if not self.inserted_imports: + self.inserted_imports = True + default_imports = _generate_imports(self.typing_imports) + return [*default_imports, node] + return node + + def visit_ImportFrom( + self, node: ast.ImportFrom + ) -> ast.Import | ast.ImportFrom | list[ast.Import | ast.ImportFrom] | None: + """Visit an ImportFrom node. + + Remove any `from __future__ import *` statements, and hand off to visit_Import. + + Args: + node: The ImportFrom node to visit. + + Returns: + The modified ImportFrom node. + """ + if node.module == "__future__": + return None # ignore __future__ imports: https://docs.astral.sh/ruff/rules/future-annotations-in-stub/ + return self.visit_Import(node) + + def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: + """Visit a ClassDef node. + + Remove all assignments in the class body, and add a create functiondef + if one does not exist. + + Args: + node: The ClassDef node to visit. + + Returns: + The modified ClassDef node. + """ + self.current_class = node.name + self._remove_docstring(node) + + # Define `__call__` as a real function so the docstring appears in the stub. + call_definition = None + for child in node.body[:]: + found_call = False + if ( + isinstance(child, ast.AnnAssign) + and isinstance(child.target, ast.Name) + and child.target.id.startswith("_") + ): + node.body.remove(child) + if isinstance(child, ast.Assign): + for target in child.targets[:]: + if isinstance(target, ast.Name) and target.id == "__call__": + child.targets.remove(target) + found_call = True + if not found_call: + continue + if not child.targets[:]: + node.body.remove(child) + call_definition = _generate_namespace_call_functiondef( + node, + self.current_class, + self.classes, + type_hint_globals=self.type_hint_globals, + ) + break + + self.generic_visit(node) # Visit child nodes. + + if ( + not any( + isinstance(child, ast.FunctionDef) and child.name == "create" + for child in node.body + ) + and (clz := self._current_class_is_component()) is not None + ): + # Add a new .create FunctionDef since one does not exist. + node.body.append( + _generate_component_create_functiondef( + clz=clz, + type_hint_globals=self.type_hint_globals, + lineno=node.lineno, + ) + ) + if call_definition is not None: + node.body.append(call_definition) + if not node.body: + # We should never return an empty body. + node.body.append(ast.Expr(value=ast.Constant(value=Ellipsis))) + self.current_class = None + return node + + def visit_FunctionDef(self, node: ast.FunctionDef) -> Any: + """Visit a FunctionDef node. + + Special handling for `.create` functions to add type hints for all props + defined on the component class. + + Remove all private functions and blank out the function body of the + remaining public functions. + + Args: + node: The FunctionDef node to visit. + + Returns: + The modified FunctionDef node (or None). + """ + if ( + node.name == "create" + and self.current_class in self.classes + and issubclass((clz := self.classes[self.current_class]), Component) + ): + node = _generate_component_create_functiondef( + clz=clz, + type_hint_globals=self.type_hint_globals, + lineno=node.lineno, + decorator_list=node.decorator_list, + ) + else: + if node.name.startswith("_") and node.name != "__call__": + return None # remove private methods + + if node.body[-1] != ast.Expr(value=ast.Constant(value=Ellipsis)): + # Blank out the function body for public functions. + node.body = [ast.Expr(value=ast.Constant(value=Ellipsis))] + return node + + def visit_Assign(self, node: ast.Assign) -> ast.Assign | None: + """Remove non-annotated assignment statements. + + Args: + node: The Assign node to visit. + + Returns: + The modified Assign node (or None). + """ + # Special case for assignments to `typing.Any` as fallback. + if ( + node.value is not None + and isinstance(node.value, ast.Name) + and node.value.id == "Any" + ): + return node + + if self._current_class_is_component(): + # Remove annotated assignments in Component classes (props) + return None + + # remove dunder method assignments for lazy_loader.attach + for target in node.targets: + if isinstance(target, ast.Tuple): + for name in target.elts: + if isinstance(name, ast.Name) and name.id.startswith("_"): + return None + + return node + + def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AnnAssign | None: + """Visit an AnnAssign node (Annotated assignment). + + Remove private target and remove the assignment value in the stub. + + Args: + node: The AnnAssign node to visit. + + Returns: + The modified AnnAssign node (or None). + """ + # skip ClassVars + if ( + isinstance(node.annotation, ast.Subscript) + and isinstance(node.annotation.value, ast.Name) + and node.annotation.value.id == "ClassVar" + ): + return node + if isinstance(node.target, ast.Name) and node.target.id.startswith("_"): + return None + if self._current_class_is_component(): + # Remove annotated assignments in Component classes (props) + return None + # Blank out assignments in type stubs. + node.value = None + return node + + +class InitStubGenerator(StubGenerator): + """A node transformer that will generate the stubs for a given init file.""" + + def visit_Import( + self, node: ast.Import | ast.ImportFrom + ) -> ast.Import | ast.ImportFrom | list[ast.Import | ast.ImportFrom]: + """Collect import statements from the init module. + + Args: + node: The import node to visit. + + Returns: + The modified import node(s). + """ + return [node] + + +def _path_to_module_name(path: Path) -> str: + """Convert a file path to a dotted module name. + + Args: + path: The file path to convert. + + Returns: + The dotted module name. + """ + return _relative_to_pwd(path).with_suffix("").as_posix().replace("/", ".") + + +def _write_pyi_file(module_path: Path, source: str) -> str: + relpath = str(_relative_to_pwd(module_path)).replace("\\", "/") + pyi_content = ( + "\n".join([ + f'"""Stub file for {relpath}"""', + "# ------------------- DO NOT EDIT ----------------------", + "# This file was generated by `reflex/utils/pyi_generator.py`!", + "# ------------------------------------------------------", + "", + ]) + + source + ) + + pyi_path = module_path.with_suffix(".pyi") + pyi_path.write_text(pyi_content) + logger.info(f"Wrote {relpath}") + return md5(pyi_content.encode()).hexdigest() + + +# Mapping from component subpackage name to its target Python package. +_COMPONENT_SUBPACKAGE_TARGETS: dict[str, str] = { + # reflex-components (base package) + "base": "reflex_components_core.base", + "core": "reflex_components_core.core", + "datadisplay": "reflex_components_core.datadisplay", + "el": "reflex_components_core.el", + "gridjs": "reflex_components_gridjs", + "lucide": "reflex_components_lucide", + "moment": "reflex_components_moment", + # Deep overrides (datadisplay split) + "datadisplay.code": "reflex_components_code.code", + "datadisplay.shiki_code_block": "reflex_components_code.shiki_code_block", + "datadisplay.dataeditor": "reflex_components_dataeditor.dataeditor", + # Standalone packages + "markdown": "reflex_components_markdown", + "plotly": "reflex_components_plotly", + "radix": "reflex_components_radix", + "react_player": "reflex_components_react_player", + "react_router": "reflex_components_core.react_router", + "recharts": "reflex_components_recharts", + "sonner": "reflex_components_sonner", +} + + +def _rewrite_component_import(module: str) -> str: + """Rewrite a lazy-loader module path to the correct absolute package import. + + Args: + module: The module path from ``_SUBMOD_ATTRS`` (e.g. ``"components.radix.themes.base"``). + + Returns: + An absolute import path (``"reflex_components_radix.themes.base"``) for moved + components, or a relative path (``".components.component"``) for everything else. + """ + if module == "components": + # "components": ["el", "radix", ...] — these are re-exported submodules. + # Can't map to a single package, but the pyi generator handles each attr individually. + return "reflex_components_core" + if module.startswith("components."): + rest = module[len("components.") :] + # Try progressively deeper matches (e.g. "datadisplay.code" before "datadisplay"). + parts = rest.split(".") + for depth in range(min(len(parts), 2), 0, -1): + key = ".".join(parts[:depth]) + target = _COMPONENT_SUBPACKAGE_TARGETS.get(key) + if target is not None: + remainder = ".".join(parts[depth:]) + return f"{target}.{remainder}" if remainder else target + return f".{module}" + + +def _get_init_lazy_imports(mod: tuple | ModuleType, new_tree: ast.AST): + # retrieve the _SUBMODULES and _SUBMOD_ATTRS from an init file if present. + sub_mods: set[str] | None = getattr(mod, "_SUBMODULES", None) + sub_mod_attrs: dict[str, list[str | tuple[str, str]]] | None = getattr( + mod, "_SUBMOD_ATTRS", None + ) + extra_mappings: dict[str, str] | None = getattr(mod, "_EXTRA_MAPPINGS", None) + + if not sub_mods and not sub_mod_attrs and not extra_mappings: + return None + sub_mods_imports = [] + sub_mod_attrs_imports = [] + extra_mappings_imports = [] + + if sub_mods: + sub_mods_imports = [f"from . import {mod}" for mod in sorted(sub_mods)] + sub_mods_imports.append("") + + if sub_mod_attrs: + flattened_sub_mod_attrs = { + imported: module + for module, attrs in sub_mod_attrs.items() + for imported in attrs + } + # construct the import statement and handle special cases for aliases + for imported, module in flattened_sub_mod_attrs.items(): + # For "components": ["el", "radix", ...], resolve each attr to its package. + if ( + module == "components" + and isinstance(imported, str) + and imported in _COMPONENT_SUBPACKAGE_TARGETS + ): + target = _COMPONENT_SUBPACKAGE_TARGETS[imported] + sub_mod_attrs_imports.append(f"import {target} as {imported}") + continue + + rewritten = _rewrite_component_import(module) + if isinstance(imported, tuple): + suffix = ( + (imported[0] + " as " + imported[1]) + if imported[0] != imported[1] + else imported[0] + ) + else: + suffix = imported + sub_mod_attrs_imports.append(f"from {rewritten} import {suffix}") + sub_mod_attrs_imports.append("") + + if extra_mappings: + for alias, import_path in extra_mappings.items(): + if "." not in import_path: + # Handle simple module imports (e.g. "import reflex_components_markdown as markdown"). + extra_mappings_imports.append(f"import {import_path} as {alias}") + continue + module_name, import_name = import_path.rsplit(".", 1) + extra_mappings_imports.append( + f"from {module_name} import {import_name} as {alias}" + ) + + text = ( + "\n" + + "\n".join([ + *sub_mods_imports, + *sub_mod_attrs_imports, + *extra_mappings_imports, + ]) + + "\n" + ) + text += ast.unparse(new_tree) + "\n\n" + text += f"__all__ = {getattr(mod, '__all__', [])!r}\n" + return text + + +def _scan_file(module_path: Path) -> tuple[str, str] | None: + """Process a single Python file and generate its .pyi stub. + + Args: + module_path: Path to the Python source file. + + Returns: + Tuple of (pyi_path, content_hash) or None if no stub needed. + """ + module_import = _path_to_module_name(module_path) + module = importlib.import_module(module_import) + logger.debug(f"Read {module_path}") + class_names = { + name: obj + for name, obj in vars(module).items() + if isinstance(obj, type) + and (_safe_issubclass(obj, Component) or _safe_issubclass(obj, SimpleNamespace)) + and obj != Component + and inspect.getmodule(obj) == module + } + is_init_file = _relative_to_pwd(module_path).name == "__init__.py" + if not class_names and not is_init_file: + return None + + if is_init_file: + new_tree = InitStubGenerator(module, class_names).visit( + ast.parse(_get_source(module)) + ) + init_imports = _get_init_lazy_imports(module, new_tree) + if not init_imports: + return None + content_hash = _write_pyi_file(module_path, init_imports) + else: + new_tree = StubGenerator(module, class_names).visit( + ast.parse(_get_source(module)) + ) + content_hash = _write_pyi_file(module_path, ast.unparse(new_tree)) + return str(module_path.with_suffix(".pyi").resolve()), content_hash + + +class PyiGenerator: + """A .pyi file generator that will scan all defined Component in Reflex and + generate the appropriate stub. + """ + + modules: list = [] + root: str = "" + current_module: Any = {} + written_files: list[tuple[str, str]] = [] + + def _scan_files(self, files: list[Path]): + max_workers = min(multiprocessing.cpu_count() or 1, len(files), 8) + use_parallel = ( + max_workers > 1 and "fork" in multiprocessing.get_all_start_methods() + ) + + if not use_parallel: + # Serial fallback: _scan_file handles its own imports. + for file in files: + result = _scan_file(file) + if result is not None: + self.written_files.append(result) + return + + # Pre-import all modules sequentially to populate sys.modules + # so forked workers inherit the cache and skip redundant imports. + importable_files: list[Path] = [] + for file in files: + module_import = _path_to_module_name(file) + try: + importlib.import_module(module_import) + importable_files.append(file) + except Exception: + logger.exception(f"Failed to import {module_import}") + + # Generate stubs in parallel using forked worker processes. + ctx = multiprocessing.get_context("fork") + with ProcessPoolExecutor(max_workers=max_workers, mp_context=ctx) as executor: + self.written_files.extend( + r for r in executor.map(_scan_file, importable_files) if r is not None + ) + + def scan_all( + self, + targets: list, + changed_files: list[Path] | None = None, + use_json: bool = False, + ): + """Scan all targets for class inheriting Component and generate the .pyi files. + + Args: + targets: the list of file/folders to scan. + changed_files (optional): the list of changed files since the last run. + use_json: whether to use json to store the hashes. + """ + file_targets = [] + for target in targets: + target_path = Path(target) + if ( + target_path.is_file() + and target_path.suffix == ".py" + and target_path.name not in EXCLUDED_FILES + ): + file_targets.append(target_path) + continue + if not target_path.is_dir(): + continue + for file_path in _walk_files(target_path): + relative = _relative_to_pwd(file_path) + if relative.name in EXCLUDED_FILES or file_path.suffix != ".py": + continue + if ( + changed_files is not None + and _relative_to_pwd(file_path) not in changed_files + ): + continue + file_targets.append(file_path) + + # check if pyi changed but not the source + if changed_files is not None: + for changed_file in changed_files: + if changed_file.suffix != ".pyi": + continue + py_file_path = changed_file.with_suffix(".py") + if not py_file_path.exists() and changed_file.exists(): + changed_file.unlink() + if py_file_path in file_targets: + continue + subprocess.run(["git", "checkout", changed_file]) + + self._scan_files(file_targets) + + file_paths, hashes = ( + [f[0] for f in self.written_files], + [f[1] for f in self.written_files], + ) + + # Fix generated pyi files with ruff. + if file_paths: + subprocess.run(["ruff", "check", "--fix", *file_paths]) + subprocess.run(["ruff", "format", *file_paths]) + + if use_json: + if file_paths and changed_files is None: + file_paths = list(map(Path, file_paths)) + top_dir = file_paths[0].parent + for file_path in file_paths: + file_parent = file_path.parent + while len(file_parent.parts) > len(top_dir.parts): + file_parent = file_parent.parent + while len(top_dir.parts) > len(file_parent.parts): + top_dir = top_dir.parent + while not file_parent.samefile(top_dir): + file_parent = file_parent.parent + top_dir = top_dir.parent + + while ( + not top_dir.samefile(top_dir.parent) + and not (top_dir / PYI_HASHES).exists() + ): + top_dir = top_dir.parent + + pyi_hashes_file = top_dir / PYI_HASHES + + if pyi_hashes_file.exists(): + pyi_hashes_file.write_text( + json.dumps( + dict( + zip( + [ + f.relative_to(pyi_hashes_file.parent).as_posix() + for f in file_paths + ], + hashes, + strict=True, + ) + ), + indent=2, + sort_keys=True, + ) + + "\n", + ) + elif file_paths: + file_paths = list(map(Path, file_paths)) + pyi_hashes_parent = file_paths[0].parent + while ( + not pyi_hashes_parent.samefile(pyi_hashes_parent.parent) + and not (pyi_hashes_parent / PYI_HASHES).exists() + ): + pyi_hashes_parent = pyi_hashes_parent.parent + + pyi_hashes_file = pyi_hashes_parent / PYI_HASHES + if pyi_hashes_file.exists(): + pyi_hashes = json.loads(pyi_hashes_file.read_text()) + for file_path, hashed_content in zip( + file_paths, hashes, strict=False + ): + formatted_path = file_path.relative_to( + pyi_hashes_parent + ).as_posix() + pyi_hashes[formatted_path] = hashed_content + + pyi_hashes_file.write_text( + json.dumps(pyi_hashes, indent=2, sort_keys=True) + "\n" + ) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Generate .pyi stub files") + parser.add_argument( + "targets", + nargs="*", + default=["reflex/components", "reflex/experimental", "reflex/__init__.py"], + help="Target directories/files to process", + ) + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + logging.getLogger("blib2to3.pgen2.driver").setLevel(logging.INFO) + + gen = PyiGenerator() + gen.scan_all(args.targets, None, use_json=True) diff --git a/packages/reflex-core/src/reflex_core/utils/serializers.py b/packages/reflex-core/src/reflex_core/utils/serializers.py new file mode 100644 index 00000000000..49b3233c838 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/serializers.py @@ -0,0 +1,498 @@ +"""Serializers used to convert Var types to JSON strings.""" + +from __future__ import annotations + +import contextlib +import dataclasses +import decimal +import functools +import inspect +import json +import warnings +from collections.abc import Callable, Mapping, Sequence +from datetime import date, datetime, time, timedelta +from enum import Enum +from importlib.util import find_spec +from pathlib import Path +from typing import Any, Literal, TypeVar, get_type_hints, overload +from uuid import UUID + +from reflex_core.constants.colors import Color +from reflex_core.utils import console, types + +# Mapping from type to a serializer. +# The serializer should convert the type to a JSON object. +SerializedType = str | bool | int | float | list | dict | None + + +Serializer = Callable[[Any], SerializedType] + + +SERIALIZERS: dict[type, Serializer] = {} +SERIALIZER_TYPES: dict[type, type] = {} + +SERIALIZED_FUNCTION = TypeVar("SERIALIZED_FUNCTION", bound=Serializer) + + +@overload +def serializer( + fn: None = None, + to: type[SerializedType] | None = None, + overwrite: bool | None = None, +) -> Callable[[SERIALIZED_FUNCTION], SERIALIZED_FUNCTION]: ... + + +@overload +def serializer( + fn: SERIALIZED_FUNCTION, + to: type[SerializedType] | None = None, + overwrite: bool | None = None, +) -> SERIALIZED_FUNCTION: ... + + +def serializer( + fn: SERIALIZED_FUNCTION | None = None, + to: Any = None, + overwrite: bool | None = None, +) -> SERIALIZED_FUNCTION | Callable[[SERIALIZED_FUNCTION], SERIALIZED_FUNCTION]: + """Decorator to add a serializer for a given type. + + Args: + fn: The function to decorate. + to: The type returned by the serializer. If this is `str`, then any Var created from this type will be treated as a string. + overwrite: Whether to overwrite the existing serializer. + + Returns: + The decorated function. + """ + + def wrapper(fn: SERIALIZED_FUNCTION) -> SERIALIZED_FUNCTION: + # Check the type hints to get the type of the argument. + type_hints = get_type_hints(fn) + args = [arg for arg in type_hints if arg != "return"] + + # Make sure the function takes a single argument. + if len(args) != 1: + msg = "Serializer must take a single argument." + raise ValueError(msg) + + # Get the type of the argument. + type_ = type_hints[args[0]] + + # Make sure the type is not already registered. + registered_fn = SERIALIZERS.get(type_) + if registered_fn is not None and registered_fn != fn and overwrite is not True: + message = f"Overwriting serializer for type {type_} from {registered_fn.__module__}:{registered_fn.__qualname__} to {fn.__module__}:{fn.__qualname__}." + if overwrite is False: + raise ValueError(message) + caller_frame = next( + filter( + lambda frame: frame.filename != __file__, + inspect.getouterframes(inspect.currentframe()), + ), + None, + ) + file_info = ( + f"(at {caller_frame.filename}:{caller_frame.lineno})" + if caller_frame + else "" + ) + console.warn( + f"{message} Call rx.serializer with `overwrite=True` if this is intentional. {file_info}" + ) + + to_type = to or type_hints.get("return") + + # Apply type transformation if requested + if to_type: + SERIALIZER_TYPES[type_] = to_type + get_serializer_type.cache_clear() + + # Register the serializer. + SERIALIZERS[type_] = fn + get_serializer.cache_clear() + + # Return the function. + return fn + + if fn is not None: + return wrapper(fn) + return wrapper + + +@overload +def serialize( + value: Any, get_type: Literal[True] +) -> tuple[SerializedType | None, types.GenericType | None]: ... + + +@overload +def serialize(value: Any, get_type: Literal[False]) -> SerializedType | None: ... + + +@overload +def serialize(value: Any) -> SerializedType | None: ... + + +def serialize( + value: Any, get_type: bool = False +) -> SerializedType | tuple[SerializedType | None, types.GenericType | None] | None: + """Serialize the value to a JSON string. + + Args: + value: The value to serialize. + get_type: Whether to return the type of the serialized value. + + Returns: + The serialized value, or None if a serializer is not found. + """ + # Get the serializer for the type. + serializer = get_serializer(type(value)) + + # If there is no serializer, return None. + if serializer is None: + if dataclasses.is_dataclass(value) and not isinstance(value, type): + return {k.name: getattr(value, k.name) for k in dataclasses.fields(value)} + + if get_type: + return None, None + return None + + # Serialize the value. + serialized = serializer(value) + + # Return the serialized value and the type. + if get_type: + return serialized, get_serializer_type(type(value)) + return serialized + + +@functools.lru_cache +def get_serializer(type_: type) -> Serializer | None: + """Get the serializer for the type. + + Args: + type_: The type to get the serializer for. + + Returns: + The serializer for the type, or None if there is no serializer. + """ + # First, check if the type is registered. + serializer = SERIALIZERS.get(type_) + if serializer is not None: + return serializer + + # If the type is not registered, check if it is a subclass of a registered type. + for registered_type, serializer in reversed(SERIALIZERS.items()): + if issubclass(type_, registered_type): + return serializer + + # If there is no serializer, return None. + return None + + +@functools.lru_cache +def get_serializer_type(type_: type) -> type | None: + """Get the converted type for the type after serializing. + + Args: + type_: The type to get the serializer type for. + + Returns: + The serialized type for the type, or None if there is no type conversion registered. + """ + # First, check if the type is registered. + serializer = SERIALIZER_TYPES.get(type_) + if serializer is not None: + return serializer + + # If the type is not registered, check if it is a subclass of a registered type. + for registered_type, serializer in reversed(SERIALIZER_TYPES.items()): + if issubclass(type_, registered_type): + return serializer + + # If there is no serializer, return None. + return None + + +def has_serializer(type_: type, into_type: type | None = None) -> bool: + """Check if there is a serializer for the type. + + Args: + type_: The type to check. + into_type: The type to serialize into. + + Returns: + Whether there is a serializer for the type. + """ + serializer_for_type = get_serializer(type_) + return serializer_for_type is not None and ( + into_type is None or get_serializer_type(type_) == into_type + ) + + +def can_serialize(type_: type, into_type: type | None = None) -> bool: + """Check if there is a serializer for the type. + + Args: + type_: The type to check. + into_type: The type to serialize into. + + Returns: + Whether there is a serializer for the type. + """ + return ( + isinstance(type_, type) + and dataclasses.is_dataclass(type_) + and (into_type is None or into_type is dict) + ) or has_serializer(type_, into_type) + + +@serializer(to=str) +def serialize_type(value: type) -> str: + """Serialize a python type. + + Args: + value: the type to serialize. + + Returns: + The serialized type. + """ + return value.__name__ + + +if find_spec("pydantic"): + from pydantic import BaseModel + + @serializer(to=dict) + def serialize_base_model(model: BaseModel) -> dict: + """Serialize a pydantic v2 BaseModel instance. + + Args: + model: The BaseModel to serialize. + + Returns: + The serialized BaseModel. + """ + return model.model_dump() + + +@serializer +def serialize_set(value: set) -> list: + """Serialize a set to a JSON serializable list. + + Args: + value: The set to serialize. + + Returns: + The serialized list. + """ + return list(value) + + +@serializer +def serialize_sequence(value: Sequence) -> list: + """Serialize a sequence to a JSON serializable list. + + Args: + value: The sequence to serialize. + + Returns: + The serialized list. + """ + return list(value) + + +@serializer(to=dict) +def serialize_mapping(value: Mapping) -> dict: + """Serialize a mapping type to a dictionary. + + Args: + value: The mapping instance to serialize. + + Returns: + A new dictionary containing the same key-value pairs as the input mapping. + """ + return {**value} + + +@serializer(to=str) +def serialize_datetime(dt: date | datetime | time | timedelta) -> str: + """Serialize a datetime to a JSON string. + + Args: + dt: The datetime to serialize. + + Returns: + The serialized datetime. + """ + return str(dt) + + +@serializer(to=str) +def serialize_path(path: Path) -> str: + """Serialize a pathlib.Path to a JSON string. + + Args: + path: The path to serialize. + + Returns: + The serialized path. + """ + return str(path.as_posix()) + + +@serializer +def serialize_enum(en: Enum) -> str: + """Serialize a enum to a JSON string. + + Args: + en: The enum to serialize. + + Returns: + The serialized enum. + """ + return en.value + + +@serializer(to=str) +def serialize_uuid(uuid: UUID) -> str: + """Serialize a UUID to a JSON string. + + Args: + uuid: The UUID to serialize. + + Returns: + The serialized UUID. + """ + return str(uuid) + + +@serializer(to=float) +def serialize_decimal(value: decimal.Decimal) -> float: + """Serialize a Decimal to a float. + + Args: + value: The Decimal to serialize. + + Returns: + The serialized Decimal as a float. + """ + return float(value) + + +@serializer(to=str) +def serialize_color(color: Color) -> str: + """Serialize a color. + + Args: + color: The color to serialize. + + Returns: + The serialized color. + """ + return color.__format__("") + + +with contextlib.suppress(ImportError): + from pandas import DataFrame + + def format_dataframe_values(df: DataFrame) -> list[list[Any]]: + """Format dataframe values to a list of lists. + + Args: + df: The dataframe to format. + + Returns: + The dataframe as a list of lists. + """ + return [ + [str(d) if isinstance(d, (list, tuple)) else d for d in data] + for data in list(df.to_numpy().tolist()) + ] + + @serializer + def serialize_dataframe(df: DataFrame) -> dict: + """Serialize a pandas dataframe. + + Args: + df: The dataframe to serialize. + + Returns: + The serialized dataframe. + """ + return { + "columns": df.columns.tolist(), + "data": format_dataframe_values(df), + } + + +with contextlib.suppress(ImportError): + from plotly.graph_objects import Figure, layout + from plotly.io import to_json + + @serializer + def serialize_figure(figure: Figure) -> dict: + """Serialize a plotly figure. + + Args: + figure: The figure to serialize. + + Returns: + The serialized figure. + """ + return json.loads(str(to_json(figure))) + + @serializer + def serialize_template(template: layout.Template) -> dict: + """Serialize a plotly template. + + Args: + template: The template to serialize. + + Returns: + The serialized template. + """ + return { + "data": json.loads(str(to_json(template.data))), + "layout": json.loads(str(to_json(template.layout))), + } + + +with contextlib.suppress(ImportError): + import base64 + import io + + from PIL.Image import MIME + from PIL.Image import Image as Img + + @serializer + def serialize_image(image: Img) -> str: + """Serialize a plotly figure. + + Args: + image: The image to serialize. + + Returns: + The serialized image. + """ + buff = io.BytesIO() + image_format = getattr(image, "format", None) or "PNG" + image.save(buff, format=image_format) + image_bytes = buff.getvalue() + base64_image = base64.b64encode(image_bytes).decode("utf-8") + try: + # Newer method to get the mime type, but does not always work. + mime_type = image.get_format_mimetype() # pyright: ignore [reportAttributeAccessIssue] + except AttributeError: + try: + # Fallback method + mime_type = MIME[image_format] + except KeyError: + # Unknown mime_type: warn and return image/png and hope the browser can sort it out. + warnings.warn( # noqa: B028 + f"Unknown mime type for {image} {image_format}. Defaulting to image/png" + ) + mime_type = "image/png" + + return f"data:{mime_type};base64,{base64_image}" diff --git a/packages/reflex-core/src/reflex_core/utils/types.py b/packages/reflex-core/src/reflex_core/utils/types.py new file mode 100644 index 00000000000..65ee7581801 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/utils/types.py @@ -0,0 +1,1197 @@ +"""Contains custom types and methods to check types.""" + +from __future__ import annotations + +import dataclasses +import sys +import types +from collections.abc import Callable, Iterable, Mapping, Sequence +from enum import Enum +from functools import cached_property, lru_cache +from importlib.util import find_spec +from types import GenericAlias +from typing import ( # noqa: UP035 + TYPE_CHECKING, + Any, + Awaitable, + ClassVar, + Dict, + ForwardRef, + List, + Literal, + MutableMapping, + NoReturn, + Protocol, + Tuple, + TypeVar, + Union, + _eval_type, # pyright: ignore [reportAttributeAccessIssue] + _GenericAlias, # pyright: ignore [reportAttributeAccessIssue] + _SpecialGenericAlias, # pyright: ignore [reportAttributeAccessIssue] + get_args, + is_typeddict, +) +from typing import get_origin as get_origin_og +from typing import get_type_hints as get_type_hints_og + +from typing_extensions import Self as Self +from typing_extensions import override as override + +from reflex_core import constants +from reflex_core.utils import console + +# Potential GenericAlias types for isinstance checks. +GenericAliasTypes = (_GenericAlias, GenericAlias, _SpecialGenericAlias) + +# Potential Union types for isinstance checks. +UnionTypes = (Union, types.UnionType) + +# Union of generic types. +GenericType = type | _GenericAlias + +# Valid state var types. +PrimitiveTypes = (int, float, bool, str, list, dict, set, tuple) +StateVarTypes = (*PrimitiveTypes, type(None)) + +if TYPE_CHECKING: + from reflex.state import BaseState + from reflex_core.vars.base import Var + +VAR1 = TypeVar("VAR1", bound="Var") +VAR2 = TypeVar("VAR2", bound="Var") +VAR3 = TypeVar("VAR3", bound="Var") +VAR4 = TypeVar("VAR4", bound="Var") +VAR5 = TypeVar("VAR5", bound="Var") +VAR6 = TypeVar("VAR6", bound="Var") +VAR7 = TypeVar("VAR7", bound="Var") + + +class _ArgsSpec0(Protocol): + def __call__(self) -> Sequence[Var]: ... + + +class _ArgsSpec1(Protocol): + def __call__(self, var1: VAR1, /) -> Sequence[Var]: ... # pyright: ignore [reportInvalidTypeVarUse] + + +class _ArgsSpec2(Protocol): + def __call__(self, var1: VAR1, var2: VAR2, /) -> Sequence[Var]: ... # pyright: ignore [reportInvalidTypeVarUse] + + +class _ArgsSpec3(Protocol): + def __call__(self, var1: VAR1, var2: VAR2, var3: VAR3, /) -> Sequence[Var]: ... # pyright: ignore [reportInvalidTypeVarUse] + + +class _ArgsSpec4(Protocol): + def __call__( + self, + var1: VAR1, # pyright: ignore [reportInvalidTypeVarUse] + var2: VAR2, # pyright: ignore [reportInvalidTypeVarUse] + var3: VAR3, # pyright: ignore [reportInvalidTypeVarUse] + var4: VAR4, # pyright: ignore [reportInvalidTypeVarUse] + /, + ) -> Sequence[Var]: ... + + +class _ArgsSpec5(Protocol): + def __call__( + self, + var1: VAR1, # pyright: ignore [reportInvalidTypeVarUse] + var2: VAR2, # pyright: ignore [reportInvalidTypeVarUse] + var3: VAR3, # pyright: ignore [reportInvalidTypeVarUse] + var4: VAR4, # pyright: ignore [reportInvalidTypeVarUse] + var5: VAR5, # pyright: ignore [reportInvalidTypeVarUse] + /, + ) -> Sequence[Var]: ... + + +class _ArgsSpec6(Protocol): + def __call__( + self, + var1: VAR1, # pyright: ignore [reportInvalidTypeVarUse] + var2: VAR2, # pyright: ignore [reportInvalidTypeVarUse] + var3: VAR3, # pyright: ignore [reportInvalidTypeVarUse] + var4: VAR4, # pyright: ignore [reportInvalidTypeVarUse] + var5: VAR5, # pyright: ignore [reportInvalidTypeVarUse] + var6: VAR6, # pyright: ignore [reportInvalidTypeVarUse] + /, + ) -> Sequence[Var]: ... + + +class _ArgsSpec7(Protocol): + def __call__( + self, + var1: VAR1, # pyright: ignore [reportInvalidTypeVarUse] + var2: VAR2, # pyright: ignore [reportInvalidTypeVarUse] + var3: VAR3, # pyright: ignore [reportInvalidTypeVarUse] + var4: VAR4, # pyright: ignore [reportInvalidTypeVarUse] + var5: VAR5, # pyright: ignore [reportInvalidTypeVarUse] + var6: VAR6, # pyright: ignore [reportInvalidTypeVarUse] + var7: VAR7, # pyright: ignore [reportInvalidTypeVarUse] + /, + ) -> Sequence[Var]: ... + + +ArgsSpec = ( + _ArgsSpec0 + | _ArgsSpec1 + | _ArgsSpec2 + | _ArgsSpec3 + | _ArgsSpec4 + | _ArgsSpec5 + | _ArgsSpec6 + | _ArgsSpec7 +) + +Scope = MutableMapping[str, Any] +Message = MutableMapping[str, Any] + +Receive = Callable[[], Awaitable[Message]] +Send = Callable[[Message], Awaitable[None]] + +ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]] + +PrimitiveToAnnotation = { + list: List, # noqa: UP006 + tuple: Tuple, # noqa: UP006 + dict: Dict, # noqa: UP006 +} + +RESERVED_BACKEND_VAR_NAMES = {"_abc_impl", "_backend_vars", "_was_touched", "_mixin"} + + +class Unset: + """A class to represent an unset value. + + This is used to differentiate between a value that is not set and a value that is set to None. + """ + + def __repr__(self) -> str: + """Return the string representation of the class. + + Returns: + The string representation of the class. + """ + return "Unset" + + def __bool__(self) -> bool: + """Return False when the class is used in a boolean context. + + Returns: + False + """ + return False + + +@lru_cache +def _get_origin_cached(tp: Any): + return get_origin_og(tp) + + +def get_origin(tp: Any): + """Get the origin of a class. + + Args: + tp: The class to get the origin of. + + Returns: + The origin of the class. + """ + return ( + origin + if (origin := getattr(tp, "__origin__", None)) is not None + else _get_origin_cached(tp) + ) + + +@lru_cache +def is_generic_alias(cls: GenericType) -> bool: + """Check whether the class is a generic alias. + + Args: + cls: The class to check. + + Returns: + Whether the class is a generic alias. + """ + return isinstance(cls, GenericAliasTypes) + + +@lru_cache +def get_type_hints(obj: Any) -> dict[str, Any]: + """Get the type hints of a class. + + Args: + obj: The class to get the type hints of. + + Returns: + The type hints of the class. + """ + return get_type_hints_og(obj) + + +def _unionize(args: list[GenericType]) -> GenericType: + if not args: + return Any # pyright: ignore [reportReturnType] + if len(args) == 1: + return args[0] + return Union[tuple(args)] # noqa: UP007 + + +def unionize(*args: GenericType) -> type: + """Unionize the types. + + Args: + args: The types to unionize. + + Returns: + The unionized types. + """ + return _unionize([arg for arg in args if arg is not NoReturn]) + + +def is_none(cls: GenericType) -> bool: + """Check if a class is None. + + Args: + cls: The class to check. + + Returns: + Whether the class is None. + """ + return cls is type(None) or cls is None + + +def is_union(cls: GenericType) -> bool: + """Check if a class is a Union. + + Args: + cls: The class to check. + + Returns: + Whether the class is a Union. + """ + origin = getattr(cls, "__origin__", None) + if origin is Union: + return True + return origin is None and isinstance(cls, types.UnionType) + + +def is_literal(cls: GenericType) -> bool: + """Check if a class is a Literal. + + Args: + cls: The class to check. + + Returns: + Whether the class is a literal. + """ + return getattr(cls, "__origin__", None) is Literal + + +@lru_cache +def has_args(cls: type) -> bool: + """Check if the class has generic parameters. + + Args: + cls: The class to check. + + Returns: + Whether the class has generic + """ + if get_args(cls): + return True + + # Check if the class inherits from a generic class (using __orig_bases__) + if hasattr(cls, "__orig_bases__"): + for base in cls.__orig_bases__: + if get_args(base): + return True + + return False + + +def is_optional(cls: GenericType) -> bool: + """Check if a class is an Optional. + + Args: + cls: The class to check. + + Returns: + Whether the class is an Optional. + """ + return ( + cls is None + or cls is type(None) + or (is_union(cls) and type(None) in get_args(cls)) + ) + + +def is_classvar(a_type: Any) -> bool: + """Check if a type is a ClassVar. + + Args: + a_type: The type to check. + + Returns: + Whether the type is a ClassVar. + """ + return ( + a_type is ClassVar + or (type(a_type) is _GenericAlias and a_type.__origin__ is ClassVar) + or ( + type(a_type) is ForwardRef and a_type.__forward_arg__.startswith("ClassVar") + ) + ) + + +def value_inside_optional(cls: GenericType) -> GenericType: + """Get the value inside an Optional type or the original type. + + Args: + cls: The class to check. + + Returns: + The value inside the Optional type or the original type. + """ + if is_union(cls) and len(args := get_args(cls)) >= 2 and type(None) in args: + if len(args) == 2: + return args[0] if args[1] is type(None) else args[1] + return unionize(*[arg for arg in args if arg is not type(None)]) + return cls + + +def get_field_type(cls: GenericType, field_name: str) -> GenericType | None: + """Get the type of a field in a class. + + Args: + cls: The class to check. + field_name: The name of the field to check. + + Returns: + The type of the field, if it exists, else None. + """ + if (fields := getattr(cls, "_fields", None)) is not None and field_name in fields: + return fields[field_name].annotated_type + if ( + hasattr(cls, "__fields__") + and field_name in cls.__fields__ + and hasattr(cls.__fields__[field_name], "annotation") + and not isinstance(cls.__fields__[field_name].annotation, (str, ForwardRef)) + ): + return cls.__fields__[field_name].annotation + type_hints = get_type_hints(cls) + return type_hints.get(field_name, None) + + +PROPERTY_CLASSES = (property,) +if find_spec("sqlalchemy") and find_spec("sqlalchemy.ext"): + from sqlalchemy.ext.hybrid import hybrid_property + + PROPERTY_CLASSES += (hybrid_property,) + + +def get_property_hint(attr: Any | None) -> GenericType | None: + """Check if an attribute is a property and return its type hint. + + Args: + attr: The descriptor to check. + + Returns: + The type hint of the property, if it is a property, else None. + """ + if not isinstance(attr, PROPERTY_CLASSES): + return None + hints = get_type_hints(attr.fget) + return hints.get("return", None) + + +def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None: + """Check if an attribute can be accessed on the cls and return its type. + + Supports pydantic models, unions, and annotated attributes on rx.Model. + + Args: + cls: The class to check. + name: The name of the attribute to check. + + Returns: + The type of the attribute, if accessible, or None + """ + try: + attr = getattr(cls, name, None) + except NotImplementedError: + attr = None + + if hint := get_property_hint(attr): + return hint + + if hasattr(cls, "__fields__") and name in cls.__fields__: + # pydantic models + return get_field_type(cls, name) + if find_spec("sqlalchemy") and find_spec("sqlalchemy.orm"): + import sqlalchemy + from sqlalchemy.ext.associationproxy import AssociationProxyInstance + from sqlalchemy.orm import ( + DeclarativeBase, + Mapped, + QueryableAttribute, + Relationship, + ) + + from reflex.model import Model + + if find_spec("sqlmodel"): + from sqlmodel import SQLModel + + sqlmodel_types = (Model, SQLModel) + else: + sqlmodel_types = (Model,) + + if isinstance(cls, type) and issubclass(cls, DeclarativeBase): + insp = sqlalchemy.inspect(cls) + if name in insp.columns: + # check for list types + column = insp.columns[name] + column_type = column.type + try: + type_ = insp.columns[name].type.python_type + except NotImplementedError: + type_ = None + if type_ is not None: + if hasattr(column_type, "item_type"): + try: + item_type = column_type.item_type.python_type # pyright: ignore [reportAttributeAccessIssue] + except NotImplementedError: + item_type = None + if item_type is not None: + if type_ in PrimitiveToAnnotation: + type_ = PrimitiveToAnnotation[type_] + type_ = type_[item_type] # pyright: ignore [reportIndexIssue] + if hasattr(column, "nullable") and column.nullable: + type_ = type_ | None + return type_ + if name in insp.all_orm_descriptors: + descriptor = insp.all_orm_descriptors[name] + if hint := get_property_hint(descriptor): + return hint + if isinstance(descriptor, QueryableAttribute): + prop = descriptor.property + if isinstance(prop, Relationship): + type_ = prop.mapper.class_ + # TODO: check for nullable? + return list[type_] if prop.uselist else type_ | None + if isinstance(attr, AssociationProxyInstance): + return list[ + get_attribute_access_type( + attr.target_class, + attr.remote_attr.key, # pyright: ignore [reportAttributeAccessIssue] + ) + ] + elif ( + isinstance(cls, type) + and not is_generic_alias(cls) + and issubclass(cls, sqlmodel_types) + ): + # Check in the annotations directly (for sqlmodel.Relationship) + hints = get_type_hints(cls) # pyright: ignore [reportArgumentType] + if name in hints: + type_ = hints[name] + type_origin = get_origin(type_) + if isinstance(type_origin, type) and issubclass(type_origin, Mapped): + return get_args(type_)[0] # SQLAlchemy v2 + return type_ + if is_union(cls): + # Check in each arg of the annotation. + return unionize( + *(get_attribute_access_type(arg, name) for arg in get_args(cls)) + ) + if isinstance(cls, type): + # Bare class + exceptions = NameError + try: + hints = get_type_hints(cls) # pyright: ignore [reportArgumentType] + if name in hints: + return hints[name] + except exceptions as e: + console.warn(f"Failed to resolve ForwardRefs for {cls}.{name} due to {e}") + return None # Attribute is not accessible. + + +@lru_cache +def get_base_class(cls: GenericType) -> type: + """Get the base class of a class. + + Args: + cls: The class. + + Returns: + The base class of the class. + + Raises: + TypeError: If a literal has multiple types. + """ + if is_literal(cls): + # only literals of the same type are supported. + arg_type = type(get_args(cls)[0]) + if not all(type(arg) is arg_type for arg in get_args(cls)): + msg = "only literals of the same type are supported" + raise TypeError(msg) + return type(get_args(cls)[0]) + + if is_union(cls): + return tuple(get_base_class(arg) for arg in get_args(cls)) # pyright: ignore [reportReturnType] + + return get_base_class(cls.__origin__) if is_generic_alias(cls) else cls + + +def does_obj_satisfy_typed_dict( + obj: Any, + cls: GenericType, + *, + nested: int = 0, + treat_var_as_type: bool = True, + treat_mutable_obj_as_immutable: bool = False, +) -> bool: + """Check if an object satisfies a typed dict. + + Args: + obj: The object to check. + cls: The typed dict to check against. + nested: How many levels deep to check. + treat_var_as_type: Whether to treat Var as the type it represents, i.e. _var_type. + treat_mutable_obj_as_immutable: Whether to treat mutable objects as immutable. Useful if a component declares a mutable object as a prop, but the value is not expected to change. + + Returns: + Whether the object satisfies the typed dict. + """ + if not isinstance(obj, Mapping): + return False + + key_names_to_values = get_type_hints(cls) + required_keys: frozenset[str] = getattr(cls, "__required_keys__", frozenset()) + is_closed = getattr(cls, "__closed__", False) + extra_items_type = getattr(cls, "__extra_items__", Any) + + for key, value in obj.items(): + if is_closed and key not in key_names_to_values: + return False + if nested: + if key in key_names_to_values: + expected_type = key_names_to_values[key] + if not _isinstance( + value, + expected_type, + nested=nested - 1, + treat_var_as_type=treat_var_as_type, + treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable, + ): + return False + else: + if not _isinstance( + value, + extra_items_type, + nested=nested - 1, + treat_var_as_type=treat_var_as_type, + treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable, + ): + return False + + # required keys are all present + return required_keys.issubset(frozenset(obj)) + + +def _isinstance( + obj: Any, + cls: GenericType, + *, + nested: int = 0, + treat_var_as_type: bool = True, + treat_mutable_obj_as_immutable: bool = False, +) -> bool: + """Check if an object is an instance of a class. + + Args: + obj: The object to check. + cls: The class to check against. + nested: How many levels deep to check. + treat_var_as_type: Whether to treat Var as the type it represents, i.e. _var_type. + treat_mutable_obj_as_immutable: Whether to treat mutable objects as immutable. Useful if a component declares a mutable object as a prop, but the value is not expected to change. + + Returns: + Whether the object is an instance of the class. + """ + if cls is Any: + return True + + from reflex_core.vars import LiteralVar, Var + + if cls is Var: + return isinstance(obj, Var) + if isinstance(obj, LiteralVar): + return treat_var_as_type and _isinstance( + obj._var_value, cls, nested=nested, treat_var_as_type=True + ) + if isinstance(obj, Var): + return treat_var_as_type and typehint_issubclass( + obj._var_type, + cls, + treat_mutable_superclasss_as_immutable=treat_mutable_obj_as_immutable, + treat_literals_as_union_of_types=True, + treat_any_as_subtype_of_everything=True, + ) + + if cls is None or cls is type(None): + return obj is None + + if cls is not None and is_union(cls): + return any( + _isinstance(obj, arg, nested=nested, treat_var_as_type=treat_var_as_type) + for arg in get_args(cls) + ) + + if is_literal(cls): + return obj in get_args(cls) + + origin = get_origin(cls) + + if origin is None: + # cls is a typed dict + if is_typeddict(cls): + if nested: + return does_obj_satisfy_typed_dict( + obj, + cls, + nested=nested - 1, + treat_var_as_type=treat_var_as_type, + treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable, + ) + return isinstance(obj, dict) + + # cls is a float + if cls is float: + return isinstance(obj, (float, int)) + + # cls is a simple class + return isinstance(obj, cls) + + args = get_args(cls) + + if not args: + if treat_mutable_obj_as_immutable: + if origin is dict: + origin = Mapping + elif origin is list or origin is set: + origin = Sequence + # cls is a simple generic class + return isinstance(obj, origin) + + if origin is Var and args: + # cls is a Var + return _isinstance( + obj, + args[0], + nested=nested, + treat_var_as_type=treat_var_as_type, + treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable, + ) + + if nested > 0 and args: + if origin is list: + expected_class = Sequence if treat_mutable_obj_as_immutable else list + return isinstance(obj, expected_class) and all( + _isinstance( + item, + args[0], + nested=nested - 1, + treat_var_as_type=treat_var_as_type, + ) + for item in obj + ) + if origin is tuple: + if args[-1] is Ellipsis: + return isinstance(obj, tuple) and all( + _isinstance( + item, + args[0], + nested=nested - 1, + treat_var_as_type=treat_var_as_type, + ) + for item in obj + ) + return ( + isinstance(obj, tuple) + and len(obj) == len(args) + and all( + _isinstance( + item, + arg, + nested=nested - 1, + treat_var_as_type=treat_var_as_type, + ) + for item, arg in zip(obj, args, strict=True) + ) + ) + if safe_issubclass(origin, Mapping): + expected_class = ( + dict + if origin is dict and not treat_mutable_obj_as_immutable + else Mapping + ) + return isinstance(obj, expected_class) and all( + _isinstance( + key, args[0], nested=nested - 1, treat_var_as_type=treat_var_as_type + ) + and _isinstance( + value, + args[1], + nested=nested - 1, + treat_var_as_type=treat_var_as_type, + ) + for key, value in obj.items() + ) + if origin is set: + expected_class = Sequence if treat_mutable_obj_as_immutable else set + return isinstance(obj, expected_class) and all( + _isinstance( + item, + args[0], + nested=nested - 1, + treat_var_as_type=treat_var_as_type, + ) + for item in obj + ) + + if args: + from reflex_core.vars import Field + + if origin is Field: + return _isinstance( + obj, args[0], nested=nested, treat_var_as_type=treat_var_as_type + ) + + return isinstance(obj, get_base_class(cls)) + + +def is_dataframe(value: type) -> bool: + """Check if the given value is a dataframe. + + Args: + value: The value to check. + + Returns: + Whether the value is a dataframe. + """ + if is_generic_alias(value) or value == Any: + return False + return value.__name__ == "DataFrame" + + +def is_valid_var_type(type_: type) -> bool: + """Check if the given type is a valid prop type. + + Args: + type_: The type to check. + + Returns: + Whether the type is a valid prop type. + """ + from reflex_core.utils import serializers + + if is_union(type_): + return all(is_valid_var_type(arg) for arg in get_args(type_)) + + if is_literal(type_): + types = {type(value) for value in get_args(type_)} + return all(is_valid_var_type(type_) for type_ in types) + + type_ = origin if (origin := get_origin(type_)) is not None else type_ + + return ( + issubclass(type_, StateVarTypes) + or serializers.has_serializer(type_) + or dataclasses.is_dataclass(type_) + ) + + +def is_backend_base_variable(name: str, cls: type[BaseState]) -> bool: + """Check if this variable name correspond to a backend variable. + + Args: + name: The name of the variable to check + cls: The class of the variable to check (must be a BaseState subclass) + + Returns: + bool: The result of the check + """ + if name in RESERVED_BACKEND_VAR_NAMES: + return False + + if not name.startswith("_"): + return False + + if name.startswith("__"): + return False + + if name.startswith(f"_{cls.__name__}__"): + return False + + hints = cls._get_type_hints() + if name in hints: + hint = get_origin(hints[name]) + if hint == ClassVar: + return False + + if name in cls.inherited_backend_vars: + return False + + from reflex_core.vars.base import is_computed_var + + if name in cls.__dict__: + value = cls.__dict__[name] + if type(value) is classmethod: + return False + if callable(value): + return False + + if isinstance( + value, + ( + types.FunctionType, + property, + cached_property, + ), + ) or is_computed_var(value): + return False + + return True + + +def check_type_in_allowed_types(value_type: type, allowed_types: Iterable) -> bool: + """Check that a value type is found in a list of allowed types. + + Args: + value_type: Type of value. + allowed_types: Iterable of allowed types. + + Returns: + If the type is found in the allowed types. + """ + return get_base_class(value_type) in allowed_types + + +def check_prop_in_allowed_types(prop: Any, allowed_types: Iterable) -> bool: + """Check that a prop value is in a list of allowed types. + Does the check in a way that works regardless if it's a raw value or a state Var. + + Args: + prop: The prop to check. + allowed_types: The list of allowed types. + + Returns: + If the prop type match one of the allowed_types. + """ + from reflex_core.vars import Var + + type_ = prop._var_type if isinstance(prop, Var) else type(prop) + return type_ in allowed_types + + +def is_encoded_fstring(value: Any) -> bool: + """Check if a value is an encoded Var f-string. + + Args: + value: The value string to check. + + Returns: + Whether the value is an f-string + """ + return isinstance(value, str) and constants.REFLEX_VAR_OPENING_TAG in value + + +def validate_literal(key: str, value: Any, expected_type: type, comp_name: str): + """Check that a value is a valid literal. + + Args: + key: The prop name. + value: The prop value to validate. + expected_type: The expected type(literal type). + comp_name: Name of the component. + + Raises: + ValueError: When the value is not a valid literal. + """ + from reflex_core.vars import Var + + if ( + is_literal(expected_type) + and not isinstance(value, Var) # validating vars is not supported yet. + and not is_encoded_fstring(value) # f-strings are not supported. + and value not in expected_type.__args__ + ): + allowed_values = expected_type.__args__ + if value not in allowed_values: + allowed_value_str = ",".join([ + str(v) if not isinstance(v, str) else f"'{v}'" for v in allowed_values + ]) + value_str = f"'{value}'" if isinstance(value, str) else value + msg = f"prop value for {key!s} of the `{comp_name}` component should be one of the following: {allowed_value_str}. Got {value_str} instead" + raise ValueError(msg) + + +def safe_issubclass(cls: Any, cls_check: Any | tuple[Any, ...]): + """Check if a class is a subclass of another class. Returns False if internal error occurs. + + Args: + cls: The class to check. + cls_check: The class to check against. + + Returns: + Whether the class is a subclass of the other class. + """ + try: + return issubclass(cls, cls_check) + except TypeError: + return False + + +def typehint_issubclass( + possible_subclass: Any, + possible_superclass: Any, + *, + treat_mutable_superclasss_as_immutable: bool = False, + treat_literals_as_union_of_types: bool = True, + treat_any_as_subtype_of_everything: bool = False, +) -> bool: + """Check if a type hint is a subclass of another type hint. + + Args: + possible_subclass: The type hint to check. + possible_superclass: The type hint to check against. + treat_mutable_superclasss_as_immutable: Whether to treat target classes as immutable. + treat_literals_as_union_of_types: Whether to treat literals as a union of their types. + treat_any_as_subtype_of_everything: Whether to treat Any as a subtype of everything. This is the default behavior in Python. + + Returns: + Whether the type hint is a subclass of the other type hint. + """ + if possible_subclass is possible_superclass or possible_superclass is Any: + return True + if possible_subclass is Any: + return treat_any_as_subtype_of_everything + if possible_subclass is NoReturn: + return True + + provided_type_origin = get_origin(possible_subclass) + accepted_type_origin = get_origin(possible_superclass) + + if provided_type_origin is None and accepted_type_origin is None: + # In this case, we are dealing with a non-generic type, so we can use issubclass + return issubclass(possible_subclass, possible_superclass) + + if treat_literals_as_union_of_types and is_literal(possible_superclass): + args = get_args(possible_superclass) + return any( + typehint_issubclass( + possible_subclass, + type(arg), + treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, + treat_literals_as_union_of_types=treat_literals_as_union_of_types, + treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, + ) + for arg in args + ) + + if is_literal(possible_subclass): + args = get_args(possible_subclass) + return all( + _isinstance( + arg, + possible_superclass, + treat_mutable_obj_as_immutable=treat_mutable_superclasss_as_immutable, + nested=2, + ) + for arg in args + ) + + provided_type_origin = ( + Union if provided_type_origin is types.UnionType else provided_type_origin + ) + accepted_type_origin = ( + Union if accepted_type_origin is types.UnionType else accepted_type_origin + ) + + # Get type arguments (e.g., [float, int] for dict[float, int]) + provided_args = get_args(possible_subclass) + accepted_args = get_args(possible_superclass) + + if accepted_type_origin is Union: + if provided_type_origin is not Union: + return any( + typehint_issubclass( + possible_subclass, + accepted_arg, + treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, + treat_literals_as_union_of_types=treat_literals_as_union_of_types, + treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, + ) + for accepted_arg in accepted_args + ) + return all( + any( + typehint_issubclass( + provided_arg, + accepted_arg, + treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, + treat_literals_as_union_of_types=treat_literals_as_union_of_types, + treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, + ) + for accepted_arg in accepted_args + ) + for provided_arg in provided_args + ) + if provided_type_origin is Union: + return all( + typehint_issubclass( + provided_arg, + possible_superclass, + treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, + treat_literals_as_union_of_types=treat_literals_as_union_of_types, + treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, + ) + for provided_arg in provided_args + ) + + provided_type_origin = provided_type_origin or possible_subclass + accepted_type_origin = accepted_type_origin or possible_superclass + + if treat_mutable_superclasss_as_immutable: + if accepted_type_origin is dict: + accepted_type_origin = Mapping + elif accepted_type_origin is list or accepted_type_origin is set: + accepted_type_origin = Sequence + + # Check if the origin of both types is the same (e.g., list for list[int]) + if not safe_issubclass( + provided_type_origin or possible_subclass, + accepted_type_origin or possible_superclass, + ): + return False + + # Ensure all specific types are compatible with accepted types + # Note this is not necessarily correct, as it doesn't check against contravariance and covariance + # It also ignores when the length of the arguments is different + return all( + typehint_issubclass( + provided_arg, + accepted_arg, + treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, + treat_literals_as_union_of_types=treat_literals_as_union_of_types, + treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, + ) + for provided_arg, accepted_arg in zip( + provided_args, accepted_args, strict=False + ) + if accepted_arg is not Any + ) + + +def resolve_annotations( + raw_annotations: Mapping[str, type[Any]], module_name: str | None +) -> dict[str, type[Any]]: + """Partially taken from typing.get_type_hints. + + Resolve string or ForwardRef annotations into type objects if possible. + + Args: + raw_annotations: The raw annotations to resolve. + module_name: The name of the module. + + Returns: + The resolved annotations. + """ + module = sys.modules.get(module_name, None) if module_name is not None else None + + base_globals: dict[str, Any] | None = ( + module.__dict__ if module is not None else None + ) + + annotations = {} + for name, value in raw_annotations.items(): + if isinstance(value, str): + if sys.version_info == (3, 10, 0): + value = ForwardRef(value, is_argument=False) + else: + value = ForwardRef(value, is_argument=False, is_class=True) + try: + if sys.version_info >= (3, 13): + value = _eval_type(value, base_globals, None, type_params=()) + else: + value = _eval_type(value, base_globals, None) + except NameError: + # this is ok, it can be fixed with update_forward_refs + pass + annotations[name] = value + return annotations + + +TYPES_THAT_HAS_DEFAULT_VALUE = (int, float, tuple, list, set, dict, str) + + +def get_default_value_for_type(t: GenericType) -> Any: + """Get the default value of the var. + + Args: + t: The type of the var. + + Returns: + The default value of the var, if it has one, else None. + + Raises: + ImportError: If the var is a dataframe and pandas is not installed. + """ + if is_optional(t): + return None + + origin = get_origin(t) if is_generic_alias(t) else t + if origin is Literal: + args = get_args(t) + return args[0] if args else None + if safe_issubclass(origin, TYPES_THAT_HAS_DEFAULT_VALUE): + return origin() + if safe_issubclass(origin, Mapping): + return {} + if is_dataframe(origin): + try: + import pandas as pd + + return pd.DataFrame() + except ImportError as e: + msg = "Please install pandas to use dataframes in your app." + raise ImportError(msg) from e + return None + + +IMMUTABLE_TYPES = ( + int, + float, + bool, + str, + bytes, + frozenset, + tuple, + type(None), + Enum, +) + + +def is_immutable(i: Any) -> bool: + """Check if a value is immutable. + + Args: + i: The value to check. + + Returns: + Whether the value is immutable. + """ + return isinstance(i, IMMUTABLE_TYPES) diff --git a/packages/reflex-core/src/reflex_core/vars/__init__.py b/packages/reflex-core/src/reflex_core/vars/__init__.py new file mode 100644 index 00000000000..4c0ebe85c94 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/vars/__init__.py @@ -0,0 +1,67 @@ +"""Immutable-Based Var System.""" + +from . import base, color, datetime, function, number, object, sequence +from .base import ( + BaseStateMeta, + EvenMoreBasicBaseState, + Field, + LiteralVar, + Var, + VarData, + field, + get_unique_variable_name, + get_uuid_string_var, + var_operation, + var_operation_return, +) +from .color import ColorVar, LiteralColorVar +from .datetime import DateTimeVar +from .function import FunctionStringVar, FunctionVar, VarOperationCall +from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar +from .object import LiteralObjectVar, ObjectVar, RestProp +from .sequence import ( + ArrayVar, + ConcatVarOperation, + LiteralArrayVar, + LiteralStringVar, + StringVar, +) + +__all__ = [ + "ArrayVar", + "BaseStateMeta", + "BooleanVar", + "ColorVar", + "ConcatVarOperation", + "DateTimeVar", + "EvenMoreBasicBaseState", + "Field", + "FunctionStringVar", + "FunctionVar", + "LiteralArrayVar", + "LiteralBooleanVar", + "LiteralColorVar", + "LiteralNumberVar", + "LiteralObjectVar", + "LiteralStringVar", + "LiteralVar", + "NumberVar", + "ObjectVar", + "RestProp", + "StringVar", + "Var", + "VarData", + "VarOperationCall", + "base", + "color", + "datetime", + "field", + "function", + "get_unique_variable_name", + "get_uuid_string_var", + "number", + "object", + "sequence", + "var_operation", + "var_operation_return", +] diff --git a/packages/reflex-core/src/reflex_core/vars/base.py b/packages/reflex-core/src/reflex_core/vars/base.py new file mode 100644 index 00000000000..e9792ff823b --- /dev/null +++ b/packages/reflex-core/src/reflex_core/vars/base.py @@ -0,0 +1,3692 @@ +"""Collection of base classes.""" + +from __future__ import annotations + +import contextlib +import copy +import dataclasses +import datetime +import functools +import inspect +import json +import re +import string +import uuid +import warnings +from abc import ABCMeta +from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence +from dataclasses import _MISSING_TYPE, MISSING +from decimal import Decimal +from types import CodeType, FunctionType +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + ClassVar, + Generic, + Literal, + NoReturn, + ParamSpec, + Protocol, + TypeGuard, + TypeVar, + cast, + get_args, + get_type_hints, + overload, +) + +from rich.markup import escape +from typing_extensions import dataclass_transform, override + +from reflex_core import constants +from reflex_core.constants.compiler import Hooks +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.utils import console, exceptions, imports, serializers, types +from reflex_core.utils.compat import annotations_from_namespace +from reflex_core.utils.decorator import once +from reflex_core.utils.exceptions import ( + ComputedVarSignatureError, + UntypedComputedVarError, + VarAttributeError, + VarDependencyError, + VarTypeError, +) +from reflex_core.utils.format import format_state_name +from reflex_core.utils.imports import ( + ImmutableImportDict, + ImmutableParsedImportDict, + ImportDict, + ImportVar, + ParsedImportTuple, + parse_imports, +) +from reflex_core.utils.types import ( + GenericType, + Self, + _isinstance, + get_origin, + has_args, + safe_issubclass, + unionize, +) + +if TYPE_CHECKING: + from reflex.state import BaseState + from reflex_core.components.component import BaseComponent + from reflex_core.constants.colors import Color + + from .color import LiteralColorVar + from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar + from .object import LiteralObjectVar, ObjectVar + from .sequence import ArrayVar, LiteralArrayVar, LiteralStringVar, StringVar + + +VAR_TYPE = TypeVar("VAR_TYPE", covariant=True) +OTHER_VAR_TYPE = TypeVar("OTHER_VAR_TYPE") +STRING_T = TypeVar("STRING_T", bound=str) +SEQUENCE_TYPE = TypeVar("SEQUENCE_TYPE", bound=Sequence) + +warnings.filterwarnings("ignore", message="fields may not start with an underscore") + +_PYDANTIC_VALIDATE_VALUES = "__pydantic_validate_values__" + + +def _pydantic_validator(*args, **kwargs): + return None + + +@dataclasses.dataclass( + eq=False, + frozen=True, +) +class VarSubclassEntry: + """Entry for a Var subclass.""" + + var_subclass: type[Var] + to_var_subclass: type[ToOperation] + python_types: tuple[GenericType, ...] + + +_var_subclasses: list[VarSubclassEntry] = [] +_var_literal_subclasses: list[tuple[type[LiteralVar], VarSubclassEntry]] = [] + + +@dataclasses.dataclass( + eq=True, + frozen=True, +) +class VarData: + """Metadata associated with a x.""" + + # The name of the enclosing state. + state: str = dataclasses.field(default="") + + # The name of the field in the state. + field_name: str = dataclasses.field(default="") + + # Imports needed to render this var + imports: ParsedImportTuple = dataclasses.field(default_factory=tuple) + + # Hooks that need to be present in the component to render this var + hooks: tuple[str, ...] = dataclasses.field(default_factory=tuple) + + # Dependencies of the var + deps: tuple[Var, ...] = dataclasses.field(default_factory=tuple) + + # Position of the hook in the component + position: Hooks.HookPosition | None = None + + # Components that are part of this var + components: tuple[BaseComponent, ...] = dataclasses.field(default_factory=tuple) + + def __init__( + self, + state: str = "", + field_name: str = "", + imports: ImmutableImportDict | ImmutableParsedImportDict | None = None, + hooks: Mapping[str, VarData | None] | Sequence[str] | str | None = None, + deps: list[Var] | None = None, + position: Hooks.HookPosition | None = None, + components: Iterable[BaseComponent] | None = None, + ): + """Initialize the var data. + + Args: + state: The name of the enclosing state. + field_name: The name of the field in the state. + imports: Imports needed to render this var. + hooks: Hooks that need to be present in the component to render this var. + deps: Dependencies of the var for useCallback. + position: Position of the hook in the component. + components: Components that are part of this var. + """ + if isinstance(hooks, str): + hooks = [hooks] + if not isinstance(hooks, dict): + hooks = dict.fromkeys(hooks or []) + immutable_imports: ParsedImportTuple = tuple( + (k, tuple(v)) for k, v in parse_imports(imports or {}).items() + ) + object.__setattr__(self, "state", state) + object.__setattr__(self, "field_name", field_name) + object.__setattr__(self, "imports", immutable_imports) + object.__setattr__(self, "hooks", tuple(hooks or {})) + object.__setattr__(self, "deps", tuple(deps or [])) + object.__setattr__(self, "position", position or None) + object.__setattr__(self, "components", tuple(components or [])) + + if hooks and any(hooks.values()): + # Merge our dependencies first, so they can be referenced. + merged_var_data = VarData.merge(*hooks.values(), self) + if merged_var_data is not None: + object.__setattr__(self, "state", merged_var_data.state) + object.__setattr__(self, "field_name", merged_var_data.field_name) + object.__setattr__(self, "imports", merged_var_data.imports) + object.__setattr__(self, "hooks", merged_var_data.hooks) + object.__setattr__(self, "deps", merged_var_data.deps) + object.__setattr__(self, "position", merged_var_data.position) + object.__setattr__(self, "components", merged_var_data.components) + + def old_school_imports(self) -> ImportDict: + """Return the imports as a mutable dict. + + Returns: + The imports as a mutable dict. + """ + return {k: list(v) for k, v in self.imports} + + def merge(*all: VarData | None) -> VarData | None: + """Merge multiple var data objects. + + Args: + *all: The var data objects to merge. + + Returns: + The merged var data object. + + Raises: + ReflexError: If trying to merge VarData with different positions. + + # noqa: DAR102 *all + """ + all_var_datas = list(filter(None, all)) + + if not all_var_datas: + return None + + if len(all_var_datas) == 1: + return all_var_datas[0] + + # Get the first non-empty field name or default to empty string. + field_name = next( + (var_data.field_name for var_data in all_var_datas if var_data.field_name), + "", + ) + + # Get the first non-empty state or default to empty string. + state = next( + (var_data.state for var_data in all_var_datas if var_data.state), "" + ) + + hooks: dict[str, VarData | None] = { + hook: None for var_data in all_var_datas for hook in var_data.hooks + } + + imports_ = imports.merge_imports( + *(var_data.imports for var_data in all_var_datas) + ) + + deps = [dep for var_data in all_var_datas for dep in var_data.deps] + + positions = list( + dict.fromkeys( + var_data.position + for var_data in all_var_datas + if var_data.position is not None + ) + ) + if positions: + if len(positions) > 1: + msg = f"Cannot merge var data with different positions: {positions}" + raise exceptions.ReflexError(msg) + position = positions[0] + else: + position = None + + components = tuple( + component for var_data in all_var_datas for component in var_data.components + ) + + return VarData( + state=state, + field_name=field_name, + imports=imports_, + hooks=hooks, + deps=deps, + position=position, + components=components, + ) + + def __bool__(self) -> bool: + """Check if the var data is non-empty. + + Returns: + True if any field is set to a non-default value. + """ + return bool( + self.state + or self.imports + or self.hooks + or self.field_name + or self.deps + or self.position + or self.components + ) + + @classmethod + def from_state(cls, state: type[BaseState] | str, field_name: str = "") -> VarData: + """Set the state of the var. + + Args: + state: The state to set or the full name of the state. + field_name: The name of the field in the state. Optional. + + Returns: + The var with the set state. + """ + from reflex_core.utils import format + + state_name = state if isinstance(state, str) else state.get_full_name() + return VarData( + state=state_name, + field_name=field_name, + hooks={ + "const {0} = useContext(StateContexts.{0})".format( + format.format_state_name(state_name) + ): None + }, + imports={ + f"$/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="StateContexts")], + "react": [ImportVar(tag="useContext")], + }, + ) + + +def _decode_var_immutable(value: str) -> tuple[VarData | None, str]: + """Decode the state name from a formatted var. + + Args: + value: The value to extract the state name from. + + Returns: + The extracted state name and the value without the state name. + """ + var_datas = [] + if isinstance(value, str): + # fast path if there is no encoded VarData + if constants.REFLEX_VAR_OPENING_TAG not in value: + return None, value + + offset = 0 + + # Find all tags. + while m := _decode_var_pattern.search(value): + start, end = m.span() + value = value[:start] + value[end:] + + serialized_data = m.group(1) + + if serialized_data.isnumeric() or ( + serialized_data[0] == "-" and serialized_data[1:].isnumeric() + ): + # This is a global immutable var. + var = _global_vars[int(serialized_data)] + var_data = var._get_all_var_data() + + if var_data is not None: + var_datas.append(var_data) + offset += end - start + + return VarData.merge(*var_datas) if var_datas else None, value + + +def can_use_in_object_var(cls: GenericType) -> bool: + """Check if the class can be used in an ObjectVar. + + Args: + cls: The class to check. + + Returns: + Whether the class can be used in an ObjectVar. + """ + if types.is_union(cls): + return all(can_use_in_object_var(t) for t in types.get_args(cls)) + return ( + isinstance(cls, type) + and not safe_issubclass(cls, Var) + and serializers.can_serialize(cls, dict) + ) + + +class MetaclassVar(type): + """Metaclass for the Var class.""" + + def __setattr__(cls, name: str, value: Any): + """Set an attribute on the class. + + Args: + name: The name of the attribute. + value: The value of the attribute. + """ + super().__setattr__( + name, value if name != _PYDANTIC_VALIDATE_VALUES else _pydantic_validator + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, +) +class Var(Generic[VAR_TYPE], metaclass=MetaclassVar): + """Base class for immutable vars.""" + + # The name of the var. + _js_expr: str = dataclasses.field() + + # The type of the var. + _var_type: types.GenericType = dataclasses.field(default=Any) + + # Extra metadata associated with the Var + _var_data: VarData | None = dataclasses.field(default=None) + + def __str__(self) -> str: + """String representation of the var. Guaranteed to be a valid Javascript expression. + + Returns: + The name of the var. + """ + return self._js_expr + + @property + def _var_is_local(self) -> bool: + """Whether this is a local javascript variable. + + Returns: + False + """ + return False + + @property + def _var_is_string(self) -> bool: + """Whether the var is a string literal. + + Returns: + False + """ + return False + + def __init_subclass__( + cls, + python_types: tuple[GenericType, ...] | GenericType = types.Unset(), + default_type: GenericType = types.Unset(), + **kwargs, + ): + """Initialize the subclass. + + Args: + python_types: The python types that the var represents. + default_type: The default type of the var. Defaults to the first python type. + **kwargs: Additional keyword arguments. + """ + super().__init_subclass__(**kwargs) + + if python_types or default_type: + python_types = ( + (python_types if isinstance(python_types, tuple) else (python_types,)) + if python_types + else () + ) + + default_type = default_type or (python_types[0] if python_types else Any) + + @dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, + ) + class ToVarOperation(ToOperation, cls): + """Base class of converting a var to another var type.""" + + _original: Var = dataclasses.field( + default=Var(_js_expr="null", _var_type=None), + ) + + _default_var_type: ClassVar[GenericType] = default_type + + new_to_var_operation_name = f"{cls.__name__.removesuffix('Var')}CastedVar" + ToVarOperation.__qualname__ = ( + ToVarOperation.__qualname__.removesuffix(ToVarOperation.__name__) + + new_to_var_operation_name + ) + ToVarOperation.__name__ = new_to_var_operation_name + + _var_subclasses.append(VarSubclassEntry(cls, ToVarOperation, python_types)) + + def __post_init__(self): + """Post-initialize the var. + + Raises: + TypeError: If _js_expr is not a string. + """ + if not isinstance(self._js_expr, str): + msg = f"Expected _js_expr to be a string, got value {self._js_expr!r} of type {type(self._js_expr).__name__}" + raise TypeError(msg) + + if self._var_data is not None and not isinstance(self._var_data, VarData): + msg = f"Expected _var_data to be a VarData, got value {self._var_data!r} of type {type(self._var_data).__name__}" + raise TypeError(msg) + + # Decode any inline Var markup and apply it to the instance + var_data_, js_expr_ = _decode_var_immutable(self._js_expr) + + if var_data_ or js_expr_ != self._js_expr: + self.__init__( + _js_expr=js_expr_, + _var_type=self._var_type, + _var_data=VarData.merge(self._var_data, var_data_), + ) + + def __hash__(self) -> int: + """Define a hash function for the var. + + Returns: + The hash of the var. + """ + return hash((self._js_expr, self._var_type, self._var_data)) + + def _get_all_var_data(self) -> VarData | None: + """Get all VarData associated with the Var. + + Returns: + The VarData of the components and all of its children. + """ + return self._var_data + + def __deepcopy__(self, memo: dict[int, Any]) -> Self: + """Deepcopy the var. + + Args: + memo: The memo dictionary to use for the deepcopy. + + Returns: + A deepcopy of the var. + """ + return self + + def equals(self, other: Var) -> bool: + """Check if two vars are equal. + + Args: + other: The other var to compare. + + Returns: + Whether the vars are equal. + """ + return ( + self._js_expr == other._js_expr + and self._var_type == other._var_type + and self._get_all_var_data() == other._get_all_var_data() + ) + + @overload + def _replace( + self, + _var_type: type[OTHER_VAR_TYPE], + merge_var_data: VarData | None = None, + **kwargs: Any, + ) -> Var[OTHER_VAR_TYPE]: ... + + @overload + def _replace( + self, + _var_type: GenericType | None = None, + merge_var_data: VarData | None = None, + **kwargs: Any, + ) -> Self: ... + + def _replace( + self, + _var_type: GenericType | None = None, + merge_var_data: VarData | None = None, + **kwargs: Any, + ) -> Self | Var: + """Make a copy of this Var with updated fields. + + Args: + _var_type: The new type of the Var. + merge_var_data: VarData to merge into the existing VarData. + **kwargs: Var fields to update. + + Returns: + A new Var with the updated fields overwriting the corresponding fields in this Var. + + Raises: + TypeError: If _var_is_local, _var_is_string, or _var_full_name_needs_state_prefix is not None. + """ + if kwargs.get("_var_is_local", False) is not False: + msg = "The _var_is_local argument is not supported for Var." + raise TypeError(msg) + + if kwargs.get("_var_is_string", False) is not False: + msg = "The _var_is_string argument is not supported for Var." + raise TypeError(msg) + + if kwargs.get("_var_full_name_needs_state_prefix", False) is not False: + msg = "The _var_full_name_needs_state_prefix argument is not supported for Var." + raise TypeError(msg) + value_with_replaced = dataclasses.replace( + self, + _var_type=_var_type or self._var_type, + _var_data=VarData.merge( + kwargs.get("_var_data", self._var_data), merge_var_data + ), + **kwargs, + ) + + if (js_expr := kwargs.get("_js_expr")) is not None: + object.__setattr__(value_with_replaced, "_js_expr", js_expr) + + return value_with_replaced + + @overload + @classmethod + def create( # pyright: ignore[reportOverlappingOverload] + cls, + value: NoReturn, + _var_data: VarData | None = None, + ) -> Var[Any]: ... + + @overload + @classmethod + def create( # pyright: ignore[reportOverlappingOverload] + cls, + value: bool, + _var_data: VarData | None = None, + ) -> LiteralBooleanVar: ... + + @overload + @classmethod + def create( + cls, + value: int, + _var_data: VarData | None = None, + ) -> LiteralNumberVar[int]: ... + + @overload + @classmethod + def create( + cls, + value: float, + _var_data: VarData | None = None, + ) -> LiteralNumberVar[float]: ... + + @overload + @classmethod + def create( + cls, + value: Decimal, + _var_data: VarData | None = None, + ) -> LiteralNumberVar[Decimal]: ... + + @overload + @classmethod + def create( # pyright: ignore [reportOverlappingOverload] + cls, + value: Color, + _var_data: VarData | None = None, + ) -> LiteralColorVar: ... + + @overload + @classmethod + def create( # pyright: ignore [reportOverlappingOverload] + cls, + value: str, + _var_data: VarData | None = None, + ) -> LiteralStringVar: ... + + @overload + @classmethod + def create( # pyright: ignore [reportOverlappingOverload] + cls, + value: STRING_T, + _var_data: VarData | None = None, + ) -> StringVar[STRING_T]: ... + + @overload + @classmethod + def create( # pyright: ignore[reportOverlappingOverload] + cls, + value: None, + _var_data: VarData | None = None, + ) -> LiteralNoneVar: ... + + @overload + @classmethod + def create( + cls, + value: MAPPING_TYPE, + _var_data: VarData | None = None, + ) -> LiteralObjectVar[MAPPING_TYPE]: ... + + @overload + @classmethod + def create( + cls, + value: SEQUENCE_TYPE, + _var_data: VarData | None = None, + ) -> LiteralArrayVar[SEQUENCE_TYPE]: ... + + @overload + @classmethod + def create( + cls, + value: OTHER_VAR_TYPE, + _var_data: VarData | None = None, + ) -> Var[OTHER_VAR_TYPE]: ... + + @classmethod + def create( + cls, + value: OTHER_VAR_TYPE, + _var_data: VarData | None = None, + ) -> Var[OTHER_VAR_TYPE]: + """Create a var from a value. + + Args: + value: The value to create the var from. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + # If the value is already a var, do nothing. + if isinstance(value, Var): + return value + + return LiteralVar.create(value, _var_data=_var_data) + + def __format__(self, format_spec: str) -> str: + """Format the var into a Javascript equivalent to an f-string. + + Args: + format_spec: The format specifier (Ignored for now). + + Returns: + The formatted var. + """ + hashed_var = hash(self) + + _global_vars[hashed_var] = self + + # Encode the _var_data into the formatted output for tracking purposes. + return f"{constants.REFLEX_VAR_OPENING_TAG}{hashed_var}{constants.REFLEX_VAR_CLOSING_TAG}{self._js_expr}" + + @overload + def to(self, output: type[str]) -> StringVar: ... # pyright: ignore[reportOverlappingOverload] + + @overload + def to(self, output: type[bool]) -> BooleanVar: ... + + @overload + def to(self, output: type[int]) -> NumberVar[int]: ... + + @overload + def to(self, output: type[float]) -> NumberVar[float]: ... + + @overload + def to(self, output: type[Decimal]) -> NumberVar[Decimal]: ... + + @overload + def to( + self, + output: type[SEQUENCE_TYPE], + ) -> ArrayVar[SEQUENCE_TYPE]: ... + + @overload + def to( + self, + output: type[MAPPING_TYPE], + ) -> ObjectVar[MAPPING_TYPE]: ... + + @overload + def to( + self, output: type[ObjectVar], var_type: type[VAR_INSIDE] + ) -> ObjectVar[VAR_INSIDE]: ... + + @overload + def to( + self, output: type[ObjectVar], var_type: None = None + ) -> ObjectVar[VAR_TYPE]: ... + + @overload + def to(self, output: VAR_SUBCLASS, var_type: None = None) -> VAR_SUBCLASS: ... + + @overload + def to( + self, + output: type[OUTPUT] | types.GenericType, + var_type: types.GenericType | None = None, + ) -> OUTPUT: ... + + def to( + self, + output: type[OUTPUT] | types.GenericType, + var_type: types.GenericType | None = None, + ) -> Var: + """Convert the var to a different type. + + Args: + output: The output type. + var_type: The type of the var. + + Returns: + The converted var. + """ + from .object import ObjectVar + + fixed_output_type = get_origin(output) or output + + # If the first argument is a python type, we map it to the corresponding Var type. + for var_subclass in _var_subclasses[::-1]: + if fixed_output_type in var_subclass.python_types or safe_issubclass( + fixed_output_type, var_subclass.python_types + ): + return self.to(var_subclass.var_subclass, output) + + if fixed_output_type is None: + return get_to_operation(NoneVar).create(self) # pyright: ignore [reportReturnType] + + # Handle fixed_output_type being Base or a dataclass. + if can_use_in_object_var(output): + return self.to(ObjectVar, output) + + if isinstance(output, type): + for var_subclass in _var_subclasses[::-1]: + if safe_issubclass(output, var_subclass.var_subclass): + current_var_type = self._var_type + if current_var_type is Any: + new_var_type = var_type + else: + new_var_type = var_type or current_var_type + return var_subclass.to_var_subclass.create( # pyright: ignore [reportReturnType] + value=self, _var_type=new_var_type + ) + + # If we can't determine the first argument, we just replace the _var_type. + if not safe_issubclass(output, Var) or var_type is None: + return dataclasses.replace( + self, + _var_type=output, + ) + + # We couldn't determine the output type to be any other Var type, so we replace the _var_type. + if var_type is not None: + return dataclasses.replace( + self, + _var_type=var_type, + ) + + return self + + @overload + def guess_type(self: Var[NoReturn]) -> Var[Any]: ... # pyright: ignore [reportOverlappingOverload] + + @overload + def guess_type(self: Var[str]) -> StringVar: ... + + @overload + def guess_type(self: Var[bool]) -> BooleanVar: ... + + @overload + def guess_type(self: Var[int] | Var[float] | Var[int | float]) -> NumberVar: ... + + @overload + def guess_type(self) -> Self: ... + + def guess_type(self) -> Var: + """Guesses the type of the variable based on its `_var_type` attribute. + + Returns: + Var: The guessed type of the variable. + + Raises: + TypeError: If the type is not supported for guessing. + """ + from .object import ObjectVar + + var_type = self._var_type + if var_type is None: + return self.to(None) + if var_type is NoReturn: + return self.to(Any) + + var_type = types.value_inside_optional(var_type) + + if var_type is Any: + return self + + fixed_type = get_origin(var_type) or var_type + + if fixed_type in types.UnionTypes: + inner_types = get_args(var_type) + non_optional_inner_types = [ + types.value_inside_optional(inner_type) for inner_type in inner_types + ] + fixed_inner_types = [ + get_origin(inner_type) or inner_type + for inner_type in non_optional_inner_types + ] + + for var_subclass in _var_subclasses[::-1]: + if all( + safe_issubclass(t, var_subclass.python_types) + for t in fixed_inner_types + ): + return self.to(var_subclass.var_subclass, self._var_type) + + if can_use_in_object_var(var_type): + return self.to(ObjectVar, self._var_type) + + return self + + if fixed_type is Literal: + args = get_args(var_type) + fixed_type = unionize(*(type(arg) for arg in args)) + + if not isinstance(fixed_type, type): + msg = f"Unsupported type {var_type} for guess_type." + raise TypeError(msg) + + if fixed_type is None: + return self.to(None) + + for var_subclass in _var_subclasses[::-1]: + if safe_issubclass(fixed_type, var_subclass.python_types): + return self.to(var_subclass.var_subclass, self._var_type) + + if can_use_in_object_var(fixed_type): + return self.to(ObjectVar, self._var_type) + + return self + + @staticmethod + def _get_setter_name_for_name( + name: str, + ) -> str: + """Get the name of the var's generated setter function. + + Args: + name: The name of the var. + + Returns: + The name of the setter function. + """ + return constants.SETTER_PREFIX + name + + def _get_setter(self, name: str) -> Callable[[BaseState, Any], None]: + """Get the var's setter function. + + Args: + name: The name of the var. + + Returns: + A function that that creates a setter for the var. + """ + setter_name = Var._get_setter_name_for_name(name) + + def setter(state: Any, value: Any): + """Get the setter for the var. + + Args: + state: The state within which we add the setter function. + value: The value to set. + """ + if self._var_type in [int, float]: + try: + value = self._var_type(value) + setattr(state, name, value) + except ValueError: + console.debug( + f"{type(state).__name__}.{self._js_expr}: Failed conversion of {value!s} to '{self._var_type.__name__}'. Value not set.", + ) + else: + setattr(state, name, value) + + setter.__annotations__["value"] = self._var_type + + setter.__qualname__ = setter_name + + return setter + + def _var_set_state(self, state: type[BaseState] | str) -> Self: + """Set the state of the var. + + Args: + state: The state to set. + + Returns: + The var with the state set. + """ + formatted_state_name = ( + state + if isinstance(state, str) + else format_state_name(state.get_full_name()) + ) + + return StateOperation.create( # pyright: ignore [reportReturnType] + formatted_state_name, + self, + _var_data=VarData.merge( + VarData.from_state(state, self._js_expr), self._var_data + ), + ).guess_type() + + def __eq__(self, other: Var | Any) -> BooleanVar: + """Check if the current variable is equal to the given variable. + + Args: + other (Var | Any): The variable to compare with. + + Returns: + BooleanVar: A BooleanVar object representing the result of the equality check. + """ + from .number import equal_operation + + return equal_operation(self, other) + + def __ne__(self, other: Var | Any) -> BooleanVar: + """Check if the current object is not equal to the given object. + + Parameters: + other (Var | Any): The object to compare with. + + Returns: + BooleanVar: A BooleanVar object representing the result of the comparison. + """ + from .number import equal_operation + + return ~equal_operation(self, other) + + def bool(self) -> BooleanVar: + """Convert the var to a boolean. + + Returns: + The boolean var. + """ + from .number import boolify + + return boolify(self) + + def is_none(self) -> BooleanVar: + """Check if the var is None. + + Returns: + A BooleanVar object representing the result of the check. + """ + from .number import is_not_none_operation + + return ~is_not_none_operation(self) + + def is_not_none(self) -> BooleanVar: + """Check if the var is not None. + + Returns: + A BooleanVar object representing the result of the check. + """ + from .number import is_not_none_operation + + return is_not_none_operation(self) + + def __and__( + self, other: Var[OTHER_VAR_TYPE] | Any + ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: + """Perform a logical AND operation on the current instance and another variable. + + Args: + other: The variable to perform the logical AND operation with. + + Returns: + A `BooleanVar` object representing the result of the logical AND operation. + """ + return and_operation(self, other) + + def __rand__( + self, other: Var[OTHER_VAR_TYPE] | Any + ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: + """Perform a logical AND operation on the current instance and another variable. + + Args: + other: The variable to perform the logical AND operation with. + + Returns: + A `BooleanVar` object representing the result of the logical AND operation. + """ + return and_operation(other, self) + + def __or__( + self, other: Var[OTHER_VAR_TYPE] | Any + ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: + """Perform a logical OR operation on the current instance and another variable. + + Args: + other: The variable to perform the logical OR operation with. + + Returns: + A `BooleanVar` object representing the result of the logical OR operation. + """ + return or_operation(self, other) + + def __ror__( + self, other: Var[OTHER_VAR_TYPE] | Any + ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: + """Perform a logical OR operation on the current instance and another variable. + + Args: + other: The variable to perform the logical OR operation with. + + Returns: + A `BooleanVar` object representing the result of the logical OR operation. + """ + return or_operation(other, self) + + def __invert__(self) -> BooleanVar: + """Perform a logical NOT operation on the current instance. + + Returns: + A `BooleanVar` object representing the result of the logical NOT operation. + """ + return ~self.bool() + + def to_string(self, use_json: bool = True) -> StringVar: + """Convert the var to a string. + + Args: + use_json: Whether to use JSON stringify. If False, uses Object.prototype.toString. + + Returns: + The string var. + """ + from .function import JSON_STRINGIFY, PROTOTYPE_TO_STRING + from .sequence import StringVar + + return ( + JSON_STRINGIFY.call(self).to(StringVar) + if use_json + else PROTOTYPE_TO_STRING.call(self).to(StringVar) + ) + + def _as_ref(self) -> Var: + """Get a reference to the var. + + Returns: + The reference to the var. + """ + return Var( + _js_expr=f"refs[{Var.create(str(self))}]", + _var_data=VarData( + imports={ + f"$/{constants.Dirs.STATE_PATH}": [imports.ImportVar(tag="refs")] + } + ), + ).to(str) + + def js_type(self) -> StringVar: + """Returns the javascript type of the object. + + This method uses the `typeof` function from the `FunctionStringVar` class + to determine the type of the object. + + Returns: + StringVar: A string variable representing the type of the object. + """ + from .function import FunctionStringVar + from .sequence import StringVar + + type_of = FunctionStringVar("typeof") + return type_of.call(self).to(StringVar) + + def _without_data(self): + """Create a copy of the var without the data. + + Returns: + The var without the data. + """ + return dataclasses.replace(self, _var_data=None) + + def _decode(self) -> Any: + """Decode Var as a python value. + + Note that Var with state set cannot be decoded python-side and will be + returned as full_name. + + Returns: + The decoded value or the Var name. + """ + if isinstance(self, LiteralVar): + return self._var_value + try: + return json.loads(str(self)) + except ValueError: + return str(self) + + @property + def _var_state(self) -> str: + """Compat method for getting the state. + + Returns: + The state name associated with the var. + """ + var_data = self._get_all_var_data() + return var_data.state if var_data else "" + + @overload + @classmethod + def range(cls, stop: int | NumberVar, /) -> ArrayVar[Sequence[int]]: ... + + @overload + @classmethod + def range( + cls, + start: int | NumberVar, + end: int | NumberVar, + step: int | NumberVar = 1, + /, + ) -> ArrayVar[Sequence[int]]: ... + + @classmethod + def range( + cls, + first_endpoint: int | NumberVar, + second_endpoint: int | NumberVar | None = None, + step: int | NumberVar | None = None, + ) -> ArrayVar[Sequence[int]]: + """Create a range of numbers. + + Args: + first_endpoint: The end of the range if second_endpoint is not provided, otherwise the start of the range. + second_endpoint: The end of the range. + step: The step of the range. + + Returns: + The range of numbers. + """ + from .sequence import ArrayVar + + return ArrayVar.range(first_endpoint, second_endpoint, step) + + if not TYPE_CHECKING: + + def __getitem__(self, key: Any) -> Var: + """Get the item from the var. + + Args: + key: The key to get. + + Raises: + UntypedVarError: If the var type is Any. + TypeError: If the var type is Any. + + # noqa: DAR101 self + """ + if self._var_type is Any: + raise exceptions.UntypedVarError( + self, + f"access the item '{key}'", + ) + msg = f"Var of type {self._var_type} does not support item access." + raise TypeError(msg) + + def __getattr__(self, name: str): + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Raises: + VarAttributeError: If the attribute does not exist. + UntypedVarError: If the var type is Any. + TypeError: If the var type is Any. + + # noqa: DAR101 self + """ + if name.startswith("_"): + msg = f"Attribute {name} not found." + raise VarAttributeError(msg) + + if name == "contains": + msg = f"Var of type {self._var_type} does not support contains check." + raise TypeError(msg) + if name == "reverse": + msg = "Cannot reverse non-list var." + raise TypeError(msg) + + if self._var_type is Any: + raise exceptions.UntypedVarError( + self, + f"access the attribute '{name}'", + ) + + msg = f"The State var {escape(self._js_expr)} of type {escape(str(self._var_type))} has no attribute '{name}' or may have been annotated wrongly." + raise VarAttributeError(msg) + + def __bool__(self) -> bool: + """Raise exception if using Var in a boolean context. + + Raises: + VarTypeError: when attempting to bool-ify the Var. + + # noqa: DAR101 self + """ + msg = ( + f"Cannot convert Var {str(self)!r} to bool for use with `if`, `and`, `or`, and `not`. " + "Instead use `rx.cond` and bitwise operators `&` (and), `|` (or), `~` (invert)." + ) + raise VarTypeError(msg) + + def __iter__(self) -> Any: + """Raise exception if using Var in an iterable context. + + Raises: + VarTypeError: when attempting to iterate over the Var. + + # noqa: DAR101 self + """ + msg = f"Cannot iterate over Var {str(self)!r}. Instead use `rx.foreach`." + raise VarTypeError(msg) + + def __contains__(self, _: Any) -> Var: + """Override the 'in' operator to alert the user that it is not supported. + + Raises: + VarTypeError: the operation is not supported + + # noqa: DAR101 self + """ + msg = ( + "'in' operator not supported for Var types, use Var.contains() instead." + ) + raise VarTypeError(msg) + + +OUTPUT = TypeVar("OUTPUT", bound=Var) + +VAR_SUBCLASS = TypeVar("VAR_SUBCLASS", bound=Var) +VAR_INSIDE = TypeVar("VAR_INSIDE") + + +class ToOperation: + """A var operation that converts a var to another type.""" + + def __getattr__(self, name: str) -> Any: + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute of the var. + """ + from .object import ObjectVar + + if isinstance(self, ObjectVar) and name != "_js_expr": + return ObjectVar.__getattr__(self, name) + return getattr(self._original, name) + + def __post_init__(self): + """Post initialization.""" + object.__delattr__(self, "_js_expr") + + def __hash__(self) -> int: + """Calculate the hash value of the object. + + Returns: + int: The hash value of the object. + """ + return hash(self._original) + + def _get_all_var_data(self) -> VarData | None: + """Get all the var data. + + Returns: + The var data. + """ + return VarData.merge( + self._original._get_all_var_data(), + self._var_data, + ) + + @classmethod + def create( + cls, + value: Var, + _var_type: GenericType | None = None, + _var_data: VarData | None = None, + ): + """Create a ToOperation. + + Args: + value: The value of the var. + _var_type: The type of the Var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The ToOperation. + """ + return cls( + _js_expr="", # pyright: ignore [reportCallIssue] + _var_data=_var_data, # pyright: ignore [reportCallIssue] + _var_type=_var_type or cls._default_var_type, # pyright: ignore [reportCallIssue, reportAttributeAccessIssue] + _original=value, # pyright: ignore [reportCallIssue] + ) + + +class LiteralVar(Var): + """Base class for immutable literal vars.""" + + def __init_subclass__(cls, **kwargs): + """Initialize the subclass. + + Args: + **kwargs: Additional keyword arguments. + + Raises: + TypeError: If the LiteralVar subclass does not have a corresponding Var subclass. + """ + super().__init_subclass__(**kwargs) + + bases = cls.__bases__ + + bases_normalized = [ + base if isinstance(base, type) else get_origin(base) for base in bases + ] + + possible_bases = [ + base + for base in bases_normalized + if safe_issubclass(base, Var) and base != LiteralVar + ] + + if not possible_bases: + msg = f"LiteralVar subclass {cls} must have a base class that is a subclass of Var and not LiteralVar." + raise TypeError(msg) + + var_subclasses = [ + var_subclass + for var_subclass in _var_subclasses + if var_subclass.var_subclass in possible_bases + ] + + if not var_subclasses: + msg = f"LiteralVar {cls} must have a base class annotated with `python_types`." + raise TypeError(msg) + + if len(var_subclasses) != 1: + msg = f"LiteralVar {cls} must have exactly one base class annotated with `python_types`." + raise TypeError(msg) + + var_subclass = var_subclasses[0] + + # Remove the old subclass, happens because __init_subclass__ is called twice + # for each subclass. This is because of __slots__ in dataclasses. + for var_literal_subclass in list(_var_literal_subclasses): + if var_literal_subclass[1] is var_subclass: + _var_literal_subclasses.remove(var_literal_subclass) + + _var_literal_subclasses.append((cls, var_subclass)) + + @classmethod + def _create_literal_var( + cls, + value: Any, + _var_data: VarData | None = None, + ) -> Var: + """Create a var from a value. + + Args: + value: The value to create the var from. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + + Raises: + TypeError: If the value is not a supported type for LiteralVar. + """ + from .object import LiteralObjectVar + from .sequence import ArrayVar, LiteralStringVar + + if isinstance(value, Var): + if _var_data is None: + return value + return value._replace(merge_var_data=_var_data) + + for literal_subclass, var_subclass in _var_literal_subclasses[::-1]: + if isinstance(value, var_subclass.python_types): + return literal_subclass.create(value, _var_data=_var_data) + + if ( + (as_var_method := getattr(value, "_as_var", None)) is not None + and callable(as_var_method) + and isinstance((resulting_var := as_var_method()), Var) + ): + return resulting_var + + from reflex_core.event import EventHandler + from reflex_core.utils.format import get_event_handler_parts + + if isinstance(value, EventHandler): + return Var(_js_expr=".".join(filter(None, get_event_handler_parts(value)))) + + serialized_value = serializers.serialize(value) + if serialized_value is not None: + if isinstance(serialized_value, Mapping): + return LiteralObjectVar.create( + serialized_value, + _var_type=type(value), + _var_data=_var_data, + ) + if isinstance(serialized_value, str): + return LiteralStringVar.create( + serialized_value, _var_type=type(value), _var_data=_var_data + ) + return LiteralVar.create(serialized_value, _var_data=_var_data) + + if dataclasses.is_dataclass(value) and not isinstance(value, type): + return LiteralObjectVar.create( + { + k.name: (None if callable(v := getattr(value, k.name)) else v) + for k in dataclasses.fields(value) + }, + _var_type=type(value), + _var_data=_var_data, + ) + + if isinstance(value, range): + return ArrayVar.range(value.start, value.stop, value.step) + + msg = f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value}." + raise TypeError(msg) + + if not TYPE_CHECKING: + create = _create_literal_var + + def __post_init__(self): + """Post-initialize the var.""" + + @classmethod + def _get_all_var_data_without_creating_var( + cls, + value: Any, + ) -> VarData | None: + return cls.create(value)._get_all_var_data() + + @classmethod + def _get_all_var_data_without_creating_var_dispatch( + cls, + value: Any, + ) -> VarData | None: + """Get all the var data without creating a var. + + Args: + value: The value to get the var data from. + + Returns: + The var data or None. + + Raises: + TypeError: If the value is not a supported type for LiteralVar. + """ + from .object import LiteralObjectVar + from .sequence import LiteralStringVar + + if isinstance(value, Var): + return value._get_all_var_data() + + for literal_subclass, var_subclass in _var_literal_subclasses[::-1]: + if isinstance(value, var_subclass.python_types): + return literal_subclass._get_all_var_data_without_creating_var(value) + + if ( + (as_var_method := getattr(value, "_as_var", None)) is not None + and callable(as_var_method) + and isinstance((resulting_var := as_var_method()), Var) + ): + return resulting_var._get_all_var_data() + + from reflex_core.event import EventHandler + from reflex_core.utils.format import get_event_handler_parts + + if isinstance(value, EventHandler): + return Var( + _js_expr=".".join(filter(None, get_event_handler_parts(value))) + )._get_all_var_data() + + serialized_value = serializers.serialize(value) + if serialized_value is not None: + if isinstance(serialized_value, Mapping): + return LiteralObjectVar._get_all_var_data_without_creating_var( + serialized_value + ) + if isinstance(serialized_value, str): + return LiteralStringVar._get_all_var_data_without_creating_var( + serialized_value + ) + return LiteralVar._get_all_var_data_without_creating_var_dispatch( + serialized_value + ) + + if dataclasses.is_dataclass(value) and not isinstance(value, type): + return LiteralObjectVar._get_all_var_data_without_creating_var({ + k.name: (None if callable(v := getattr(value, k.name)) else v) + for k in dataclasses.fields(value) + }) + + if isinstance(value, range): + return None + + msg = f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value}." + raise TypeError(msg) + + @property + def _var_value(self) -> Any: + msg = "LiteralVar subclasses must implement the _var_value property." + raise NotImplementedError(msg) + + def json(self) -> str: + """Serialize the var to a JSON string. + + Raises: + NotImplementedError: If the method is not implemented. + """ + msg = "LiteralVar subclasses must implement the json method." + raise NotImplementedError(msg) + + +@serializers.serializer +def serialize_literal(value: LiteralVar): + """Serialize a Literal type. + + Args: + value: The Literal to serialize. + + Returns: + The serialized Literal. + """ + return value._var_value + + +def get_python_literal(value: LiteralVar | Any) -> Any | None: + """Get the Python literal value. + + Args: + value: The value to get the Python literal value of. + + Returns: + The Python literal value. + """ + if isinstance(value, LiteralVar): + return value._var_value + if isinstance(value, Var): + return None + return value + + +P = ParamSpec("P") +T = TypeVar("T") + + +# NoReturn is used to match CustomVarOperationReturn with no type hint. +@overload +def var_operation( # pyright: ignore [reportOverlappingOverload] + func: Callable[P, CustomVarOperationReturn[NoReturn]], +) -> Callable[P, Var]: ... + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[None]], +) -> Callable[P, NoneVar]: ... + + +@overload +def var_operation( # pyright: ignore [reportOverlappingOverload] + func: Callable[P, CustomVarOperationReturn[bool]] + | Callable[P, CustomVarOperationReturn[bool | None]], +) -> Callable[P, BooleanVar]: ... + + +NUMBER_T = TypeVar("NUMBER_T", int, float, int | float) + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[NUMBER_T]] + | Callable[P, CustomVarOperationReturn[NUMBER_T | None]], +) -> Callable[P, NumberVar[NUMBER_T]]: ... + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[str]] + | Callable[P, CustomVarOperationReturn[str | None]], +) -> Callable[P, StringVar]: ... + + +LIST_T = TypeVar("LIST_T", bound=Sequence) + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[LIST_T]] + | Callable[P, CustomVarOperationReturn[LIST_T | None]], +) -> Callable[P, ArrayVar[LIST_T]]: ... + + +OBJECT_TYPE = TypeVar("OBJECT_TYPE", bound=Mapping) + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[OBJECT_TYPE]] + | Callable[P, CustomVarOperationReturn[OBJECT_TYPE | None]], +) -> Callable[P, ObjectVar[OBJECT_TYPE]]: ... + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[T]] + | Callable[P, CustomVarOperationReturn[T | None]], +) -> Callable[P, Var[T]]: ... + + +def var_operation( # pyright: ignore [reportInconsistentOverload] + func: Callable[P, CustomVarOperationReturn[T]], +) -> Callable[P, Var[T]]: + """Decorator for creating a var operation. + + Example: + ```python + @var_operation + def add(a: NumberVar, b: NumberVar): + return custom_var_operation(f"{a} + {b}") + ``` + + Args: + func: The function to decorate. + + Returns: + The decorated function. + """ + func_args = list(inspect.signature(func).parameters) + + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> Var[T]: + args_vars = { + func_args[i]: (LiteralVar.create(arg) if not isinstance(arg, Var) else arg) + for i, arg in enumerate(args) + } + kwargs_vars = { + key: LiteralVar.create(value) if not isinstance(value, Var) else value + for key, value in kwargs.items() + } + + return CustomVarOperation.create( + name=func.__name__, + args=tuple(list(args_vars.items()) + list(kwargs_vars.items())), + return_var=func(*args_vars.values(), **kwargs_vars), # pyright: ignore [reportCallIssue, reportReturnType] + ).guess_type() + + return wrapper + + +def figure_out_type(value: Any) -> types.GenericType: + """Figure out the type of the value. + + Args: + value: The value to figure out the type of. + + Returns: + The type of the value. + """ + if isinstance(value, (list, set, tuple, Mapping, Var)): + if isinstance(value, Var): + return value._var_type + if has_args(value_type := type(value)): + return value_type + if isinstance(value, list): + if not value: + return Sequence[NoReturn] + return Sequence[unionize(*{figure_out_type(v) for v in value[:100]})] + if isinstance(value, set): + return set[unionize(*{figure_out_type(v) for v in value})] + if isinstance(value, tuple): + if not value: + return tuple[NoReturn, ...] + if len(value) <= 5: + return tuple[tuple(figure_out_type(v) for v in value)] + return tuple[unionize(*{figure_out_type(v) for v in value[:100]}), ...] + if isinstance(value, Mapping): + if not value: + return Mapping[NoReturn, NoReturn] + return Mapping[ + unionize(*{figure_out_type(k) for k in list(value.keys())[:100]}), + unionize(*{figure_out_type(v) for v in list(value.values())[:100]}), + ] + return type(value) + + +GLOBAL_CACHE = {} + + +class cached_property: # noqa: N801 + """A cached property that caches the result of the function.""" + + def __init__(self, func: Callable): + """Initialize the cached_property. + + Args: + func: The function to cache. + """ + self._func = func + self._attrname = None + + def __set_name__(self, owner: Any, name: str): + """Set the name of the cached property. + + Args: + owner: The owner of the cached property. + name: The name of the cached property. + + Raises: + TypeError: If the cached property is assigned to two different names. + """ + if self._attrname is None: + self._attrname = name + + original_del = getattr(owner, "__del__", None) + + def delete_property(this: Any): + """Delete the cached property. + + Args: + this: The object to delete the cached property from. + """ + cached_field_name = "_reflex_cache_" + name + try: + unique_id = object.__getattribute__(this, cached_field_name) + except AttributeError: + if original_del is not None: + original_del(this) + return + GLOBAL_CACHE.pop(unique_id, None) + + if original_del is not None: + original_del(this) + + owner.__del__ = delete_property + + elif name != self._attrname: + msg = ( + "Cannot assign the same cached_property to two different names " + f"({self._attrname!r} and {name!r})." + ) + raise TypeError(msg) + + def __get__(self, instance: Any, owner: type | None = None): + """Get the cached property. + + Args: + instance: The instance to get the cached property from. + owner: The owner of the cached property. + + Returns: + The cached property. + + Raises: + TypeError: If the class does not have __set_name__. + """ + if self._attrname is None: + msg = "Cannot use cached_property on a class without __set_name__." + raise TypeError(msg) + cached_field_name = "_reflex_cache_" + self._attrname + try: + unique_id = object.__getattribute__(instance, cached_field_name) + except AttributeError: + unique_id = uuid.uuid4().int + object.__setattr__(instance, cached_field_name, unique_id) + if unique_id not in GLOBAL_CACHE: + GLOBAL_CACHE[unique_id] = self._func(instance) + return GLOBAL_CACHE[unique_id] + + +cached_property_no_lock = cached_property + + +class VarProtocol(Protocol): + """A protocol for Var.""" + + __dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]] + + @property + def _js_expr(self) -> str: ... + + @property + def _var_type(self) -> types.GenericType: ... + + @property + def _var_data(self) -> VarData: ... + + +class CachedVarOperation: + """Base class for cached var operations to lower boilerplate code.""" + + def __post_init__(self): + """Post-initialize the CachedVarOperation.""" + object.__delattr__(self, "_js_expr") + + def __getattr__(self, name: str) -> Any: + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute. + """ + if name == "_js_expr": + return self._cached_var_name + + parent_classes = inspect.getmro(type(self)) + + next_class = parent_classes[parent_classes.index(CachedVarOperation) + 1] + + return next_class.__getattr__(self, name) + + def _get_all_var_data(self) -> VarData | None: + """Get all VarData associated with the Var. + + Returns: + The VarData of the components and all of its children. + """ + return self._cached_get_all_var_data + + @cached_property_no_lock + def _cached_get_all_var_data(self: VarProtocol) -> VarData | None: + """Get the cached VarData. + + Returns: + The cached VarData. + """ + return VarData.merge( + *( + value._get_all_var_data() if isinstance(value, Var) else None + for value in ( + getattr(self, field.name) for field in dataclasses.fields(self) + ) + ), + self._var_data, + ) + + def __hash__(self: DataclassInstance) -> int: + """Calculate the hash of the object. + + Returns: + The hash of the object. + """ + return hash(( + type(self).__name__, + *[ + getattr(self, field.name) + for field in dataclasses.fields(self) + if field.name not in ["_js_expr", "_var_data", "_var_type"] + ], + )) + + +def and_operation( + a: Var[VAR_TYPE] | Any, b: Var[OTHER_VAR_TYPE] | Any +) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: + """Perform a logical AND operation on two variables. + + Args: + a: The first variable. + b: The second variable. + + Returns: + The result of the logical AND operation. + """ + return _and_operation(a, b) + + +@var_operation +def _and_operation(a: Var, b: Var): + """Perform a logical AND operation on two variables. + + Args: + a: The first variable. + b: The second variable. + + Returns: + The result of the logical AND operation. + """ + return var_operation_return( + js_expression=f"({a} && {b})", + var_type=unionize(a._var_type, b._var_type), + ) + + +def or_operation( + a: Var[VAR_TYPE] | Any, b: Var[OTHER_VAR_TYPE] | Any +) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: + """Perform a logical OR operation on two variables. + + Args: + a: The first variable. + b: The second variable. + + Returns: + The result of the logical OR operation. + """ + return _or_operation(a, b) + + +@var_operation +def _or_operation(a: Var, b: Var): + """Perform a logical OR operation on two variables. + + Args: + a: The first variable. + b: The second variable. + + Returns: + The result of the logical OR operation. + """ + return var_operation_return( + js_expression=f"({a} || {b})", + var_type=unionize(a._var_type, b._var_type), + ) + + +RETURN_TYPE = TypeVar("RETURN_TYPE") + +DICT_KEY = TypeVar("DICT_KEY") +DICT_VAL = TypeVar("DICT_VAL") + +LIST_INSIDE = TypeVar("LIST_INSIDE") + + +class FakeComputedVarBaseClass(property): + """A fake base class for ComputedVar to avoid inheriting from property.""" + + __pydantic_run_validation__ = False + + +def is_computed_var(obj: Any) -> TypeGuard[ComputedVar]: + """Check if the object is a ComputedVar. + + Args: + obj: The object to check. + + Returns: + Whether the object is a ComputedVar. + """ + return isinstance(obj, FakeComputedVarBaseClass) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class ComputedVar(Var[RETURN_TYPE]): + """A field with computed getters.""" + + # Whether to track dependencies and cache computed values + _cache: bool = dataclasses.field(default=False) + + # Whether the computed var is a backend var + _backend: bool = dataclasses.field(default=False) + + # The initial value of the computed var + _initial_value: RETURN_TYPE | types.Unset = dataclasses.field(default=types.Unset()) + + # Explicit var dependencies to track + _static_deps: dict[str | None, set[str]] = dataclasses.field(default_factory=dict) + + # 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: datetime.timedelta | None = dataclasses.field(default=None) + + _fget: Callable[[BaseState], RETURN_TYPE] = dataclasses.field( + default_factory=lambda: lambda _: None + ) # pyright: ignore [reportAssignmentType] + + _name: str = dataclasses.field(default="") + + def __init__( + self, + fget: Callable[[BASE_STATE], RETURN_TYPE], + initial_value: RETURN_TYPE | types.Unset = types.Unset(), + cache: bool = True, + deps: list[str | Var] | None = None, + auto_deps: bool = True, + interval: int | datetime.timedelta | None = None, + backend: bool | None = None, + **kwargs, + ): + """Initialize a ComputedVar. + + Args: + 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. + backend: Whether the computed var is a backend var. + **kwargs: additional attributes to set on the instance + + Raises: + TypeError: If the computed var dependencies are not Var instances or var names. + UntypedComputedVarError: If the computed var is untyped. + """ + hint = kwargs.pop("return_type", None) or get_type_hints(fget).get( + "return", Any + ) + + if hint is Any: + raise UntypedComputedVarError(var_name=fget.__name__) + is_using_fget_name = "_js_expr" not in kwargs + js_expr = kwargs.pop("_js_expr", fget.__name__ + FIELD_MARKER) + kwargs.setdefault("_var_type", hint) + + Var.__init__( + self, + _js_expr=js_expr, + _var_type=kwargs.pop("_var_type"), + _var_data=kwargs.pop( + "_var_data", + VarData(field_name=fget.__name__) if is_using_fget_name else None, + ), + ) + + if kwargs: + msg = f"Unexpected keyword arguments: {tuple(kwargs)}" + raise TypeError(msg) + + if backend is None: + backend = fget.__name__.startswith("_") + + object.__setattr__(self, "_backend", backend) + object.__setattr__(self, "_initial_value", initial_value) + object.__setattr__(self, "_cache", cache) + object.__setattr__(self, "_name", fget.__name__) + + if isinstance(interval, int): + interval = datetime.timedelta(seconds=interval) + + object.__setattr__(self, "_update_interval", interval) + + object.__setattr__( + self, + "_static_deps", + self._calculate_static_deps(deps), + ) + object.__setattr__(self, "_auto_deps", auto_deps) + + object.__setattr__(self, "_fget", fget) + + def _calculate_static_deps( + self, + deps: list[str | Var] | dict[str | None, set[str]] | None = None, + ) -> dict[str | None, set[str]]: + """Calculate the static dependencies of the computed var from user input or existing dependencies. + + Args: + deps: The user input dependencies or existing dependencies. + + Returns: + The static dependencies. + """ + if isinstance(deps, dict): + # Assume a dict is coming from _replace, so no special processing. + return deps + static_deps = {} + if deps is not None: + for dep in deps: + static_deps = self._add_static_dep(dep, static_deps) + return static_deps + + def _add_static_dep( + self, dep: str | Var, deps: dict[str | None, set[str]] | None = None + ) -> dict[str | None, set[str]]: + """Add a static dependency to the computed var or existing dependency set. + + Args: + dep: The dependency to add. + deps: The existing dependency set. + + Returns: + The updated dependency set. + + Raises: + TypeError: If the computed var dependencies are not Var instances or var names. + """ + if deps is None: + deps = self._static_deps + if isinstance(dep, Var): + state_name = ( + all_var_data.state + if (all_var_data := dep._get_all_var_data()) and all_var_data.state + else None + ) + if all_var_data is not None: + var_name = all_var_data.field_name + else: + var_name = dep._js_expr + deps.setdefault(state_name, set()).add(var_name) + elif isinstance(dep, str) and dep != "": + deps.setdefault(None, set()).add(dep) + else: + msg = "ComputedVar dependencies must be Var instances or var names (non-empty strings)." + raise TypeError(msg) + return deps + + @override + def _replace( + self, + merge_var_data: VarData | None = None, + **kwargs: Any, + ) -> Self: + """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. + + Raises: + TypeError: If kwargs contains keys that are not allowed. + """ + if "deps" in kwargs: + kwargs["deps"] = self._calculate_static_deps(kwargs["deps"]) + field_values = { + "fget": kwargs.pop("fget", self._fget), + "initial_value": kwargs.pop("initial_value", self._initial_value), + "cache": kwargs.pop("cache", self._cache), + "deps": kwargs.pop("deps", copy.copy(self._static_deps)), + "auto_deps": kwargs.pop("auto_deps", self._auto_deps), + "interval": kwargs.pop("interval", self._update_interval), + "backend": kwargs.pop("backend", self._backend), + "_js_expr": kwargs.pop("_js_expr", self._js_expr), + "_var_type": kwargs.pop("_var_type", self._var_type), + "_var_data": kwargs.pop( + "_var_data", VarData.merge(self._var_data, merge_var_data) + ), + "return_type": kwargs.pop("return_type", self._var_type), + } + + if kwargs: + unexpected_kwargs = ", ".join(kwargs.keys()) + msg = f"Unexpected keyword arguments: {unexpected_kwargs}" + raise TypeError(msg) + + return type(self)(**field_values) + + @property + def _cache_attr(self) -> str: + """Get the attribute used to cache the value on the instance. + + Returns: + An attribute name. + """ + return f"__cached_{self._js_expr}" + + @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._js_expr}" + + 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 + + @overload + def __get__( + self: ComputedVar[bool], + instance: None, + owner: type, + ) -> BooleanVar: ... + + @overload + def __get__( + self: ComputedVar[int] | ComputedVar[float], + instance: None, + owner: type, + ) -> NumberVar: ... + + @overload + def __get__( + self: ComputedVar[str], + instance: None, + owner: type, + ) -> StringVar: ... + + @overload + def __get__( + self: ComputedVar[MAPPING_TYPE], + instance: None, + owner: type, + ) -> ObjectVar[MAPPING_TYPE]: ... + + @overload + def __get__( + self: ComputedVar[list[LIST_INSIDE]], + instance: None, + owner: type, + ) -> ArrayVar[list[LIST_INSIDE]]: ... + + @overload + def __get__( + self: ComputedVar[tuple[LIST_INSIDE, ...]], + instance: None, + owner: type, + ) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ... + + @overload + def __get__( + self: ComputedVar[SQLA_TYPE], + instance: None, + owner: type, + ) -> ObjectVar[SQLA_TYPE]: ... + + if TYPE_CHECKING: + + @overload + def __get__( + self: ComputedVar[DATACLASS_TYPE], instance: None, owner: Any + ) -> ObjectVar[DATACLASS_TYPE]: ... + + @overload + def __get__(self, instance: None, owner: type) -> ComputedVar[RETURN_TYPE]: ... + + @overload + def __get__(self, instance: BaseState, owner: type) -> RETURN_TYPE: ... + + def __get__(self, instance: BaseState | None, owner: type): + """Get the ComputedVar value. + + If the value is already cached on the instance, return the cached value. + + Args: + instance: the instance of the class accessing this computed var. + owner: the class that this descriptor is attached to. + + Returns: + The value of the var for the given instance. + """ + if instance is None: + state_where_defined = owner + while self._name in state_where_defined.inherited_vars: + state_where_defined = state_where_defined.get_parent_state() + + field_name = ( + format_state_name(state_where_defined.get_full_name()) + + "." + + self._js_expr + ) + + return dispatch( + field_name, + var_data=VarData.from_state(state_where_defined, self._name), + result_var_type=self._var_type, + existing_var=self, + ) + + if not self._cache: + value = self.fget(instance) + else: + # handle caching + if not hasattr(instance, self._cache_attr) or self.needs_update(instance): + # Set cache attr on state instance. + setattr(instance, self._cache_attr, self.fget(instance)) + # 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()) + value = getattr(instance, self._cache_attr) + + self._check_deprecated_return_type(instance, value) + + return value + + def _check_deprecated_return_type(self, instance: BaseState, value: Any) -> None: + if not _isinstance(value, self._var_type, nested=1, treat_var_as_type=False): + console.error( + f"Computed var '{type(instance).__name__}.{self._name}' must return" + f" a value of type '{escape(str(self._var_type))}', got '{value!s}' of type {type(value)}." + ) + + def _deps( + self, + objclass: type[BaseState], + obj: FunctionType | CodeType | None = None, + ) -> dict[str, set[str]]: + """Determine var dependencies of this ComputedVar. + + Save references to attributes accessed on "self" or other fetched states. + + Recursively called when the function makes a method call on "self" or + define comprehensions or nested functions that may reference "self". + + Args: + objclass: the class obj this ComputedVar is attached to. + obj: the object to disassemble (defaults to the fget function). + + Returns: + A dictionary mapping state names to the set of variable names + accessed by the given obj. + """ + from .dep_tracking import DependencyTracker + + d = {} + if self._static_deps: + d.update(self._static_deps) + # None is a placeholder for the current state class. + if None in d: + d[objclass.get_full_name()] = d.pop(None) + + if not self._auto_deps: + return d + + if obj is None: + fget = self._fget + if fget is not None: + obj = cast(FunctionType, fget) + else: + return d + + try: + return DependencyTracker( + func=obj, state_cls=objclass, dependencies=d + ).dependencies + except Exception as e: + console.warn( + "Failed to automatically determine dependencies for computed var " + f"{objclass.__name__}.{self._name}: {e}. " + "Set auto_deps=False and provide accurate deps=['var1', 'var2'] to suppress this warning." + ) + return d + + def mark_dirty(self, instance: BaseState) -> None: + """Mark this ComputedVar as dirty. + + Args: + instance: the state instance that needs to recompute the value. + """ + with contextlib.suppress(AttributeError): + delattr(instance, self._cache_attr) + + def add_dependency(self, objclass: type[BaseState], dep: Var): + """Explicitly add a dependency to the ComputedVar. + + After adding the dependency, when the `dep` changes, this computed var + will be marked dirty. + + Args: + objclass: The class obj this ComputedVar is attached to. + dep: The dependency to add. + + Raises: + VarDependencyError: If the dependency is not a Var instance with a + state and field name + """ + if all_var_data := dep._get_all_var_data(): + state_name = all_var_data.state + if state_name: + var_name = all_var_data.field_name + if var_name: + self._static_deps.setdefault(state_name, set()).add(var_name) + target_state_class = objclass.get_root_state().get_class_substate( + state_name + ) + target_state_class._var_dependencies.setdefault( + var_name, set() + ).add(( + objclass.get_full_name(), + self._name, + )) + target_state_class._potentially_dirty_states.add( + objclass.get_full_name() + ) + return + msg = ( + "ComputedVar dependencies must be Var instances with a state and " + f"field name, got {dep!r}." + ) + raise VarDependencyError(msg) + + def _determine_var_type(self) -> type: + """Get the type of the var. + + Returns: + The type of the var. + """ + hints = get_type_hints(self._fget) + if "return" in hints: + return hints["return"] + return Any # pyright: ignore [reportReturnType] + + @property + def __class__(self) -> type: + """Get the class of the var. + + Returns: + The class of the var. + """ + return FakeComputedVarBaseClass + + @property + def fget(self) -> Callable[[BaseState], RETURN_TYPE]: + """Get the getter function. + + Returns: + The getter function. + """ + return self._fget + + +class DynamicRouteVar(ComputedVar[str | list[str]]): + """A ComputedVar that represents a dynamic route.""" + + +async def _default_async_computed_var(_self: BaseState) -> Any: # noqa: RUF029 + return None + + +@dataclasses.dataclass( + eq=False, + frozen=True, + init=False, + slots=True, +) +class AsyncComputedVar(ComputedVar[RETURN_TYPE]): + """A computed var that wraps a coroutinefunction.""" + + _fget: Callable[[BaseState], Coroutine[None, None, RETURN_TYPE]] = ( + dataclasses.field(default=_default_async_computed_var) + ) + + @overload + def __get__( + self: AsyncComputedVar[bool], + instance: None, + owner: type, + ) -> BooleanVar: ... + + @overload + def __get__( + self: AsyncComputedVar[int] | ComputedVar[float], + instance: None, + owner: type, + ) -> NumberVar: ... + + @overload + def __get__( + self: AsyncComputedVar[str], + instance: None, + owner: type, + ) -> StringVar: ... + + @overload + def __get__( + self: AsyncComputedVar[MAPPING_TYPE], + instance: None, + owner: type, + ) -> ObjectVar[MAPPING_TYPE]: ... + + @overload + def __get__( + self: AsyncComputedVar[list[LIST_INSIDE]], + instance: None, + owner: type, + ) -> ArrayVar[list[LIST_INSIDE]]: ... + + @overload + def __get__( + self: AsyncComputedVar[tuple[LIST_INSIDE, ...]], + instance: None, + owner: type, + ) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ... + + @overload + def __get__( + self: AsyncComputedVar[SQLA_TYPE], + instance: None, + owner: type, + ) -> ObjectVar[SQLA_TYPE]: ... + + if TYPE_CHECKING: + + @overload + def __get__( + self: AsyncComputedVar[DATACLASS_TYPE], instance: None, owner: Any + ) -> ObjectVar[DATACLASS_TYPE]: ... + + @overload + def __get__(self, instance: None, owner: type) -> AsyncComputedVar[RETURN_TYPE]: ... + + @overload + def __get__( + self, instance: BaseState, owner: type + ) -> Coroutine[None, None, RETURN_TYPE]: ... + + def __get__( + self, instance: BaseState | None, owner + ) -> Var | Coroutine[None, None, RETURN_TYPE]: + """Get the ComputedVar value. + + If the value is already cached on the instance, return the cached value. + + Args: + instance: the instance of the class accessing this computed var. + owner: the class that this descriptor is attached to. + + Returns: + The value of the var for the given instance. + """ + if instance is None: + return super(AsyncComputedVar, self).__get__(instance, owner) + + if not self._cache: + + async def _awaitable_result(instance: BaseState = instance) -> RETURN_TYPE: + value = await self.fget(instance) + self._check_deprecated_return_type(instance, value) + return value + + return _awaitable_result() + + # handle caching + async def _awaitable_result(instance: BaseState = instance) -> RETURN_TYPE: + if not hasattr(instance, self._cache_attr) or self.needs_update(instance): + # Set cache attr on state instance. + setattr(instance, self._cache_attr, await self.fget(instance)) + # 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()) + value = getattr(instance, self._cache_attr) + self._check_deprecated_return_type(instance, value) + return value + + return _awaitable_result() + + @property + def fget(self) -> Callable[[BaseState], Coroutine[None, None, RETURN_TYPE]]: + """Get the getter function. + + Returns: + The getter function. + """ + return self._fget + + +if TYPE_CHECKING: + BASE_STATE = TypeVar("BASE_STATE", bound=BaseState) + + +class _ComputedVarDecorator(Protocol): + """A protocol for the ComputedVar decorator.""" + + @overload + def __call__( + self, + fget: Callable[[BASE_STATE], Coroutine[Any, Any, RETURN_TYPE]], + ) -> AsyncComputedVar[RETURN_TYPE]: ... + + @overload + def __call__( + self, + fget: Callable[[BASE_STATE], RETURN_TYPE], + ) -> ComputedVar[RETURN_TYPE]: ... + + def __call__( + self, + fget: Callable[[BASE_STATE], Any], + ) -> ComputedVar[Any]: ... + + +@overload +def computed_var( + fget: None = None, + initial_value: Any | types.Unset = types.Unset(), + cache: bool = True, + deps: list[str | Var] | None = None, + auto_deps: bool = True, + interval: datetime.timedelta | int | None = None, + backend: bool | None = None, + **kwargs, +) -> _ComputedVarDecorator: ... + + +@overload +def computed_var( + fget: Callable[[BASE_STATE], Coroutine[Any, Any, RETURN_TYPE]], + initial_value: RETURN_TYPE | types.Unset = types.Unset(), + cache: bool = True, + deps: list[str | Var] | None = None, + auto_deps: bool = True, + interval: datetime.timedelta | int | None = None, + backend: bool | None = None, + **kwargs, +) -> AsyncComputedVar[RETURN_TYPE]: ... + + +@overload +def computed_var( + fget: Callable[[BASE_STATE], RETURN_TYPE], + initial_value: RETURN_TYPE | types.Unset = types.Unset(), + cache: bool = True, + deps: list[str | Var] | None = None, + auto_deps: bool = True, + interval: datetime.timedelta | int | None = None, + backend: bool | None = None, + **kwargs, +) -> ComputedVar[RETURN_TYPE]: ... + + +def computed_var( + fget: Callable[[BASE_STATE], Any] | None = None, + initial_value: Any | types.Unset = types.Unset(), + cache: bool = True, + deps: list[str | Var] | None = None, + auto_deps: bool = True, + interval: datetime.timedelta | int | None = None, + backend: bool | None = None, + **kwargs, +) -> ComputedVar | Callable[[Callable[[BASE_STATE], Any]], ComputedVar]: + """A ComputedVar decorator with or without kwargs. + + Args: + 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. + backend: Whether the computed var is a backend var. + **kwargs: additional attributes to set on the instance + + Returns: + A ComputedVar instance. + + Raises: + ValueError: If caching is disabled and an update interval is set. + VarDependencyError: If user supplies dependencies without caching. + ComputedVarSignatureError: If the getter function has more than one argument. + """ + if cache is False and interval is not None: + msg = "Cannot set update interval without caching." + raise ValueError(msg) + + if cache is False and (deps is not None or auto_deps is False): + msg = "Cannot track dependencies without caching." + raise VarDependencyError(msg) + + if fget is not None: + sign = inspect.signature(fget) + if len(sign.parameters) != 1: + raise ComputedVarSignatureError(fget.__name__, signature=str(sign)) + + if inspect.iscoroutinefunction(fget): + computed_var_cls = AsyncComputedVar + else: + computed_var_cls = ComputedVar + return computed_var_cls( + fget, + initial_value=initial_value, + cache=cache, + deps=deps, + auto_deps=auto_deps, + interval=interval, + backend=backend, + **kwargs, + ) + + def wrapper(fget: Callable[[BASE_STATE], Any]) -> ComputedVar: + if inspect.iscoroutinefunction(fget): + computed_var_cls = AsyncComputedVar + else: + computed_var_cls = ComputedVar + return computed_var_cls( + fget, + initial_value=initial_value, + cache=cache, + deps=deps, + auto_deps=auto_deps, + interval=interval, + backend=backend, + **kwargs, + ) + + return wrapper + + +RETURN = TypeVar("RETURN") + + +class CustomVarOperationReturn(Var[RETURN]): + """Base class for custom var operations.""" + + @classmethod + def create( + cls, + js_expression: str, + _var_type: type[RETURN] | None = None, + _var_data: VarData | None = None, + ) -> CustomVarOperationReturn[RETURN]: + """Create a CustomVarOperation. + + Args: + js_expression: The JavaScript expression to evaluate. + _var_type: The type of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The CustomVarOperation. + """ + return CustomVarOperationReturn( + _js_expr=js_expression, + _var_type=_var_type or Any, + _var_data=_var_data, + ) + + +def var_operation_return( + js_expression: str, + var_type: type[RETURN] | GenericType | None = None, + var_data: VarData | None = None, +) -> CustomVarOperationReturn[RETURN]: + """Shortcut for creating a CustomVarOperationReturn. + + Args: + js_expression: The JavaScript expression to evaluate. + var_type: The type of the var. + var_data: Additional hooks and imports associated with the Var. + + Returns: + The CustomVarOperationReturn. + """ + return CustomVarOperationReturn.create( + js_expression, + var_type, + var_data, + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class CustomVarOperation(CachedVarOperation, Var[T]): + """Base class for custom var operations.""" + + _name: str = dataclasses.field(default="") + + _args: tuple[tuple[str, Var], ...] = dataclasses.field(default_factory=tuple) + + _return: CustomVarOperationReturn[T] = dataclasses.field( + default_factory=lambda: CustomVarOperationReturn.create("") + ) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """Get the cached var name. + + Returns: + The cached var name. + """ + return str(self._return) + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> VarData | None: + """Get the cached VarData. + + Returns: + The cached VarData. + """ + return VarData.merge( + *(arg[1]._get_all_var_data() for arg in self._args), + self._return._get_all_var_data(), + self._var_data, + ) + + @classmethod + def create( + cls, + name: str, + args: tuple[tuple[str, Var], ...], + return_var: CustomVarOperationReturn[T], + _var_data: VarData | None = None, + ) -> CustomVarOperation[T]: + """Create a CustomVarOperation. + + Args: + name: The name of the operation. + args: The arguments to the operation. + return_var: The return var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The CustomVarOperation. + """ + return CustomVarOperation( + _js_expr="", + _var_type=return_var._var_type, + _var_data=_var_data, + _name=name, + _args=args, + _return=return_var, + ) + + +class NoneVar(Var[None], python_types=type(None)): + """A var representing None.""" + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralNoneVar(LiteralVar, NoneVar): + """A var representing None.""" + + _var_value: None = None + + def json(self) -> str: + """Serialize the var to a JSON string. + + Returns: + The JSON string. + """ + return "null" + + @classmethod + def _get_all_var_data_without_creating_var(cls, value: None) -> VarData | None: + return None + + @classmethod + def create( + cls, + value: None = None, + _var_data: VarData | None = None, + ) -> LiteralNoneVar: + """Create a var from a value. + + Args: + value: The value of the var. Must be None. Existed for compatibility with LiteralVar. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + return LiteralNoneVar( + _js_expr="null", + _var_type=None, + _var_data=_var_data, + ) + + +def get_to_operation(var_subclass: type[Var]) -> type[ToOperation]: + """Get the ToOperation class for a given Var subclass. + + Args: + var_subclass: The Var subclass. + + Returns: + The ToOperation class. + + Raises: + ValueError: If the ToOperation class cannot be found. + """ + possible_classes = [ + saved_var_subclass.to_var_subclass + for saved_var_subclass in _var_subclasses + if saved_var_subclass.var_subclass is var_subclass + ] + if not possible_classes: + msg = f"Could not find ToOperation for {var_subclass}." + raise ValueError(msg) + return possible_classes[0] + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class StateOperation(CachedVarOperation, Var): + """A var operation that accesses a field on an object.""" + + _state_name: str = dataclasses.field(default="") + _field: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create()) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """Get the cached var name. + + Returns: + The cached var name. + """ + return f"{self._state_name!s}.{self._field!s}" + + def __getattr__(self, name: str) -> Any: + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute. + """ + if name == "_js_expr": + return self._cached_var_name + + return getattr(self._field, name) + + @classmethod + def create( + cls, + state_name: str, + field: Var, + _var_data: VarData | None = None, + ) -> StateOperation: + """Create a DotOperation. + + Args: + state_name: The name of the state. + field: The field of the state. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The DotOperation. + """ + return StateOperation( + _js_expr="", + _var_type=field._var_type, + _var_data=_var_data, + _state_name=state_name, + _field=field, + ) + + +def get_uuid_string_var() -> Var: + """Return a Var that generates a single memoized UUID via .web/utils/state.js. + + useMemo with an empty dependency array ensures that the generated UUID is + consistent across re-renders of the component. + + Returns: + A Var that generates a UUID at runtime. + """ + from reflex_core.utils.imports import ImportVar + from reflex_core.vars import Var + + unique_uuid_var = get_unique_variable_name() + unique_uuid_var_data = VarData( + imports={ + f"$/{constants.Dirs.STATE_PATH}": ImportVar(tag="generateUUID"), + "react": "useMemo", + }, + hooks={f"const {unique_uuid_var} = useMemo(generateUUID, [])": None}, + ) + + return Var( + _js_expr=unique_uuid_var, + _var_type=str, + _var_data=unique_uuid_var_data, + ) + + +# Set of unique variable names. +USED_VARIABLES = set() + + +@once +def _rng(): + import random + + return random.Random(42) + + +def get_unique_variable_name() -> str: + """Get a unique variable name. + + Returns: + The unique variable name. + """ + name = "".join([_rng().choice(string.ascii_lowercase) for _ in range(8)]) + if name not in USED_VARIABLES: + USED_VARIABLES.add(name) + return name + return get_unique_variable_name() + + +# Compile regex for finding reflex var tags. +_decode_var_pattern_re = ( + rf"{constants.REFLEX_VAR_OPENING_TAG}(.*?){constants.REFLEX_VAR_CLOSING_TAG}" +) +_decode_var_pattern = re.compile(_decode_var_pattern_re, flags=re.DOTALL) + +# Defined global immutable vars. +_global_vars: dict[int, Var] = {} + + +dispatchers: dict[GenericType, Callable[[Var], Var]] = {} + + +def transform(fn: Callable[[Var], Var]) -> Callable[[Var], Var]: + """Register a function to transform a Var. + + Args: + fn: The function to register. + + Returns: + The decorator. + + Raises: + TypeError: If the return type of the function is not a Var. + TypeError: If the Var return type does not have a generic type. + ValueError: If a function for the generic type is already registered. + """ + types = get_type_hints(fn) + return_type = types["return"] + + origin = get_origin(return_type) + + if origin is not Var: + msg = f"Expected return type of {fn.__name__} to be a Var, got {origin}." + raise TypeError(msg) + + generic_args = get_args(return_type) + + if not generic_args: + msg = f"Expected Var return type of {fn.__name__} to have a generic type." + raise TypeError(msg) + + generic_type = get_origin(generic_args[0]) or generic_args[0] + + if generic_type in dispatchers: + msg = f"Function for {generic_type} already registered." + raise ValueError(msg) + + dispatchers[generic_type] = fn + + return fn + + +def dispatch( + field_name: str, + var_data: VarData, + result_var_type: GenericType, + existing_var: Var | None = None, +) -> Var: + """Dispatch a Var to the appropriate transformation function. + + Args: + field_name: The name of the field. + var_data: The VarData associated with the Var. + result_var_type: The type of the Var. + existing_var: The existing Var to transform. Optional. + + Returns: + The transformed Var. + + Raises: + TypeError: If the return type of the function is not a Var. + TypeError: If the Var return type does not have a generic type. + TypeError: If the first argument of the function is not a Var. + TypeError: If the first argument of the function does not have a generic type + """ + result_origin_var_type = get_origin(result_var_type) or result_var_type + + if result_origin_var_type in dispatchers: + fn = dispatchers[result_origin_var_type] + fn_types = get_type_hints(fn) + fn_first_arg_type = fn_types.get( + next(iter(inspect.signature(fn).parameters.values())).name, Any + ) + + fn_return = fn_types.get("return", Any) + + fn_return_origin = get_origin(fn_return) or fn_return + + if fn_return_origin is not Var: + msg = f"Expected return type of {fn.__name__} to be a Var, got {fn_return}." + raise TypeError(msg) + + fn_return_generic_args = get_args(fn_return) + + if not fn_return_generic_args: + msg = f"Expected generic type of {fn_return} to be a type." + raise TypeError(msg) + + arg_origin = get_origin(fn_first_arg_type) or fn_first_arg_type + + if arg_origin is not Var: + msg = f"Expected first argument of {fn.__name__} to be a Var, got {fn_first_arg_type}." + raise TypeError(msg) + + arg_generic_args = get_args(fn_first_arg_type) + + if not arg_generic_args: + msg = f"Expected generic type of {fn_first_arg_type} to be a type." + raise TypeError(msg) + + fn_return_type = fn_return_generic_args[0] + + var = ( + Var( + field_name, + _var_data=var_data, + _var_type=fn_return_type, + ).guess_type() + if existing_var is None + else existing_var._replace( + _var_type=fn_return_type, + _var_data=var_data, + _js_expr=field_name, + ).guess_type() + ) + + return fn(var) + + if existing_var is not None: + return existing_var._replace( + _js_expr=field_name, + _var_data=var_data, + _var_type=result_var_type, + ).guess_type() + return Var( + field_name, + _var_data=var_data, + _var_type=result_var_type, + ).guess_type() + + +if TYPE_CHECKING: + from _typeshed import DataclassInstance + from sqlalchemy.orm import DeclarativeBase + + SQLA_TYPE = TypeVar("SQLA_TYPE", bound=DeclarativeBase | None) + DATACLASS_TYPE = TypeVar("DATACLASS_TYPE", bound=DataclassInstance | None) + MAPPING_TYPE = TypeVar("MAPPING_TYPE", bound=Mapping | None) + V = TypeVar("V") + + +FIELD_TYPE = TypeVar("FIELD_TYPE") + + +class Field(Generic[FIELD_TYPE]): + """A field for a state.""" + + if TYPE_CHECKING: + type_: GenericType + default: FIELD_TYPE | _MISSING_TYPE + default_factory: Callable[[], FIELD_TYPE] | None + + def __init__( + self, + default: FIELD_TYPE | _MISSING_TYPE = MISSING, + default_factory: Callable[[], FIELD_TYPE] | None = None, + is_var: bool = True, + annotated_type: GenericType # pyright: ignore [reportRedeclaration] + | _MISSING_TYPE = MISSING, + ) -> None: + """Initialize the field. + + Args: + default: The default value for the field. + default_factory: The default factory for the field. + is_var: Whether the field is a Var. + annotated_type: The annotated type for the field. + """ + self.default = default + self.default_factory = default_factory + self.is_var = is_var + if annotated_type is not MISSING: + type_origin = get_origin(annotated_type) or annotated_type + if type_origin is Field and ( + args := getattr(annotated_type, "__args__", None) + ): + annotated_type: GenericType = args[0] + type_origin = get_origin(annotated_type) or annotated_type + + if self.default is MISSING and self.default_factory is None: + default_value = types.get_default_value_for_type(annotated_type) + if default_value is None and not types.is_optional(annotated_type): + annotated_type = annotated_type | None + if types.is_immutable(default_value): + self.default = default_value + else: + self.default_factory = functools.partial( + copy.deepcopy, default_value + ) + self.outer_type_ = self.annotated_type = annotated_type + + if type_origin is Annotated: + type_origin = annotated_type.__origin__ # pyright: ignore [reportAttributeAccessIssue] + + self.type_ = self.type_origin = type_origin + else: + self.outer_type_ = self.annotated_type = self.type_ = self.type_origin = Any + + def default_value(self) -> FIELD_TYPE: + """Get the default value for the field. + + Returns: + The default value for the field. + + Raises: + ValueError: If no default value or factory is provided. + """ + if self.default is not MISSING: + return self.default + if self.default_factory is not None: + return self.default_factory() + msg = "No default value or factory provided." + raise ValueError(msg) + + def __repr__(self) -> str: + """Represent the field in a readable format. + + Returns: + The string representation of the field. + """ + annotated_type_str = ( + f", annotated_type={self.annotated_type!r}" + if self.annotated_type is not MISSING + else "" + ) + if self.default is not MISSING: + return f"Field(default={self.default!r}, is_var={self.is_var}{annotated_type_str})" + return f"Field(default_factory={self.default_factory!r}, is_var={self.is_var}{annotated_type_str})" + + if TYPE_CHECKING: + + def __set__(self, instance: Any, value: FIELD_TYPE): + """Set the Var. + + Args: + instance: The instance of the class setting the Var. + value: The value to set the Var to. + + # noqa: DAR101 self + """ + + @overload + def __get__(self: Field[None], instance: None, owner: Any) -> NoneVar: ... + + @overload + def __get__( + self: Field[bool] | Field[bool | None], instance: None, owner: Any + ) -> BooleanVar: ... + + @overload + def __get__( + self: Field[int] | Field[int | None], + instance: None, + owner: Any, + ) -> NumberVar[int]: ... + + @overload + def __get__( + self: Field[float] + | Field[int | float] + | Field[float | None] + | Field[int | float | None], + instance: None, + owner: Any, + ) -> NumberVar: ... + + @overload + def __get__( + self: Field[str] | Field[str | None], instance: None, owner: Any + ) -> StringVar: ... + + @overload + def __get__( + self: Field[list[V]] + | Field[set[V]] + | Field[list[V] | None] + | Field[set[V] | None], + instance: None, + owner: Any, + ) -> ArrayVar[Sequence[V]]: ... + + @overload + def __get__( + self: Field[SEQUENCE_TYPE] | Field[SEQUENCE_TYPE | None], + instance: None, + owner: Any, + ) -> ArrayVar[SEQUENCE_TYPE]: ... + + @overload + def __get__( + self: Field[MAPPING_TYPE] | Field[MAPPING_TYPE | None], + instance: None, + owner: Any, + ) -> ObjectVar[MAPPING_TYPE]: ... + + @overload + def __get__( + self: Field[SQLA_TYPE] | Field[SQLA_TYPE | None], instance: None, owner: Any + ) -> ObjectVar[SQLA_TYPE]: ... + + if TYPE_CHECKING: + + @overload + def __get__( + self: Field[DATACLASS_TYPE] | Field[DATACLASS_TYPE | None], + instance: None, + owner: Any, + ) -> ObjectVar[DATACLASS_TYPE]: ... + + @overload + def __get__(self, instance: None, owner: Any) -> Var[FIELD_TYPE]: ... + + @overload + def __get__(self, instance: Any, owner: Any) -> FIELD_TYPE: ... + + def __get__(self, instance: Any, owner: Any): # pyright: ignore [reportInconsistentOverload] + """Get the Var. + + Args: + instance: The instance of the class accessing the Var. + owner: The class that the Var is attached to. + """ + + +@overload +def field( + default: FIELD_TYPE | _MISSING_TYPE = MISSING, + *, + is_var: Literal[False], + default_factory: Callable[[], FIELD_TYPE] | None = None, +) -> FIELD_TYPE: ... + + +@overload +def field( + default: FIELD_TYPE | _MISSING_TYPE = MISSING, + *, + default_factory: Callable[[], FIELD_TYPE] | None = None, + is_var: Literal[True] = True, +) -> Field[FIELD_TYPE]: ... + + +def field( + default: FIELD_TYPE | _MISSING_TYPE = MISSING, + *, + default_factory: Callable[[], FIELD_TYPE] | None = None, + is_var: bool = True, +) -> Field[FIELD_TYPE] | FIELD_TYPE: + """Create a field for a state. + + Args: + default: The default value for the field. + default_factory: The default factory for the field. + is_var: Whether the field is a Var. + + Returns: + The field for the state. + + Raises: + ValueError: If both default and default_factory are specified. + """ + if default is not MISSING and default_factory is not None: + msg = "cannot specify both default and default_factory" + raise ValueError(msg) + if default is not MISSING and not types.is_immutable(default): + console.warn( + "Mutable default values are not recommended. " + "Use default_factory instead to avoid unexpected behavior." + ) + return Field( + default_factory=functools.partial(copy.deepcopy, default), + is_var=is_var, + ) + return Field( + default=default, + default_factory=default_factory, + is_var=is_var, + ) + + +@dataclass_transform(kw_only_default=True, field_specifiers=(field,)) +class BaseStateMeta(ABCMeta): + """Meta class for BaseState.""" + + if TYPE_CHECKING: + __inherited_fields__: Mapping[str, Field] + __own_fields__: dict[str, Field] + __fields__: dict[str, Field] + + # Whether this state class is a mixin and should not be instantiated. + _mixin: bool = False + + def __new__( + cls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + mixin: bool = False, + ) -> type: + """Create a new class. + + Args: + name: The name of the class. + bases: The bases of the class. + namespace: The namespace of the class. + mixin: Whether the class is a mixin and should not be instantiated. + + Returns: + The new class. + """ + state_bases = [ + base for base in bases if issubclass(base, EvenMoreBasicBaseState) + ] + mixin = mixin or ( + bool(state_bases) and all(base._mixin for base in state_bases) + ) + # Add the field to the class + inherited_fields: dict[str, Field] = {} + own_fields: dict[str, Field] = {} + resolved_annotations = types.resolve_annotations( + annotations_from_namespace(namespace), namespace["__module__"] + ) + + for base in bases[::-1]: + if hasattr(base, "__inherited_fields__"): + inherited_fields.update(base.__inherited_fields__) + for base in bases[::-1]: + if hasattr(base, "__own_fields__"): + inherited_fields.update(base.__own_fields__) + + for key, value in [ + (key, value) + for key, value in namespace.items() + if key not in resolved_annotations + ]: + if isinstance(value, Field): + if value.annotated_type is not Any: + new_value = value + elif value.default is not MISSING: + new_value = Field( + default=value.default, + is_var=value.is_var, + annotated_type=figure_out_type(value.default), + ) + else: + new_value = Field( + default_factory=value.default_factory, + is_var=value.is_var, + annotated_type=Any, + ) + elif ( + not key.startswith("__") + and not callable(value) + and not isinstance(value, (staticmethod, classmethod, property, Var)) + ): + if types.is_immutable(value): + new_value = Field( + default=value, + annotated_type=figure_out_type(value), + ) + else: + new_value = Field( + default_factory=functools.partial(copy.deepcopy, value), + annotated_type=figure_out_type(value), + ) + else: + continue + + own_fields[key] = new_value + + for key, annotation in resolved_annotations.items(): + value = namespace.get(key, MISSING) + + if types.is_classvar(annotation): + # If the annotation is a classvar, skip it. + continue + + if value is MISSING: + value = Field( + annotated_type=annotation, + ) + elif not isinstance(value, Field): + if types.is_immutable(value): + value = Field( + default=value, + annotated_type=annotation, + ) + else: + value = Field( + default_factory=functools.partial(copy.deepcopy, value), + annotated_type=annotation, + ) + else: + value = Field( + default=value.default, + default_factory=value.default_factory, + is_var=value.is_var, + annotated_type=annotation, + ) + + own_fields[key] = value + + namespace["__own_fields__"] = own_fields + namespace["__inherited_fields__"] = inherited_fields + namespace["__fields__"] = inherited_fields | own_fields + namespace["_mixin"] = mixin + return super().__new__(cls, name, bases, namespace) + + +class EvenMoreBasicBaseState(metaclass=BaseStateMeta): + """A simplified base state class that provides basic functionality.""" + + def __init__( + self, + **kwargs, + ): + """Initialize the state with the given kwargs. + + Args: + **kwargs: The kwargs to pass to the state. + """ + super().__init__() + for key, value in kwargs.items(): + object.__setattr__(self, key, value) + for name, value in type(self).get_fields().items(): + if name not in kwargs: + default_value = value.default_value() + object.__setattr__(self, name, default_value) + + def set(self, **kwargs): + """Mutate the state by setting the given kwargs. Returns the state. + + Args: + **kwargs: The kwargs to set. + + Returns: + The state with the fields set to the given kwargs. + """ + for key, value in kwargs.items(): + setattr(self, key, value) + return self + + @classmethod + def get_fields(cls) -> Mapping[str, Field]: + """Get the fields of the component. + + Returns: + The fields of the component. + """ + return cls.__fields__ + + @classmethod + def add_field(cls, name: str, var: Var, default_value: Any): + """Add a field to the class after class definition. + + Used by State.add_var() to correctly handle the new variable. + + Args: + name: The name of the field to add. + var: The variable to add a field for. + default_value: The default value of the field. + """ + if types.is_immutable(default_value): + new_field = Field( + default=default_value, + annotated_type=var._var_type, + ) + else: + new_field = Field( + default_factory=functools.partial(copy.deepcopy, default_value), + annotated_type=var._var_type, + ) + cls.__fields__[name] = new_field diff --git a/packages/reflex-core/src/reflex_core/vars/color.py b/packages/reflex-core/src/reflex_core/vars/color.py new file mode 100644 index 00000000000..7db57acde2e --- /dev/null +++ b/packages/reflex-core/src/reflex_core/vars/color.py @@ -0,0 +1,166 @@ +"""Vars for colors.""" + +import dataclasses + +from reflex_core.constants.colors import Color +from reflex_core.vars.base import ( + CachedVarOperation, + LiteralVar, + Var, + VarData, + cached_property_no_lock, + get_python_literal, +) +from reflex_core.vars.number import ternary_operation +from reflex_core.vars.sequence import ConcatVarOperation, LiteralStringVar, StringVar + + +class ColorVar(StringVar[Color], python_types=Color): + """Base class for immutable color vars.""" + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralColorVar(CachedVarOperation, LiteralVar, ColorVar): + """Base class for immutable literal color vars.""" + + _var_value: Color = dataclasses.field(default_factory=lambda: Color(color="black")) + + @classmethod + def _get_all_var_data_without_creating_var( + cls, + value: Color, + ) -> VarData | None: + return VarData.merge( + LiteralStringVar._get_all_var_data_without_creating_var(value.color) + if isinstance(value.color, str) + else value.color._get_all_var_data(), + value.alpha._get_all_var_data() + if not isinstance(value.alpha, bool) + else None, + value.shade._get_all_var_data() + if not isinstance(value.shade, int) + else None, + ) + + @classmethod + def create( + cls, + value: Color, + _var_type: type[Color] | None = None, + _var_data: VarData | None = None, + ) -> ColorVar: + """Create a var from a string value. + + Args: + value: The value to create the var from. + _var_type: The type of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + return cls( + _js_expr="", + _var_type=_var_type or Color, + _var_data=_var_data, + _var_value=value, + ) + + def __hash__(self) -> int: + """Get the hash of the var. + + Returns: + The hash of the var. + """ + return hash(( + self.__class__.__name__, + self._var_value.color, + self._var_value.alpha, + self._var_value.shade, + )) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + alpha = self._var_value.alpha + alpha = ( + ternary_operation( + alpha, + LiteralStringVar.create("a"), + LiteralStringVar.create(""), + ) + if isinstance(alpha, Var) + else LiteralStringVar.create("a" if alpha else "") + ) + + shade = self._var_value.shade + shade = ( + shade.to_string(use_json=False) + if isinstance(shade, Var) + else LiteralStringVar.create(str(shade)) + ) + return str( + ConcatVarOperation.create( + LiteralStringVar.create("var(--"), + self._var_value.color, + LiteralStringVar.create("-"), + alpha, + shade, + LiteralStringVar.create(")"), + ) + ) + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> VarData | None: + """Get all the var data. + + Returns: + The var data. + """ + return VarData.merge( + LiteralStringVar._get_all_var_data_without_creating_var( + self._var_value.color + ) + if isinstance(self._var_value.color, str) + else self._var_value.color._get_all_var_data(), + self._var_value.alpha._get_all_var_data() + if not isinstance(self._var_value.alpha, bool) + else None, + self._var_value.shade._get_all_var_data() + if not isinstance(self._var_value.shade, int) + else None, + self._var_data, + ) + + def json(self) -> str: + """Get the JSON representation of the var. + + Returns: + The JSON representation of the var. + + Raises: + TypeError: If the color is not a valid color. + """ + color, alpha, shade = map( + get_python_literal, + (self._var_value.color, self._var_value.alpha, self._var_value.shade), + ) + if color is None or alpha is None or shade is None: + msg = "Cannot serialize color that contains non-literal vars." + raise TypeError(msg) + if ( + not isinstance(color, str) + or not isinstance(alpha, bool) + or not isinstance(shade, int) + ): + msg = "Color is not a valid color." + raise TypeError(msg) + return f"var(--{color}-{'a' if alpha else ''}{shade})" diff --git a/packages/reflex-core/src/reflex_core/vars/datetime.py b/packages/reflex-core/src/reflex_core/vars/datetime.py new file mode 100644 index 00000000000..d528f678781 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/vars/datetime.py @@ -0,0 +1,202 @@ +"""Immutable datetime and date vars.""" + +from __future__ import annotations + +import dataclasses +from datetime import date, datetime +from typing import Any, TypeVar + +from reflex_core.utils.exceptions import VarTypeError +from reflex_core.vars.number import BooleanVar + +from .base import ( + CustomVarOperationReturn, + LiteralVar, + Var, + VarData, + var_operation, + var_operation_return, +) + +DATETIME_T = TypeVar("DATETIME_T", datetime, date) + +datetime_types = datetime | date + + +def raise_var_type_error(): + """Raise a VarTypeError. + + Raises: + VarTypeError: Cannot compare a datetime object with a non-datetime object. + """ + msg = "Cannot compare a datetime object with a non-datetime object." + raise VarTypeError(msg) + + +class DateTimeVar(Var[DATETIME_T], python_types=(datetime, date)): + """A variable that holds a datetime or date object.""" + + def __lt__(self, other: datetime_types | DateTimeVar) -> BooleanVar: + """Less than comparison. + + Args: + other: The other datetime to compare. + + Returns: + The result of the comparison. + """ + if not isinstance(other, DATETIME_TYPES): + raise_var_type_error() + return date_lt_operation(self, other) + + def __le__(self, other: datetime_types | DateTimeVar) -> BooleanVar: + """Less than or equal comparison. + + Args: + other: The other datetime to compare. + + Returns: + The result of the comparison. + """ + if not isinstance(other, DATETIME_TYPES): + raise_var_type_error() + return date_le_operation(self, other) + + def __gt__(self, other: datetime_types | DateTimeVar) -> BooleanVar: + """Greater than comparison. + + Args: + other: The other datetime to compare. + + Returns: + The result of the comparison. + """ + if not isinstance(other, DATETIME_TYPES): + raise_var_type_error() + return date_gt_operation(self, other) + + def __ge__(self, other: datetime_types | DateTimeVar) -> BooleanVar: + """Greater than or equal comparison. + + Args: + other: The other datetime to compare. + + Returns: + The result of the comparison. + """ + if not isinstance(other, DATETIME_TYPES): + raise_var_type_error() + return date_ge_operation(self, other) + + +@var_operation +def date_gt_operation(lhs: DateTimeVar | Any, rhs: DateTimeVar | Any): + """Greater than comparison. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + + Returns: + The result of the operation. + """ + return date_compare_operation(rhs, lhs, strict=True) + + +@var_operation +def date_lt_operation(lhs: DateTimeVar | Any, rhs: DateTimeVar | Any): + """Less than comparison. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + + Returns: + The result of the operation. + """ + return date_compare_operation(lhs, rhs, strict=True) + + +@var_operation +def date_le_operation(lhs: DateTimeVar | Any, rhs: DateTimeVar | Any): + """Less than or equal comparison. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + + Returns: + The result of the operation. + """ + return date_compare_operation(lhs, rhs) + + +@var_operation +def date_ge_operation(lhs: DateTimeVar | Any, rhs: DateTimeVar | Any): + """Greater than or equal comparison. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + + Returns: + The result of the operation. + """ + return date_compare_operation(rhs, lhs) + + +def date_compare_operation( + lhs: DateTimeVar[DATETIME_T] | Any, + rhs: DateTimeVar[DATETIME_T] | Any, + strict: bool = False, +) -> CustomVarOperationReturn[bool]: + """Check if the value is less than the other value. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + strict: Whether to use strict comparison. + + Returns: + The result of the operation. + """ + return var_operation_return( + f"({lhs} {'<' if strict else '<='} {rhs})", + bool, + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralDatetimeVar(LiteralVar, DateTimeVar): + """Base class for immutable datetime and date vars.""" + + _var_value: date = dataclasses.field(default=datetime.now()) + + @classmethod + def _get_all_var_data_without_creating_var(cls, value: date) -> VarData | None: + return None + + @classmethod + def create(cls, value: date, _var_data: VarData | None = None): + """Create a new instance of the class. + + Args: + value: The value to set. + + Returns: + LiteralDatetimeVar: The new instance of the class. + """ + js_expr = f'"{value!s}"' + return cls( + _js_expr=js_expr, + _var_type=type(value), + _var_value=value, + _var_data=_var_data, + ) + + +DATETIME_TYPES = (datetime, date, DateTimeVar) diff --git a/packages/reflex-core/src/reflex_core/vars/dep_tracking.py b/packages/reflex-core/src/reflex_core/vars/dep_tracking.py new file mode 100644 index 00000000000..2dfea82eb85 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/vars/dep_tracking.py @@ -0,0 +1,485 @@ +"""Collection of base classes.""" + +from __future__ import annotations + +import contextlib +import dataclasses +import dis +import enum +import importlib +import inspect +import sys +from types import CellType, CodeType, FunctionType, ModuleType +from typing import TYPE_CHECKING, Any, ClassVar, cast + +from reflex_core.utils.exceptions import VarValueError + +if TYPE_CHECKING: + from reflex.state import BaseState + + from .base import Var + + +CellEmpty = object() + + +def get_cell_value(cell: CellType) -> Any: + """Get the value of a cell object. + + Args: + cell: The cell object to get the value from. (func.__closure__ objects) + + Returns: + The value from the cell or CellEmpty if a ValueError is raised. + """ + try: + return cell.cell_contents + except ValueError: + return CellEmpty + + +class ScanStatus(enum.Enum): + """State of the dis instruction scanning loop.""" + + SCANNING = enum.auto() + GETTING_ATTR = enum.auto() + GETTING_STATE = enum.auto() + GETTING_STATE_POST_AWAIT = enum.auto() + GETTING_VAR = enum.auto() + GETTING_IMPORT = enum.auto() + + +class UntrackedLocalVarError(VarValueError): + """Raised when a local variable is referenced, but it is not tracked in the current scope.""" + + +def assert_base_state( + local_value: Any, + local_name: str | None = None, +) -> type[BaseState]: + """Assert that a local variable is a BaseState subclass. + + Args: + local_value: The value of the local variable to check. + local_name: The name of the local variable to check. + + Returns: + The local variable value if it is a BaseState subclass. + + Raises: + VarValueError: If the object is not a BaseState subclass. + """ + from reflex.state import BaseState + + if not isinstance(local_value, type) or not issubclass(local_value, BaseState): + msg = f"Cannot determine dependencies in fetched state {local_name!r}: {local_value!r} is not a BaseState." + raise VarValueError(msg) + return local_value + + +@dataclasses.dataclass +class DependencyTracker: + """State machine for identifying state attributes that are accessed by a function.""" + + func: FunctionType | CodeType = dataclasses.field() + state_cls: type[BaseState] = dataclasses.field() + + dependencies: dict[str, set[str]] = dataclasses.field(default_factory=dict) + + scan_status: ScanStatus = dataclasses.field(default=ScanStatus.SCANNING) + top_of_stack: str | None = dataclasses.field(default=None) + + tracked_locals: dict[str, type[BaseState] | ModuleType] = dataclasses.field( + default_factory=dict + ) + + _getting_state_class: type[BaseState] | ModuleType | None = dataclasses.field( + default=None + ) + _get_var_value_positions: dis.Positions | None = dataclasses.field(default=None) + _last_import_name: str | None = dataclasses.field(default=None) + + INVALID_NAMES: ClassVar[list[str]] = ["parent_state", "substates", "get_substate"] + + def __post_init__(self): + """After initializing, populate the dependencies dict.""" + with contextlib.suppress(AttributeError): + # unbox functools.partial + self.func = cast(FunctionType, self.func.func) # pyright: ignore[reportAttributeAccessIssue] + with contextlib.suppress(AttributeError): + # unbox EventHandler + self.func = cast(FunctionType, self.func.fn) # pyright: ignore[reportAttributeAccessIssue,reportFunctionMemberAccess] + + if isinstance(self.func, FunctionType): + with contextlib.suppress(AttributeError, IndexError): + # the first argument to the function is the name of "self" arg + self.tracked_locals[self.func.__code__.co_varnames[0]] = self.state_cls + + self._populate_dependencies() + + def _merge_deps(self, tracker: DependencyTracker) -> None: + """Merge dependencies from another DependencyTracker. + + Args: + tracker: The DependencyTracker to merge dependencies from. + """ + for state_name, dep_name in tracker.dependencies.items(): + self.dependencies.setdefault(state_name, set()).update(dep_name) + + def get_tracked_local(self, local_name: str) -> type[BaseState] | ModuleType: + """Get the value of a local name tracked in the current function scope. + + Args: + local_name: The name of the local variable to fetch. + + Returns: + The value of local name tracked in the current scope (a referenced + BaseState subclass or imported module). + + Raises: + UntrackedLocalVarError: If the local variable is not being tracked. + """ + try: + local_value = self.tracked_locals[local_name] + except KeyError as ke: + msg = f"{local_name!r} is not tracked in the current scope." + raise UntrackedLocalVarError(msg) from ke + return local_value + + def load_attr_or_method(self, instruction: dis.Instruction) -> None: + """Handle loading an attribute or method from the object on top of the stack. + + This method directly tracks attributes and recursively merges + dependencies from analyzing the dependencies of any methods called. + + Args: + instruction: The dis instruction to process. + + Raises: + VarValueError: if the attribute is an disallowed name or attribute + does not reference a BaseState. + """ + from .base import ComputedVar + + if instruction.argval in self.INVALID_NAMES: + msg = f"Cached var {self!s} cannot access arbitrary state via `{instruction.argval}`." + raise VarValueError(msg) + if instruction.argval == "get_state": + # Special case: arbitrary state access requested. + self.scan_status = ScanStatus.GETTING_STATE + return + if instruction.argval == "get_var_value": + # Special case: arbitrary var access requested. + if sys.version_info >= (3, 11): + self._get_var_value_positions = instruction.positions + self.scan_status = ScanStatus.GETTING_VAR + return + + # Reset status back to SCANNING after attribute is accessed. + self.scan_status = ScanStatus.SCANNING + if not self.top_of_stack: + return + target_obj = self.get_tracked_local(self.top_of_stack) + try: + target_state = assert_base_state(target_obj, local_name=self.top_of_stack) + except VarValueError: + # If the target state is not a BaseState, we cannot track dependencies on it. + return + try: + ref_obj = getattr(target_state, instruction.argval) + except AttributeError: + # Not found on this state class, maybe it is a dynamic attribute that will be picked up later. + ref_obj = None + + if isinstance(ref_obj, property) and not isinstance(ref_obj, ComputedVar): + # recurse into property fget functions + ref_obj = ref_obj.fget + if callable(ref_obj): + # recurse into callable attributes + self._merge_deps( + type(self)(func=cast(FunctionType, ref_obj), state_cls=target_state) + ) + elif ( + instruction.argval in target_state.backend_vars + or instruction.argval in target_state.vars + ): + # var access + self.dependencies.setdefault(target_state.get_full_name(), set()).add( + instruction.argval + ) + + def _get_globals(self) -> dict[str, Any]: + """Get the globals of the function. + + Returns: + The var names and values in the globals of the function. + """ + if isinstance(self.func, CodeType): + return {} + return self.func.__globals__ # pyright: ignore[reportAttributeAccessIssue] + + def _get_closure(self) -> dict[str, Any]: + """Get the closure of the function, with unbound values omitted. + + Returns: + The var names and values in the closure of the function. + """ + if isinstance(self.func, CodeType): + return {} + return { + var_name: get_cell_value(cell) + for var_name, cell in zip( + self.func.__code__.co_freevars, # pyright: ignore[reportAttributeAccessIssue] + self.func.__closure__ or (), + strict=False, + ) + if get_cell_value(cell) is not CellEmpty + } + + def handle_getting_state(self, instruction: dis.Instruction) -> None: + """Handle bytecode analysis when `get_state` was called in the function. + + If the wrapped function is getting an arbitrary state and saving it to a + local variable, this method associates the local variable name with the + state class in self.tracked_locals. + + When an attribute/method is accessed on a tracked local, it will be + associated with this state. + + Args: + instruction: The dis instruction to process. + + Raises: + VarValueError: if the state class cannot be determined from the instruction. + """ + if isinstance(self.func, CodeType): + msg = "Dependency detection cannot identify get_state class from a code object." + raise VarValueError(msg) + if instruction.opname in ("LOAD_FAST", "LOAD_FAST_BORROW"): + self._getting_state_class = self.get_tracked_local( + local_name=instruction.argval, + ) + elif instruction.opname == "LOAD_GLOBAL": + # Special case: referencing state class from global scope. + try: + self._getting_state_class = self._get_globals()[instruction.argval] + except (ValueError, KeyError) as ve: + msg = f"Cached var {self!s} cannot access arbitrary state `{instruction.argval}`, not found in globals." + raise VarValueError(msg) from ve + elif instruction.opname == "LOAD_DEREF": + # Special case: referencing state class from closure. + try: + self._getting_state_class = self._get_closure()[instruction.argval] + except (ValueError, KeyError) as ve: + msg = f"Cached var {self!s} cannot access arbitrary state `{instruction.argval}`, is it defined yet?" + raise VarValueError(msg) from ve + elif instruction.opname in ("LOAD_ATTR", "LOAD_METHOD"): + self._getting_state_class = getattr( + self._getting_state_class, + instruction.argval, + ) + elif instruction.opname == "GET_AWAITABLE": + # Now inside the `await` machinery, subsequent instructions + # operate on the result of the `get_state` call. + self.scan_status = ScanStatus.GETTING_STATE_POST_AWAIT + if self._getting_state_class is not None: + self.top_of_stack = "_" + self.tracked_locals[self.top_of_stack] = self._getting_state_class + self._getting_state_class = None + + def handle_getting_state_post_await(self, instruction: dis.Instruction) -> None: + """Handle bytecode analysis after `get_state` was called in the function. + + This function is called _after_ awaiting self.get_state to capture the + local variable holding the state instance or directly record access to + attributes accessed on the result of get_state. + + Args: + instruction: The dis instruction to process. + + Raises: + VarValueError: if the state class cannot be determined from the instruction. + """ + if instruction.opname == "STORE_FAST" and self.top_of_stack: + # Storing the result of get_state in a local variable. + self.tracked_locals[instruction.argval] = self.tracked_locals.pop( + self.top_of_stack + ) + self.top_of_stack = None + self.scan_status = ScanStatus.SCANNING + elif instruction.opname in ("LOAD_ATTR", "LOAD_METHOD"): + # Attribute access on an inline `get_state`, not assigned to a variable. + self.load_attr_or_method(instruction) + + def _eval_var(self, positions: dis.Positions) -> Var: + """Evaluate instructions from the wrapped function to get the Var object. + + Args: + positions: The disassembly positions of the get_var_value call. + + Returns: + The Var object. + + Raises: + VarValueError: if the source code for the var cannot be determined. + """ + # Get the original source code and eval it to get the Var. + module = inspect.getmodule(self.func) + if module is None or self._get_var_value_positions is None: + msg = f"Cannot determine the source code for the var in {self.func!r}." + raise VarValueError(msg) + start_line = self._get_var_value_positions.end_lineno + start_column = self._get_var_value_positions.end_col_offset + end_line = positions.end_lineno + end_column = positions.end_col_offset + if ( + start_line is None + or start_column is None + or end_line is None + or end_column is None + ): + msg = f"Cannot determine the source code for the var in {self.func!r}." + raise VarValueError(msg) + source = inspect.getsource(module).splitlines(True)[start_line - 1 : end_line] + # Create a python source string snippet. + if len(source) > 1: + snipped_source = "".join([ + *source[0][start_column:], + *source[1:-1], + *source[-1][:end_column], + ]) + else: + snipped_source = source[0][start_column:end_column] + # Evaluate the string in the context of the function's globals, closure and tracked local scope. + return eval( + f"({snipped_source})", + self._get_globals(), + {**self._get_closure(), **self.tracked_locals}, + ) + + def handle_getting_var(self, instruction: dis.Instruction) -> None: + """Handle bytecode analysis when `get_var_value` was called in the function. + + This only really works if the expression passed to `get_var_value` is + evaluable in the function's global scope or closure, so getting the var + value from a var saved in a local variable or in the class instance is + not possible. + + Args: + instruction: The dis instruction to process. + + Raises: + VarValueError: if the source code for the var cannot be determined. + """ + if instruction.opname == "CALL": + if instruction.positions is None: + msg = f"Cannot determine the source code for the var in {self.func!r}." + raise VarValueError(msg) + the_var = self._eval_var(instruction.positions) + the_var_data = the_var._get_all_var_data() + if the_var_data is None: + msg = f"Cannot determine the source code for the var in {self.func!r}." + raise VarValueError(msg) + self.dependencies.setdefault(the_var_data.state, set()).add( + the_var_data.field_name + ) + self.scan_status = ScanStatus.SCANNING + + def _populate_dependencies(self) -> None: + """Update self.dependencies based on the disassembly of self.func. + + Save references to attributes accessed on "self" or other fetched states. + + Recursively called when the function makes a method call on "self" or + define comprehensions or nested functions that may reference "self". + """ + for instruction in dis.get_instructions(self.func): + if self.scan_status == ScanStatus.GETTING_STATE: + self.handle_getting_state(instruction) + elif self.scan_status == ScanStatus.GETTING_STATE_POST_AWAIT: + self.handle_getting_state_post_await(instruction) + elif self.scan_status == ScanStatus.GETTING_VAR: + self.handle_getting_var(instruction) + elif ( + instruction.opname + in ( + "LOAD_FAST", + "LOAD_DEREF", + "LOAD_FAST_BORROW", + "LOAD_FAST_CHECK", + "LOAD_FAST_AND_CLEAR", + ) + and instruction.argval in self.tracked_locals + ): + # bytecode loaded the class instance to the top of stack, next load instruction + # is referencing an attribute on self + self.top_of_stack = instruction.argval + self.scan_status = ScanStatus.GETTING_ATTR + elif ( + instruction.opname + in ( + "LOAD_FAST_LOAD_FAST", + "LOAD_FAST_BORROW_LOAD_FAST_BORROW", + "STORE_FAST_LOAD_FAST", + ) + and instruction.argval[-1] in self.tracked_locals + ): + # Double LOAD_FAST family instructions load multiple values onto the stack, + # the last value in the argval list is the top of the stack. + self.top_of_stack = instruction.argval[-1] + self.scan_status = ScanStatus.GETTING_ATTR + elif self.scan_status == ScanStatus.GETTING_ATTR and instruction.opname in ( + "LOAD_ATTR", + "LOAD_METHOD", + ): + self.load_attr_or_method(instruction) + self.top_of_stack = None + elif instruction.opname == "LOAD_CONST" and isinstance( + instruction.argval, CodeType + ): + # recurse into nested functions / comprehensions, which can reference + # instance attributes from the outer scope + self._merge_deps( + type(self)( + func=instruction.argval, + state_cls=self.state_cls, + tracked_locals=self.tracked_locals, + ) + ) + elif instruction.opname == "IMPORT_NAME" and instruction.argval is not None: + self.scan_status = ScanStatus.GETTING_IMPORT + self._last_import_name = instruction.argval + importlib.import_module(instruction.argval) + top_module_name = instruction.argval.split(".")[0] + self.tracked_locals[instruction.argval] = sys.modules[top_module_name] + self.top_of_stack = instruction.argval + elif instruction.opname == "IMPORT_FROM": + if not self._last_import_name: + msg = f"Cannot find package associated with import {instruction.argval} in {self.func!r}." + raise VarValueError(msg) + if instruction.argval in self._last_import_name.split("."): + # `import ... as ...` case: + # import from interim package, update tracked_locals for the last imported name. + self.tracked_locals[self._last_import_name] = getattr( + self.tracked_locals[self._last_import_name], instruction.argval + ) + continue + # Importing a name from a package/module. + if self._last_import_name is not None and self.top_of_stack: + # The full import name does NOT end up in scope for a `from ... import`. + self.tracked_locals.pop(self._last_import_name) + self.tracked_locals[instruction.argval] = getattr( + importlib.import_module(self._last_import_name), + instruction.argval, + ) + # If we see a STORE_FAST, we can assign the top of stack to an aliased name. + self.top_of_stack = instruction.argval + elif ( + self.scan_status == ScanStatus.GETTING_IMPORT + and instruction.opname == "STORE_FAST" + and self.top_of_stack is not None + ): + self.tracked_locals[instruction.argval] = self.tracked_locals.pop( + self.top_of_stack + ) + self.top_of_stack = None diff --git a/packages/reflex-core/src/reflex_core/vars/function.py b/packages/reflex-core/src/reflex_core/vars/function.py new file mode 100644 index 00000000000..6efec1b5679 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/vars/function.py @@ -0,0 +1,555 @@ +"""Immutable function vars.""" + +from __future__ import annotations + +import dataclasses +from collections.abc import Callable, Sequence +from typing import Any, Concatenate, Generic, ParamSpec, Protocol, TypeVar, overload + +from reflex_core.utils import format +from reflex_core.utils.types import GenericType + +from .base import CachedVarOperation, LiteralVar, Var, VarData, cached_property_no_lock + +P = ParamSpec("P") +V1 = TypeVar("V1") +V2 = TypeVar("V2") +V3 = TypeVar("V3") +V4 = TypeVar("V4") +V5 = TypeVar("V5") +V6 = TypeVar("V6") +R = TypeVar("R") + + +class ReflexCallable(Protocol[P, R]): + """Protocol for a callable.""" + + __call__: Callable[P, R] + + +CALLABLE_TYPE = TypeVar("CALLABLE_TYPE", bound=ReflexCallable, covariant=True) +OTHER_CALLABLE_TYPE = TypeVar( + "OTHER_CALLABLE_TYPE", bound=ReflexCallable, covariant=True +) + + +def _is_js_identifier_start(char: str) -> bool: + """Check whether a character can start a JavaScript identifier. + + Returns: + True if the character is valid as the first character of a JS identifier. + """ + return char == "$" or char == "_" or char.isalpha() + + +def _is_js_identifier_char(char: str) -> bool: + """Check whether a character can continue a JavaScript identifier. + + Returns: + True if the character is valid within a JS identifier. + """ + return _is_js_identifier_start(char) or char.isdigit() + + +def _starts_with_arrow_function(expr: str) -> bool: + """Check whether an expression starts with an inline arrow function. + + Returns: + True if the expression begins with an arrow function. + """ + if "=>" not in expr: + return False + + expr = expr.lstrip() + if not expr: + return False + + if expr.startswith("async"): + async_remainder = expr[len("async") :] + if async_remainder[:1].isspace(): + expr = async_remainder.lstrip() + + if not expr: + return False + + if _is_js_identifier_start(expr[0]): + end_index = 1 + while end_index < len(expr) and _is_js_identifier_char(expr[end_index]): + end_index += 1 + return expr[end_index:].lstrip().startswith("=>") + + if not expr.startswith("("): + return False + + depth = 0 + string_delimiter: str | None = None + escaped = False + + for index, char in enumerate(expr): + if string_delimiter is not None: + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == string_delimiter: + string_delimiter = None + continue + + if char in {"'", '"', "`"}: + string_delimiter = char + continue + + if char == "(": + depth += 1 + continue + + if char == ")": + depth -= 1 + if depth == 0: + return expr[index + 1 :].lstrip().startswith("=>") + + return False + + +class FunctionVar(Var[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any]): + """Base class for immutable function vars.""" + + @overload + def partial(self) -> FunctionVar[CALLABLE_TYPE]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, P], R]], + arg1: V1 | Var[V1], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, P], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, P], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + arg3: V3 | Var[V3], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, V4, P], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + arg3: V3 | Var[V3], + arg4: V4 | Var[V4], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, V4, V5, P], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + arg3: V3 | Var[V3], + arg4: V4 | Var[V4], + arg5: V5 | Var[V5], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, V4, V5, V6, P], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + arg3: V3 | Var[V3], + arg4: V4 | Var[V4], + arg5: V5 | Var[V5], + arg6: V6 | Var[V6], + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial( + self: FunctionVar[ReflexCallable[P, R]], *args: Var | Any + ) -> FunctionVar[ReflexCallable[P, R]]: ... + + @overload + def partial(self, *args: Var | Any) -> FunctionVar: ... + + def partial(self, *args: Var | Any) -> FunctionVar: # pyright: ignore [reportInconsistentOverload] + """Partially apply the function with the given arguments. + + Args: + *args: The arguments to partially apply the function with. + + Returns: + The partially applied function. + """ + if not args: + return self + return ArgsFunctionOperation.create( + ("...args",), + VarOperationCall.create(self, *args, Var(_js_expr="...args")), + ) + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1], R]], arg1: V1 | Var[V1] + ) -> VarOperationCall[[V1], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + ) -> VarOperationCall[[V1, V2], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2, V3], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + arg3: V3 | Var[V3], + ) -> VarOperationCall[[V1, V2, V3], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2, V3, V4], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + arg3: V3 | Var[V3], + arg4: V4 | Var[V4], + ) -> VarOperationCall[[V1, V2, V3, V4], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2, V3, V4, V5], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + arg3: V3 | Var[V3], + arg4: V4 | Var[V4], + arg5: V5 | Var[V5], + ) -> VarOperationCall[[V1, V2, V3, V4, V5], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[[V1, V2, V3, V4, V5, V6], R]], + arg1: V1 | Var[V1], + arg2: V2 | Var[V2], + arg3: V3 | Var[V3], + arg4: V4 | Var[V4], + arg5: V5 | Var[V5], + arg6: V6 | Var[V6], + ) -> VarOperationCall[[V1, V2, V3, V4, V5, V6], R]: ... + + @overload + def call( + self: FunctionVar[ReflexCallable[P, R]], *args: Var | Any + ) -> VarOperationCall[P, R]: ... + + @overload + def call(self, *args: Var | Any) -> Var: ... + + def call(self, *args: Var | Any) -> Var: # pyright: ignore [reportInconsistentOverload] + """Call the function with the given arguments. + + Args: + *args: The arguments to call the function with. + + Returns: + The function call operation. + """ + return VarOperationCall.create(self, *args).guess_type() + + __call__ = call + + +class BuilderFunctionVar( + FunctionVar[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any] +): + """Base class for immutable function vars with the builder pattern.""" + + __call__ = FunctionVar.partial + + +class FunctionStringVar(FunctionVar[CALLABLE_TYPE]): + """Base class for immutable function vars from a string.""" + + @classmethod + def create( + cls, + func: str, + _var_type: type[OTHER_CALLABLE_TYPE] = ReflexCallable[Any, Any], + _var_data: VarData | None = None, + ) -> FunctionStringVar[OTHER_CALLABLE_TYPE]: + """Create a new function var from a string. + + Args: + func: The function to call. + _var_type: The type of the Var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The function var. + """ + return FunctionStringVar( + _js_expr=func, + _var_type=_var_type, + _var_data=_var_data, + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class VarOperationCall(Generic[P, R], CachedVarOperation, Var[R]): + """Base class for immutable vars that are the result of a function call.""" + + _func: FunctionVar[ReflexCallable[P, R]] | None = dataclasses.field(default=None) + _args: tuple[Var | Any, ...] = dataclasses.field(default_factory=tuple) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + func_expr = str(self._func) + if _starts_with_arrow_function(func_expr) and not format.is_wrapped( + func_expr, "(" + ): + func_expr = format.wrap(func_expr, "(") + + return f"({func_expr}({', '.join([str(LiteralVar.create(arg)) for arg in self._args])}))" + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> VarData | None: + """Get all the var data associated with the var. + + Returns: + All the var data associated with the var. + """ + return VarData.merge( + self._func._get_all_var_data() if self._func is not None else None, + *[LiteralVar.create(arg)._get_all_var_data() for arg in self._args], + self._var_data, + ) + + @classmethod + def create( + cls, + func: FunctionVar[ReflexCallable[P, R]], + *args: Var | Any, + _var_type: GenericType = Any, + _var_data: VarData | None = None, + ) -> VarOperationCall: + """Create a new function call var. + + Args: + func: The function to call. + *args: The arguments to call the function with. + _var_type: The type of the Var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The function call var. + """ + function_return_type = ( + func._var_type.__args__[1] + if getattr(func._var_type, "__args__", None) + else Any + ) + var_type = _var_type if _var_type is not Any else function_return_type + return cls( + _js_expr="", + _var_type=var_type, + _var_data=_var_data, + _func=func, + _args=args, + ) + + +@dataclasses.dataclass(frozen=True) +class DestructuredArg: + """Class for destructured arguments.""" + + fields: tuple[str, ...] = () + rest: str | None = None + + def to_javascript(self) -> str: + """Convert the destructured argument to JavaScript. + + Returns: + The destructured argument in JavaScript. + """ + return format.wrap( + ", ".join(self.fields) + (f", ...{self.rest}" if self.rest else ""), + "{", + "}", + ) + + +@dataclasses.dataclass( + frozen=True, +) +class FunctionArgs: + """Class for function arguments.""" + + args: tuple[str | DestructuredArg, ...] = () + rest: str | None = None + + +def format_args_function_operation( + args: FunctionArgs, return_expr: Var | Any, explicit_return: bool +) -> str: + """Format an args function operation. + + Args: + args: The function arguments. + return_expr: The return expression. + explicit_return: Whether to use explicit return syntax. + + Returns: + The formatted args function operation. + """ + arg_names_str = ", ".join([ + arg if isinstance(arg, str) else arg.to_javascript() for arg in args.args + ]) + (f", ...{args.rest}" if args.rest else "") + + return_expr_str = str(LiteralVar.create(return_expr)) + + # Wrap return expression in curly braces if explicit return syntax is used. + return_expr_str_wrapped = ( + format.wrap(return_expr_str, "{", "}") if explicit_return else return_expr_str + ) + + return f"(({arg_names_str}) => {return_expr_str_wrapped})" + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class ArgsFunctionOperation(CachedVarOperation, FunctionVar): + """Base class for immutable function defined via arguments and return expression.""" + + _args: FunctionArgs = dataclasses.field(default_factory=FunctionArgs) + _return_expr: Var | Any = dataclasses.field(default=None) + _explicit_return: bool = dataclasses.field(default=False) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + return format_args_function_operation( + self._args, self._return_expr, self._explicit_return + ) + + @classmethod + def create( + cls, + args_names: Sequence[str | DestructuredArg], + return_expr: Var | Any, + rest: str | None = None, + explicit_return: bool = False, + _var_type: GenericType = Callable, + _var_data: VarData | None = None, + ): + """Create a new function var. + + Args: + args_names: The names of the arguments. + return_expr: The return expression of the function. + rest: The name of the rest argument. + explicit_return: Whether to use explicit return syntax. + _var_type: The type of the Var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The function var. + """ + return_expr = Var.create(return_expr) + return cls( + _js_expr="", + _var_type=_var_type, + _var_data=_var_data, + _args=FunctionArgs(args=tuple(args_names), rest=rest), + _return_expr=return_expr, + _explicit_return=explicit_return, + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class ArgsFunctionOperationBuilder(CachedVarOperation, BuilderFunctionVar): + """Base class for immutable function defined via arguments and return expression with the builder pattern.""" + + _args: FunctionArgs = dataclasses.field(default_factory=FunctionArgs) + _return_expr: Var | Any = dataclasses.field(default=None) + _explicit_return: bool = dataclasses.field(default=False) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + return format_args_function_operation( + self._args, self._return_expr, self._explicit_return + ) + + @classmethod + def create( + cls, + args_names: Sequence[str | DestructuredArg], + return_expr: Var | Any, + rest: str | None = None, + explicit_return: bool = False, + _var_type: GenericType = Callable, + _var_data: VarData | None = None, + ): + """Create a new function var. + + Args: + args_names: The names of the arguments. + return_expr: The return expression of the function. + rest: The name of the rest argument. + explicit_return: Whether to use explicit return syntax. + _var_type: The type of the Var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The function var. + """ + return_expr = Var.create(return_expr) + return cls( + _js_expr="", + _var_type=_var_type, + _var_data=_var_data, + _args=FunctionArgs(args=tuple(args_names), rest=rest), + _return_expr=return_expr, + _explicit_return=explicit_return, + ) + + +JSON_STRINGIFY = FunctionStringVar.create( + "JSON.stringify", _var_type=ReflexCallable[[Any], str] +) +ARRAY_ISARRAY = FunctionStringVar.create( + "Array.isArray", _var_type=ReflexCallable[[Any], bool] +) +PROTOTYPE_TO_STRING = FunctionStringVar.create( + "((__to_string) => __to_string.toString())", + _var_type=ReflexCallable[[Any], str], +) diff --git a/packages/reflex-core/src/reflex_core/vars/number.py b/packages/reflex-core/src/reflex_core/vars/number.py new file mode 100644 index 00000000000..3652a81ab4a --- /dev/null +++ b/packages/reflex-core/src/reflex_core/vars/number.py @@ -0,0 +1,1148 @@ +"""Immutable number vars.""" + +from __future__ import annotations + +import dataclasses +import decimal +import json +import math +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, NoReturn, TypeVar, overload + +from typing_extensions import TypeVar as TypeVarExt + +from reflex_core.constants.base import Dirs +from reflex_core.utils.exceptions import ( + PrimitiveUnserializableToJSONError, + VarTypeError, + VarValueError, +) +from reflex_core.utils.imports import ImportDict, ImportVar +from reflex_core.utils.types import safe_issubclass + +from .base import ( + CustomVarOperationReturn, + LiteralVar, + Var, + VarData, + unionize, + var_operation, + var_operation_return, +) + +NUMBER_T = TypeVarExt( + "NUMBER_T", + bound=(int | float | decimal.Decimal), + default=(int | float | decimal.Decimal), + covariant=True, +) + +if TYPE_CHECKING: + from .sequence import ArrayVar + + +def raise_unsupported_operand_types( + operator: str, operands_types: tuple[type, ...] +) -> NoReturn: + """Raise an unsupported operand types error. + + Args: + operator: The operator. + operands_types: The types of the operands. + + Raises: + VarTypeError: The operand types are unsupported. + """ + msg = f"Unsupported Operand type(s) for {operator}: {', '.join(t.__name__ for t in operands_types)}" + raise VarTypeError(msg) + + +class NumberVar(Var[NUMBER_T], python_types=(int, float, decimal.Decimal)): + """Base class for immutable number vars.""" + + def __add__(self, other: number_types) -> NumberVar: + """Add two numbers. + + Args: + other: The other number. + + Returns: + The number addition operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("+", (type(self), type(other))) + return number_add_operation(self, +other) + + def __radd__(self, other: number_types) -> NumberVar: + """Add two numbers. + + Args: + other: The other number. + + Returns: + The number addition operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("+", (type(other), type(self))) + return number_add_operation(+other, self) + + def __sub__(self, other: number_types) -> NumberVar: + """Subtract two numbers. + + Args: + other: The other number. + + Returns: + The number subtraction operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("-", (type(self), type(other))) + + return number_subtract_operation(self, +other) + + def __rsub__(self, other: number_types) -> NumberVar: + """Subtract two numbers. + + Args: + other: The other number. + + Returns: + The number subtraction operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("-", (type(other), type(self))) + + return number_subtract_operation(+other, self) + + def __abs__(self): + """Get the absolute value of the number. + + Returns: + The number absolute operation. + """ + return number_abs_operation(self) + + @overload + def __mul__(self, other: number_types | boolean_types) -> NumberVar: ... + + @overload + def __mul__(self, other: list | tuple | set | ArrayVar) -> ArrayVar: ... + + def __mul__(self, other: Any): + """Multiply two numbers. + + Args: + other: The other number. + + Returns: + The number multiplication operation. + """ + from .sequence import ArrayVar, LiteralArrayVar + + if isinstance(other, (list, tuple, ArrayVar)): + if isinstance(other, ArrayVar): + return other * self + return LiteralArrayVar.create(other) * self + + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("*", (type(self), type(other))) + + return number_multiply_operation(self, +other) + + @overload + def __rmul__(self, other: number_types | boolean_types) -> NumberVar: ... + + @overload + def __rmul__(self, other: list | tuple | set | ArrayVar) -> ArrayVar: ... + + def __rmul__(self, other: Any): + """Multiply two numbers. + + Args: + other: The other number. + + Returns: + The number multiplication operation. + """ + from .sequence import ArrayVar, LiteralArrayVar + + if isinstance(other, (list, tuple, ArrayVar)): + if isinstance(other, ArrayVar): + return other * self + return LiteralArrayVar.create(other) * self + + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("*", (type(other), type(self))) + + return number_multiply_operation(+other, self) + + def __truediv__(self, other: number_types) -> NumberVar: + """Divide two numbers. + + Args: + other: The other number. + + Returns: + The number true division operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("/", (type(self), type(other))) + + return number_true_division_operation(self, +other) + + def __rtruediv__(self, other: number_types) -> NumberVar: + """Divide two numbers. + + Args: + other: The other number. + + Returns: + The number true division operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("/", (type(other), type(self))) + + return number_true_division_operation(+other, self) + + def __floordiv__(self, other: number_types) -> NumberVar: + """Floor divide two numbers. + + Args: + other: The other number. + + Returns: + The number floor division operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("//", (type(self), type(other))) + + return number_floor_division_operation(self, +other) + + def __rfloordiv__(self, other: number_types) -> NumberVar: + """Floor divide two numbers. + + Args: + other: The other number. + + Returns: + The number floor division operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("//", (type(other), type(self))) + + return number_floor_division_operation(+other, self) + + def __mod__(self, other: number_types) -> NumberVar: + """Modulo two numbers. + + Args: + other: The other number. + + Returns: + The number modulo operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("%", (type(self), type(other))) + + return number_modulo_operation(self, +other) + + def __rmod__(self, other: number_types) -> NumberVar: + """Modulo two numbers. + + Args: + other: The other number. + + Returns: + The number modulo operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("%", (type(other), type(self))) + + return number_modulo_operation(+other, self) + + def __pow__(self, other: number_types) -> NumberVar: + """Exponentiate two numbers. + + Args: + other: The other number. + + Returns: + The number exponent operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("**", (type(self), type(other))) + + return number_exponent_operation(self, +other) + + def __rpow__(self, other: number_types) -> NumberVar: + """Exponentiate two numbers. + + Args: + other: The other number. + + Returns: + The number exponent operation. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("**", (type(other), type(self))) + + return number_exponent_operation(+other, self) + + def __neg__(self) -> NumberVar: + """Negate the number. + + Returns: + The number negation operation. + """ + return number_negate_operation(self) # pyright: ignore [reportReturnType] + + def __invert__(self): + """Boolean NOT the number. + + Returns: + The boolean NOT operation. + """ + return boolean_not_operation(self.bool()) + + def __pos__(self) -> NumberVar: + """Positive the number. + + Returns: + The number. + """ + return self + + def __round__(self, ndigits: int | NumberVar = 0) -> NumberVar: + """Round the number. + + Args: + ndigits: The number of digits to round. + + Returns: + The number round operation. + """ + if not isinstance(ndigits, NUMBER_TYPES): + raise_unsupported_operand_types("round", (type(self), type(ndigits))) + + return number_round_operation(self, +ndigits) + + def __ceil__(self): + """Ceil the number. + + Returns: + The number ceil operation. + """ + return number_ceil_operation(self) + + def __floor__(self): + """Floor the number. + + Returns: + The number floor operation. + """ + return number_floor_operation(self) + + def __trunc__(self): + """Trunc the number. + + Returns: + The number trunc operation. + """ + return number_trunc_operation(self) + + def __lt__(self, other: number_types) -> BooleanVar: + """Less than comparison. + + Args: + other: The other number. + + Returns: + The result of the comparison. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("<", (type(self), type(other))) + return less_than_operation(+self, +other) + + def __le__(self, other: number_types) -> BooleanVar: + """Less than or equal comparison. + + Args: + other: The other number. + + Returns: + The result of the comparison. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types("<=", (type(self), type(other))) + return less_than_or_equal_operation(+self, +other) + + def __eq__(self, other: Any): + """Equal comparison. + + Args: + other: The other number. + + Returns: + The result of the comparison. + """ + if isinstance(other, NUMBER_TYPES): + return equal_operation(+self, +other) + return equal_operation(self, other) + + def __ne__(self, other: Any): + """Not equal comparison. + + Args: + other: The other number. + + Returns: + The result of the comparison. + """ + if isinstance(other, NUMBER_TYPES): + return not_equal_operation(+self, +other) + return not_equal_operation(self, other) + + def __gt__(self, other: number_types) -> BooleanVar: + """Greater than comparison. + + Args: + other: The other number. + + Returns: + The result of the comparison. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types(">", (type(self), type(other))) + return greater_than_operation(+self, +other) + + def __ge__(self, other: number_types) -> BooleanVar: + """Greater than or equal comparison. + + Args: + other: The other number. + + Returns: + The result of the comparison. + """ + if not isinstance(other, NUMBER_TYPES): + raise_unsupported_operand_types(">=", (type(self), type(other))) + return greater_than_or_equal_operation(+self, +other) + + def _is_strict_float(self) -> bool: + """Check if the number is a float. + + Returns: + bool: True if the number is a float. + """ + return safe_issubclass(self._var_type, float) + + def _is_strict_int(self) -> bool: + """Check if the number is an int. + + Returns: + bool: True if the number is an int. + """ + return safe_issubclass(self._var_type, int) + + def __format__(self, format_spec: str) -> str: + """Format the number. + + Args: + format_spec: The format specifier. + + Returns: + The formatted number. + + Raises: + VarValueError: If the format specifier is not supported. + """ + from .sequence import ( + get_decimal_string_operation, + get_decimal_string_separator_operation, + ) + + separator = "" + + if format_spec and format_spec[:1] == ",": + separator = "," + format_spec = format_spec[1:] + elif format_spec and format_spec[:1] == "_": + separator = "_" + format_spec = format_spec[1:] + + if ( + format_spec + and format_spec[-1] == "f" + and format_spec[0] == "." + and format_spec[1:-1].isdigit() + ): + how_many_decimals = int(format_spec[1:-1]) + return f"{get_decimal_string_operation(self, Var.create(how_many_decimals), Var.create(separator))}" + + if not format_spec and separator: + return ( + f"{get_decimal_string_separator_operation(self, Var.create(separator))}" + ) + + if format_spec: + msg = ( + f"Unknown format code '{format_spec}' for object of type 'NumberVar'. It is only supported to use ',', '_', and '.f' for float numbers." + "If possible, use computed variables instead: https://reflex.dev/docs/vars/computed-vars/" + ) + raise VarValueError(msg) + + return super().__format__(format_spec) + + +def binary_number_operation( + func: Callable[[NumberVar, NumberVar], str], +) -> Callable[[number_types, number_types], NumberVar]: + """Decorator to create a binary number operation. + + Args: + func: The binary number operation function. + + Returns: + The binary number operation. + """ + + @var_operation + def operation(lhs: NumberVar, rhs: NumberVar): + return var_operation_return( + js_expression=func(lhs, rhs), + var_type=unionize(lhs._var_type, rhs._var_type), + ) + + def wrapper(lhs: number_types, rhs: number_types) -> NumberVar: + """Create the binary number operation. + + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The binary number operation. + """ + return operation(lhs, rhs) # pyright: ignore [reportReturnType, reportArgumentType] + + return wrapper + + +@binary_number_operation +def number_add_operation(lhs: NumberVar, rhs: NumberVar): + """Add two numbers. + + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The number addition operation. + """ + return f"({lhs} + {rhs})" + + +@binary_number_operation +def number_subtract_operation(lhs: NumberVar, rhs: NumberVar): + """Subtract two numbers. + + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The number subtraction operation. + """ + return f"({lhs} - {rhs})" + + +@var_operation +def number_abs_operation(value: NumberVar): + """Get the absolute value of the number. + + Args: + value: The number. + + Returns: + The number absolute operation. + """ + return var_operation_return( + js_expression=f"Math.abs({value})", var_type=value._var_type + ) + + +@binary_number_operation +def number_multiply_operation(lhs: NumberVar, rhs: NumberVar): + """Multiply two numbers. + + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The number multiplication operation. + """ + return f"({lhs} * {rhs})" + + +@var_operation +def number_negate_operation( + value: NumberVar[NUMBER_T], +) -> CustomVarOperationReturn[NUMBER_T]: + """Negate the number. + + Args: + value: The number. + + Returns: + The number negation operation. + """ + return var_operation_return(js_expression=f"-({value})", var_type=value._var_type) + + +@binary_number_operation +def number_true_division_operation(lhs: NumberVar, rhs: NumberVar): + """Divide two numbers. + + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The number true division operation. + """ + return f"({lhs} / {rhs})" + + +@binary_number_operation +def number_floor_division_operation(lhs: NumberVar, rhs: NumberVar): + """Floor divide two numbers. + + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The number floor division operation. + """ + return f"Math.floor({lhs} / {rhs})" + + +@binary_number_operation +def number_modulo_operation(lhs: NumberVar, rhs: NumberVar): + """Modulo two numbers. + + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The number modulo operation. + """ + return f"({lhs} % {rhs})" + + +@binary_number_operation +def number_exponent_operation(lhs: NumberVar, rhs: NumberVar): + """Exponentiate two numbers. + + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The number exponent operation. + """ + return f"({lhs} ** {rhs})" + + +@var_operation +def number_round_operation(value: NumberVar, ndigits: NumberVar | int): + """Round the number. + + Args: + value: The number. + ndigits: The number of digits. + + Returns: + The number round operation. + """ + if (isinstance(ndigits, LiteralNumberVar) and ndigits._var_value == 0) or ( + isinstance(ndigits, int) and ndigits == 0 + ): + return var_operation_return(js_expression=f"Math.round({value})", var_type=int) + return var_operation_return( + js_expression=f"(+{value}.toFixed({ndigits}))", var_type=float + ) + + +@var_operation +def number_ceil_operation(value: NumberVar): + """Ceil the number. + + Args: + value: The number. + + Returns: + The number ceil operation. + """ + return var_operation_return(js_expression=f"Math.ceil({value})", var_type=int) + + +@var_operation +def number_floor_operation(value: NumberVar): + """Floor the number. + + Args: + value: The number. + + Returns: + The number floor operation. + """ + return var_operation_return(js_expression=f"Math.floor({value})", var_type=int) + + +@var_operation +def number_trunc_operation(value: NumberVar): + """Trunc the number. + + Args: + value: The number. + + Returns: + The number trunc operation. + """ + return var_operation_return(js_expression=f"Math.trunc({value})", var_type=int) + + +class BooleanVar(NumberVar[bool], python_types=bool): + """Base class for immutable boolean vars.""" + + def __invert__(self): + """NOT the boolean. + + Returns: + The boolean NOT operation. + """ + return boolean_not_operation(self) + + def __int__(self): + """Convert the boolean to an int. + + Returns: + The boolean to int operation. + """ + return boolean_to_number_operation(self) + + def __pos__(self): + """Convert the boolean to an int. + + Returns: + The boolean to int operation. + """ + return boolean_to_number_operation(self) + + def bool(self) -> BooleanVar: + """Boolean conversion. + + Returns: + The boolean value of the boolean. + """ + return self + + def __lt__(self, other: Any): + """Less than comparison. + + Args: + other: The other boolean. + + Returns: + The result of the comparison. + """ + return +self < other + + def __le__(self, other: Any): + """Less than or equal comparison. + + Args: + other: The other boolean. + + Returns: + The result of the comparison. + """ + return +self <= other + + def __gt__(self, other: Any): + """Greater than comparison. + + Args: + other: The other boolean. + + Returns: + The result of the comparison. + """ + return +self > other + + def __ge__(self, other: Any): + """Greater than or equal comparison. + + Args: + other: The other boolean. + + Returns: + The result of the comparison. + """ + return +self >= other + + +@var_operation +def boolean_to_number_operation(value: BooleanVar): + """Convert the boolean to a number. + + Args: + value: The boolean. + + Returns: + The boolean to number operation. + """ + return var_operation_return(js_expression=f"Number({value})", var_type=int) + + +def comparison_operator( + func: Callable[[Var, Var], str], +) -> Callable[[Var | Any, Var | Any], BooleanVar]: + """Decorator to create a comparison operation. + + Args: + func: The comparison operation function. + + Returns: + The comparison operation. + """ + + @var_operation + def operation(lhs: Var, rhs: Var): + return var_operation_return( + js_expression=func(lhs, rhs), + var_type=bool, + ) + + def wrapper(lhs: Var | Any, rhs: Var | Any) -> BooleanVar: + """Create the comparison operation. + + Args: + lhs: The first value. + rhs: The second value. + + Returns: + The comparison operation. + """ + return operation(lhs, rhs) + + return wrapper + + +@comparison_operator +def greater_than_operation(lhs: Var, rhs: Var): + """Greater than comparison. + + Args: + lhs: The first value. + rhs: The second value. + + Returns: + The result of the comparison. + """ + return f"({lhs} > {rhs})" + + +@comparison_operator +def greater_than_or_equal_operation(lhs: Var, rhs: Var): + """Greater than or equal comparison. + + Args: + lhs: The first value. + rhs: The second value. + + Returns: + The result of the comparison. + """ + return f"({lhs} >= {rhs})" + + +@comparison_operator +def less_than_operation(lhs: Var, rhs: Var): + """Less than comparison. + + Args: + lhs: The first value. + rhs: The second value. + + Returns: + The result of the comparison. + """ + return f"({lhs} < {rhs})" + + +@comparison_operator +def less_than_or_equal_operation(lhs: Var, rhs: Var): + """Less than or equal comparison. + + Args: + lhs: The first value. + rhs: The second value. + + Returns: + The result of the comparison. + """ + return f"({lhs} <= {rhs})" + + +@comparison_operator +def equal_operation(lhs: Var, rhs: Var): + """Equal comparison. + + Args: + lhs: The first value. + rhs: The second value. + + Returns: + The result of the comparison. + """ + return f"({lhs}?.valueOf?.() === {rhs}?.valueOf?.())" + + +@comparison_operator +def not_equal_operation(lhs: Var, rhs: Var): + """Not equal comparison. + + Args: + lhs: The first value. + rhs: The second value. + + Returns: + The result of the comparison. + """ + return f"({lhs}?.valueOf?.() !== {rhs}?.valueOf?.())" + + +@var_operation +def boolean_not_operation(value: BooleanVar): + """Boolean NOT the boolean. + + Args: + value: The boolean. + + Returns: + The boolean NOT operation. + """ + return var_operation_return(js_expression=f"!({value})", var_type=bool) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralNumberVar(LiteralVar, NumberVar[NUMBER_T]): + """Base class for immutable literal number vars.""" + + _var_value: float | int | decimal.Decimal = dataclasses.field(default=0) + + def json(self) -> str: + """Get the JSON representation of the var. + + Returns: + The JSON representation of the var. + + Raises: + PrimitiveUnserializableToJSONError: If the var is unserializable to JSON. + """ + if isinstance(self._var_value, decimal.Decimal): + return json.dumps(float(self._var_value)) + if math.isinf(self._var_value) or math.isnan(self._var_value): + msg = f"No valid JSON representation for {self}" + raise PrimitiveUnserializableToJSONError(msg) + return json.dumps(self._var_value) + + def __hash__(self) -> int: + """Calculate the hash value of the object. + + Returns: + int: The hash value of the object. + """ + return hash((type(self).__name__, self._var_value)) + + @classmethod + def _get_all_var_data_without_creating_var( + cls, value: float | int | decimal.Decimal + ) -> VarData | None: + """Get all the var data without creating the var. + + Args: + value: The value of the var. + + Returns: + The var data. + """ + return None + + @classmethod + def create( + cls, value: float | int | decimal.Decimal, _var_data: VarData | None = None + ): + """Create the number var. + + Args: + value: The value of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The number var. + """ + if math.isinf(value): + js_expr = "Infinity" if value > 0 else "-Infinity" + elif math.isnan(value): + js_expr = "NaN" + else: + js_expr = str(value) + + return cls( + _js_expr=js_expr, + _var_type=type(value), + _var_data=_var_data, + _var_value=value, + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralBooleanVar(LiteralVar, BooleanVar): + """Base class for immutable literal boolean vars.""" + + _var_value: bool = dataclasses.field(default=False) + + def json(self) -> str: + """Get the JSON representation of the var. + + Returns: + The JSON representation of the var. + """ + return "true" if self._var_value else "false" + + def __hash__(self) -> int: + """Calculate the hash value of the object. + + Returns: + int: The hash value of the object. + """ + return hash((type(self).__name__, self._var_value)) + + @classmethod + def _get_all_var_data_without_creating_var(cls, value: bool) -> VarData | None: + """Get all the var data without creating the var. + + Args: + value: The value of the var. + + Returns: + The var data. + """ + return None + + @classmethod + def create(cls, value: bool, _var_data: VarData | None = None): + """Create the boolean var. + + Args: + value: The value of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The boolean var. + """ + return cls( + _js_expr="true" if value else "false", + _var_type=bool, + _var_data=_var_data, + _var_value=value, + ) + + +number_types = NumberVar | int | float | decimal.Decimal +boolean_types = BooleanVar | bool + + +_IS_TRUE_IMPORT: ImportDict = { + f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")], +} + +_IS_NOT_NULL_OR_UNDEFINED_IMPORT: ImportDict = { + f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isNotNullOrUndefined")], +} + + +@var_operation +def boolify(value: Var): + """Convert the value to a boolean. + + Args: + value: The value. + + Returns: + The boolean value. + """ + return var_operation_return( + js_expression=f"isTrue({value})", + var_type=bool, + var_data=VarData(imports=_IS_TRUE_IMPORT), + ) + + +@var_operation +def is_not_none_operation(value: Var): + """Check if the value is not None. + + Args: + value: The value. + + Returns: + The boolean value. + """ + return var_operation_return( + js_expression=f"isNotNullOrUndefined({value})", + var_type=bool, + var_data=VarData(imports=_IS_NOT_NULL_OR_UNDEFINED_IMPORT), + ) + + +T = TypeVar("T") +U = TypeVar("U") + + +@var_operation +def ternary_operation( + condition: Var[bool], if_true: Var[T], if_false: Var[U] +) -> CustomVarOperationReturn[T | U]: + """Create a ternary operation. + + Args: + condition: The condition. + if_true: The value if the condition is true. + if_false: The value if the condition is false. + + Returns: + The ternary operation. + """ + type_value: type[T] | type[U] = unionize(if_true._var_type, if_false._var_type) + value: CustomVarOperationReturn[T | U] = var_operation_return( + js_expression=f"({condition} ? {if_true} : {if_false})", + var_type=type_value, + ) + return value + + +NUMBER_TYPES = (int, float, decimal.Decimal, NumberVar) diff --git a/packages/reflex-core/src/reflex_core/vars/object.py b/packages/reflex-core/src/reflex_core/vars/object.py new file mode 100644 index 00000000000..1e02cee562d --- /dev/null +++ b/packages/reflex-core/src/reflex_core/vars/object.py @@ -0,0 +1,647 @@ +"""Classes for immutable object vars.""" + +from __future__ import annotations + +import collections.abc +import dataclasses +import typing +from collections.abc import Mapping +from importlib.util import find_spec +from typing import ( + Any, + NoReturn, + TypeVar, + get_args, + get_type_hints, + is_typeddict, + overload, +) + +from rich.markup import escape + +from reflex_core.utils import types +from reflex_core.utils.exceptions import VarAttributeError +from reflex_core.utils.types import ( + GenericType, + get_attribute_access_type, + get_origin, + safe_issubclass, + unionize, +) + +from .base import ( + CachedVarOperation, + LiteralVar, + Var, + VarData, + cached_property_no_lock, + figure_out_type, + var_operation, + var_operation_return, +) +from .number import BooleanVar, NumberVar, raise_unsupported_operand_types +from .sequence import ArrayVar, LiteralArrayVar, StringVar + +OBJECT_TYPE = TypeVar("OBJECT_TYPE", covariant=True) + +KEY_TYPE = TypeVar("KEY_TYPE") +VALUE_TYPE = TypeVar("VALUE_TYPE") + +ARRAY_INNER_TYPE = TypeVar("ARRAY_INNER_TYPE") + +OTHER_KEY_TYPE = TypeVar("OTHER_KEY_TYPE") + + +def _determine_value_type(var_type: GenericType): + origin_var_type = get_origin(var_type) or var_type + + if origin_var_type in types.UnionTypes: + return unionize(*[ + _determine_value_type(arg) + for arg in get_args(var_type) + if arg is not type(None) + ]) + + if is_typeddict(origin_var_type) or dataclasses.is_dataclass(origin_var_type): + annotations = get_type_hints(origin_var_type) + return unionize(*annotations.values()) + + if origin_var_type in [dict, Mapping, collections.abc.Mapping]: + args = get_args(var_type) + return args[1] if args else Any + + return Any + + +PYTHON_TYPES = (Mapping,) +if find_spec("pydantic"): + import pydantic + + PYTHON_TYPES += (pydantic.BaseModel,) + + +class ObjectVar(Var[OBJECT_TYPE], python_types=PYTHON_TYPES): + """Base class for immutable object vars.""" + + def _key_type(self) -> type: + """Get the type of the keys of the object. + + Returns: + The type of the keys of the object. + """ + return str + + @overload + def _value_type( + self: ObjectVar[Mapping[Any, VALUE_TYPE]], + ) -> type[VALUE_TYPE]: ... + + @overload + def _value_type(self) -> GenericType: ... + + def _value_type(self) -> GenericType: + """Get the type of the values of the object. + + Returns: + The type of the values of the object. + """ + return _determine_value_type(self._var_type) + + def keys(self) -> ArrayVar[list[str]]: + """Get the keys of the object. + + Returns: + The keys of the object. + """ + return object_keys_operation(self) + + @overload + def values( + self: ObjectVar[Mapping[Any, VALUE_TYPE]], + ) -> ArrayVar[list[VALUE_TYPE]]: ... + + @overload + def values(self) -> ArrayVar: ... + + def values(self) -> ArrayVar: + """Get the values of the object. + + Returns: + The values of the object. + """ + return object_values_operation(self) + + @overload + def entries( + self: ObjectVar[Mapping[Any, VALUE_TYPE]], + ) -> ArrayVar[list[tuple[str, VALUE_TYPE]]]: ... + + @overload + def entries(self) -> ArrayVar: ... + + def entries(self) -> ArrayVar: + """Get the entries of the object. + + Returns: + The entries of the object. + """ + return object_entries_operation(self) + + items = entries + + def length(self) -> NumberVar[int]: + """Get the length of the object. + + Returns: + The length of the object. + """ + return self.keys().length() + + def merge(self, other: ObjectVar): + """Merge two objects. + + Args: + other: The other object to merge. + + Returns: + The merged object. + """ + return object_merge_operation(self, other) + + # NoReturn is used here to catch when key value is Any + @overload + def __getitem__( # pyright: ignore [reportOverlappingOverload] + self: ObjectVar[Mapping[Any, NoReturn]], + key: Var | Any, + ) -> Var: ... + + @overload + def __getitem__( + self: (ObjectVar[Mapping[Any, bool]]), + key: Var | Any, + ) -> BooleanVar: ... + + @overload + def __getitem__( + self: ( + ObjectVar[Mapping[Any, int]] + | ObjectVar[Mapping[Any, float]] + | ObjectVar[Mapping[Any, int | float]] + ), + key: Var | Any, + ) -> NumberVar: ... + + @overload + def __getitem__( + self: ObjectVar[Mapping[Any, str]], + key: Var | Any, + ) -> StringVar: ... + + @overload + def __getitem__( + self: ObjectVar[Mapping[Any, list[ARRAY_INNER_TYPE]]], + key: Var | Any, + ) -> ArrayVar[list[ARRAY_INNER_TYPE]]: ... + + @overload + def __getitem__( + self: ObjectVar[Mapping[Any, tuple[ARRAY_INNER_TYPE, ...]]], + key: Var | Any, + ) -> ArrayVar[tuple[ARRAY_INNER_TYPE, ...]]: ... + + @overload + def __getitem__( + self: ObjectVar[Mapping[Any, Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]], + key: Var | Any, + ) -> ObjectVar[Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]: ... + + @overload + def __getitem__( + self: ObjectVar[Mapping[Any, VALUE_TYPE]], + key: Var | Any, + ) -> Var[VALUE_TYPE]: ... + + def __getitem__(self, key: Var | Any) -> Var: + """Get an item from the object. + + Args: + key: The key to get from the object. + + Returns: + The item from the object. + """ + from .sequence import LiteralStringVar + + if not isinstance(key, (StringVar, str, int, NumberVar)) or ( + isinstance(key, NumberVar) and key._is_strict_float() + ): + raise_unsupported_operand_types("[]", (type(self), type(key))) + if isinstance(key, str) and isinstance(Var.create(key), LiteralStringVar): + return self.__getattr__(key) + return ObjectItemOperation.create(self, key).guess_type() + + def get(self, key: Var | Any, default: Var | Any | None = None) -> Var: + """Get an item from the object. + + Args: + key: The key to get from the object. + default: The default value if the key is not found. + + Returns: + The item from the object. + """ + from reflex_components_core.core.cond import cond + + if default is None: + default = Var.create(None) + + value = self.__getitem__(key) # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue,reportUnknownMemberType] + + return cond( # pyright: ignore[reportUnknownVariableType] + value, + value, + default, + ) + + # NoReturn is used here to catch when key value is Any + @overload + def __getattr__( # pyright: ignore [reportOverlappingOverload] + self: ObjectVar[Mapping[Any, NoReturn]], + name: str, + ) -> Var: ... + + @overload + def __getattr__( + self: ( + ObjectVar[Mapping[Any, int]] + | ObjectVar[Mapping[Any, float]] + | ObjectVar[Mapping[Any, int | float]] + ), + name: str, + ) -> NumberVar: ... + + @overload + def __getattr__( + self: ObjectVar[Mapping[Any, str]], + name: str, + ) -> StringVar: ... + + @overload + def __getattr__( + self: ObjectVar[Mapping[Any, list[ARRAY_INNER_TYPE]]], + name: str, + ) -> ArrayVar[list[ARRAY_INNER_TYPE]]: ... + + @overload + def __getattr__( + self: ObjectVar[Mapping[Any, tuple[ARRAY_INNER_TYPE, ...]]], + name: str, + ) -> ArrayVar[tuple[ARRAY_INNER_TYPE, ...]]: ... + + @overload + def __getattr__( + self: ObjectVar[Mapping[Any, Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]], + name: str, + ) -> ObjectVar[Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]: ... + + @overload + def __getattr__( + self: ObjectVar, + name: str, + ) -> ObjectItemOperation: ... + + def __getattr__(self, name: str) -> Var: + """Get an attribute of the var. + + Args: + name: The name of the attribute. + + Returns: + The attribute of the var. + + Raises: + VarAttributeError: The State var has no such attribute or may have been annotated wrongly. + """ + if name.startswith("__") and name.endswith("__"): + return getattr(super(type(self), self), name) + + var_type = self._var_type + + var_type = types.value_inside_optional(var_type) + + fixed_type = get_origin(var_type) or var_type + + if ( + is_typeddict(fixed_type) + or ( + isinstance(fixed_type, type) + and not safe_issubclass(fixed_type, Mapping) + ) + or (fixed_type in types.UnionTypes) + ): + attribute_type = get_attribute_access_type(var_type, name) + if attribute_type is None: + msg = ( + f"The State var `{self!s}` of type {escape(str(self._var_type))} has no attribute '{name}' or may have been annotated " + f"wrongly." + ) + raise VarAttributeError(msg) + return ObjectItemOperation.create(self, name, attribute_type).guess_type() + return ObjectItemOperation.create(self, name).guess_type() + + def contains(self, key: Var | Any) -> BooleanVar: + """Check if the object contains a key. + + Args: + key: The key to check. + + Returns: + The result of the check. + """ + return object_has_own_property_operation(self, key) + + +class RestProp(ObjectVar[dict[str, Any]]): + """A special object var representing forwarded rest props.""" + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): + """Base class for immutable literal object vars.""" + + _var_value: Mapping[Var | Any, Var | Any] = dataclasses.field(default_factory=dict) + + def _key_type(self) -> type: + """Get the type of the keys of the object. + + Returns: + The type of the keys of the object. + """ + args_list = typing.get_args(self._var_type) + return args_list[0] if args_list else Any # pyright: ignore [reportReturnType] + + def _value_type(self) -> type: + """Get the type of the values of the object. + + Returns: + The type of the values of the object. + """ + args_list = typing.get_args(self._var_type) + return args_list[1] if args_list else Any # pyright: ignore [reportReturnType] + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + return ( + "({ " + + ", ".join([ + f"[{LiteralVar.create(key)!s}] : {LiteralVar.create(value)!s}" + for key, value in self._var_value.items() + ]) + + " })" + ) + + def json(self) -> str: + """Get the JSON representation of the object. + + Returns: + The JSON representation of the object. + + Raises: + TypeError: The keys and values of the object must be literal vars to get the JSON representation + """ + keys_and_values = [] + for key, value in self._var_value.items(): + key = LiteralVar.create(key) + value = LiteralVar.create(value) + if not isinstance(key, LiteralVar) or not isinstance(value, LiteralVar): + msg = "The keys and values of the object must be literal vars to get the JSON representation." + raise TypeError(msg) + keys_and_values.append(f"{key.json()}:{value.json()}") + return "{" + ", ".join(keys_and_values) + "}" + + def __hash__(self) -> int: + """Get the hash of the var. + + Returns: + The hash of the var. + """ + return hash((type(self).__name__, self._js_expr)) + + @classmethod + def _get_all_var_data_without_creating_var( + cls, + value: Mapping, + ) -> VarData | None: + """Get all the var data without creating a var. + + Args: + value: The value to get the var data from. + + Returns: + The var data. + """ + return VarData.merge( + LiteralArrayVar._get_all_var_data_without_creating_var(value), + LiteralArrayVar._get_all_var_data_without_creating_var(value.values()), + ) + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> VarData | None: + """Get all the var data. + + Returns: + The var data. + """ + return VarData.merge( + LiteralArrayVar._get_all_var_data_without_creating_var(self._var_value), + LiteralArrayVar._get_all_var_data_without_creating_var( + self._var_value.values() + ), + self._var_data, + ) + + @classmethod + def create( + cls, + _var_value: Mapping, + _var_type: type[OBJECT_TYPE] | None = None, + _var_data: VarData | None = None, + ) -> LiteralObjectVar[OBJECT_TYPE]: + """Create the literal object var. + + Args: + _var_value: The value of the var. + _var_type: The type of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The literal object var. + + Raises: + TypeError: If the value is not a mapping type or a dataclass. + """ + if not isinstance(_var_value, collections.abc.Mapping): + from reflex_core.utils.serializers import serialize + + serialized = serialize(_var_value, get_type=False) + if not isinstance(serialized, collections.abc.Mapping): + msg = f"Expected a mapping type or a dataclass, got {_var_value!r} of type {type(_var_value).__name__}." + raise TypeError(msg) + + return LiteralObjectVar( + _js_expr="", + _var_type=(type(_var_value) if _var_type is None else _var_type), + _var_data=_var_data, + _var_value=serialized, + ) + + return LiteralObjectVar( + _js_expr="", + _var_type=(figure_out_type(_var_value) if _var_type is None else _var_type), + _var_data=_var_data, + _var_value=_var_value, + ) + + +@var_operation +def object_keys_operation(value: ObjectVar): + """Get the keys of an object. + + Args: + value: The object to get the keys from. + + Returns: + The keys of the object. + """ + return var_operation_return( + js_expression=f"Object.keys({value} ?? {{}})", + var_type=list[str], + ) + + +@var_operation +def object_values_operation(value: ObjectVar): + """Get the values of an object. + + Args: + value: The object to get the values from. + + Returns: + The values of the object. + """ + return var_operation_return( + js_expression=f"Object.values({value} ?? {{}})", + var_type=list[value._value_type()], + ) + + +@var_operation +def object_entries_operation(value: ObjectVar): + """Get the entries of an object. + + Args: + value: The object to get the entries from. + + Returns: + The entries of the object. + """ + return var_operation_return( + js_expression=f"Object.entries({value} ?? {{}})", + var_type=list[tuple[str, value._value_type()]], + ) + + +@var_operation +def object_merge_operation(lhs: ObjectVar, rhs: ObjectVar): + """Merge two objects. + + Args: + lhs: The first object to merge. + rhs: The second object to merge. + + Returns: + The merged object. + """ + return var_operation_return( + js_expression=f"({{...{lhs}, ...{rhs}}})", + var_type=Mapping[ + lhs._key_type() | rhs._key_type(), + lhs._value_type() | rhs._value_type(), + ], + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class ObjectItemOperation(CachedVarOperation, Var): + """Operation to get an item from an object.""" + + _object: ObjectVar = dataclasses.field( + default_factory=lambda: LiteralObjectVar.create({}) + ) + _key: Var | Any = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the operation. + + Returns: + The name of the operation. + """ + return f"{self._object!s}?.[{self._key!s}]" + + @classmethod + def create( + cls, + object: ObjectVar, + key: Var | Any, + _var_type: GenericType | None = None, + _var_data: VarData | None = None, + ) -> ObjectItemOperation: + """Create the object item operation. + + Args: + object: The object to get the item from. + key: The key to get from the object. + _var_type: The type of the item. + _var_data: Additional hooks and imports associated with the operation. + + Returns: + The object item operation. + """ + return cls( + _js_expr="", + _var_type=object._value_type() if _var_type is None else _var_type, + _var_data=_var_data, + _object=object, + _key=key if isinstance(key, Var) else LiteralVar.create(key), + ) + + +@var_operation +def object_has_own_property_operation(object: ObjectVar, key: Var): + """Check if an object has a key. + + Args: + object: The object to check. + key: The key to check. + + Returns: + The result of the check. + """ + return var_operation_return( + js_expression=f"{object}.hasOwnProperty({key})", + var_type=bool, + ) diff --git a/packages/reflex-core/src/reflex_core/vars/sequence.py b/packages/reflex-core/src/reflex_core/vars/sequence.py new file mode 100644 index 00000000000..089a2b80813 --- /dev/null +++ b/packages/reflex-core/src/reflex_core/vars/sequence.py @@ -0,0 +1,1841 @@ +"""Collection of string classes and utilities.""" + +from __future__ import annotations + +import collections.abc +import dataclasses +import decimal +import inspect +import json +import re +from collections.abc import Iterable, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Literal, TypeVar, get_args, overload + +from typing_extensions import TypeVar as TypingExtensionsTypeVar + +from reflex_core import constants +from reflex_core.constants.base import REFLEX_VAR_OPENING_TAG +from reflex_core.utils import types +from reflex_core.utils.exceptions import VarTypeError +from reflex_core.utils.types import GenericType, get_origin + +from .base import ( + CachedVarOperation, + CustomVarOperationReturn, + LiteralVar, + Var, + VarData, + _global_vars, + cached_property_no_lock, + figure_out_type, + get_unique_variable_name, + unionize, + var_operation, + var_operation_return, +) +from .number import ( + BooleanVar, + LiteralNumberVar, + NumberVar, + raise_unsupported_operand_types, +) + +if TYPE_CHECKING: + from .base import DATACLASS_TYPE, SQLA_TYPE + from .function import FunctionVar + from .object import ObjectVar + +ARRAY_VAR_TYPE = TypeVar("ARRAY_VAR_TYPE", bound=Sequence, covariant=True) +OTHER_ARRAY_VAR_TYPE = TypeVar("OTHER_ARRAY_VAR_TYPE", bound=Sequence, covariant=True) +MAPPING_VAR_TYPE = TypeVar("MAPPING_VAR_TYPE", bound=Mapping, covariant=True) + +OTHER_TUPLE = TypeVar("OTHER_TUPLE") + +INNER_ARRAY_VAR = TypeVar("INNER_ARRAY_VAR") + + +KEY_TYPE = TypeVar("KEY_TYPE") +VALUE_TYPE = TypeVar("VALUE_TYPE") + + +class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(Sequence, set)): + """Base class for immutable array vars.""" + + def join(self, sep: StringVar | str = "") -> StringVar: + """Join the elements of the array. + + Args: + sep: The separator between elements. + + Returns: + The joined elements. + """ + if not isinstance(sep, (StringVar, str)): + raise_unsupported_operand_types("join", (type(self), type(sep))) + if ( + isinstance(self, LiteralArrayVar) + and ( + len( + args := [ + x + for x in self._var_value + if isinstance(x, (LiteralStringVar, str)) + ] + ) + == len(self._var_value) + ) + and isinstance(sep, (LiteralStringVar, str)) + ): + sep_str = sep._var_value if isinstance(sep, LiteralStringVar) else sep + return LiteralStringVar.create( + sep_str.join( + i._var_value if isinstance(i, LiteralStringVar) else i for i in args + ) + ) + return array_join_operation(self, sep) + + def reverse(self) -> ArrayVar[ARRAY_VAR_TYPE]: + """Reverse the array. + + Returns: + The reversed array. + """ + return array_reverse_operation(self) + + def __add__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> ArrayVar[ARRAY_VAR_TYPE]: + """Concatenate two arrays. + + Parameters: + other: The other array to concatenate. + + Returns: + ArrayConcatOperation: The concatenation of the two arrays. + """ + if not isinstance(other, ArrayVar): + raise_unsupported_operand_types("+", (type(self), type(other))) + + return array_concat_operation(self, other) + + @overload + def __getitem__(self, i: slice) -> ArrayVar[ARRAY_VAR_TYPE]: ... + + @overload + def __getitem__( + self: ( + ArrayVar[tuple[int, OTHER_TUPLE]] + | ArrayVar[tuple[float, OTHER_TUPLE]] + | ArrayVar[tuple[int | float, OTHER_TUPLE]] + ), + i: Literal[0, -2], + ) -> NumberVar: ... + + @overload + def __getitem__( + self: ArrayVar[tuple[Any, bool]], i: Literal[1, -1] + ) -> BooleanVar: ... + + @overload + def __getitem__( + self: ( + ArrayVar[tuple[Any, int]] + | ArrayVar[tuple[Any, float]] + | ArrayVar[tuple[Any, int | float]] + ), + i: Literal[1, -1], + ) -> NumberVar: ... + + @overload + def __getitem__( # pyright: ignore [reportOverlappingOverload] + self: ArrayVar[tuple[str, Any]], i: Literal[0, -2] + ) -> StringVar: ... + + @overload + def __getitem__( + self: ArrayVar[tuple[Any, str]], i: Literal[1, -1] + ) -> StringVar: ... + + @overload + def __getitem__( + self: ArrayVar[tuple[bool, Any]], i: Literal[0, -2] + ) -> BooleanVar: ... + + @overload + def __getitem__( + self: ArrayVar[Sequence[bool]], i: int | NumberVar + ) -> BooleanVar: ... + + @overload + def __getitem__( + self: ( + ArrayVar[Sequence[int]] + | ArrayVar[Sequence[float]] + | ArrayVar[Sequence[int | float]] + ), + i: int | NumberVar, + ) -> NumberVar: ... + + @overload + def __getitem__(self: ArrayVar[Sequence[str]], i: int | NumberVar) -> StringVar: ... + + @overload + def __getitem__( + self: ArrayVar[Sequence[OTHER_ARRAY_VAR_TYPE]], + i: int | NumberVar, + ) -> ArrayVar[OTHER_ARRAY_VAR_TYPE]: ... + + @overload + def __getitem__( + self: ArrayVar[Sequence[MAPPING_VAR_TYPE]], + i: int | NumberVar, + ) -> ObjectVar[MAPPING_VAR_TYPE]: ... + + @overload + def __getitem__( + self: ArrayVar[Sequence[SQLA_TYPE]], + i: int | NumberVar, + ) -> ObjectVar[SQLA_TYPE]: ... + + @overload + def __getitem__( + self: ArrayVar[Sequence[DATACLASS_TYPE]], + i: int | NumberVar, + ) -> ObjectVar[DATACLASS_TYPE]: ... + + @overload + def __getitem__(self, i: int | NumberVar) -> Var: ... + + def __getitem__(self, i: Any) -> ArrayVar[ARRAY_VAR_TYPE] | Var: + """Get a slice of the array. + + Args: + i: The slice. + + Returns: + The array slice operation. + """ + if isinstance(i, slice): + return ArraySliceOperation.create(self, i) + if not isinstance(i, (int, NumberVar)) or ( + isinstance(i, NumberVar) and i._is_strict_float() + ): + raise_unsupported_operand_types("[]", (type(self), type(i))) + return array_item_operation(self, i) + + def length(self) -> NumberVar[int]: + """Get the length of the array. + + Returns: + The length of the array. + """ + return array_length_operation(self) + + @overload + @classmethod + def range(cls, stop: int | NumberVar, /) -> ArrayVar[list[int]]: ... + + @overload + @classmethod + def range( + cls, + start: int | NumberVar, + end: int | NumberVar, + step: int | NumberVar = 1, + /, + ) -> ArrayVar[list[int]]: ... + + @overload + @classmethod + def range( + cls, + first_endpoint: int | NumberVar, + second_endpoint: int | NumberVar | None = None, + step: int | NumberVar | None = None, + ) -> ArrayVar[list[int]]: ... + + @classmethod + def range( + cls, + first_endpoint: int | NumberVar, + second_endpoint: int | NumberVar | None = None, + step: int | NumberVar | None = None, + ) -> ArrayVar[list[int]]: + """Create a range of numbers. + + Args: + first_endpoint: The end of the range if second_endpoint is not provided, otherwise the start of the range. + second_endpoint: The end of the range. + step: The step of the range. + + Returns: + The range of numbers. + """ + if any( + not isinstance(i, (int, NumberVar)) + for i in (first_endpoint, second_endpoint, step) + if i is not None + ): + raise_unsupported_operand_types( + "range", (type(first_endpoint), type(second_endpoint), type(step)) + ) + if second_endpoint is None: + start = 0 + end = first_endpoint + else: + start = first_endpoint + end = second_endpoint + + return array_range_operation(start, end, step or 1) + + @overload + def contains(self, other: Any) -> BooleanVar: ... + + @overload + def contains(self, other: Any, field: StringVar | str) -> BooleanVar: ... + + def contains(self, other: Any, field: Any = None) -> BooleanVar: + """Check if the array contains an element. + + Args: + other: The element to check for. + field: The field to check. + + Returns: + The array contains operation. + """ + if field is not None: + if not isinstance(field, (StringVar, str)): + raise_unsupported_operand_types("contains", (type(self), type(field))) + return array_contains_field_operation(self, other, field) + return array_contains_operation(self, other) + + def pluck(self, field: StringVar | str) -> ArrayVar: + """Pluck a field from the array. + + Args: + field: The field to pluck from the array. + + Returns: + The array pluck operation. + """ + return array_pluck_operation(self, field) + + def __mul__(self, other: NumberVar | int) -> ArrayVar[ARRAY_VAR_TYPE]: + """Multiply the sequence by a number or integer. + + Parameters: + other: The number or integer to multiply the sequence by. + + Returns: + ArrayVar[ARRAY_VAR_TYPE]: The result of multiplying the sequence by the given number or integer. + """ + if not isinstance(other, (NumberVar, int)) or ( + isinstance(other, NumberVar) and other._is_strict_float() + ): + raise_unsupported_operand_types("*", (type(self), type(other))) + + return repeat_array_operation(self, other) + + __rmul__ = __mul__ + + @overload + def __lt__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> BooleanVar: ... + + @overload + def __lt__(self, other: list | tuple) -> BooleanVar: ... + + def __lt__(self, other: Any): + """Check if the array is less than another array. + + Args: + other: The other array. + + Returns: + The array less than operation. + """ + if not isinstance(other, (ArrayVar, list, tuple)): + raise_unsupported_operand_types("<", (type(self), type(other))) + + return array_lt_operation(self, other) + + @overload + def __gt__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> BooleanVar: ... + + @overload + def __gt__(self, other: list | tuple) -> BooleanVar: ... + + def __gt__(self, other: Any): + """Check if the array is greater than another array. + + Args: + other: The other array. + + Returns: + The array greater than operation. + """ + if not isinstance(other, (ArrayVar, list, tuple)): + raise_unsupported_operand_types(">", (type(self), type(other))) + + return array_gt_operation(self, other) + + @overload + def __le__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> BooleanVar: ... + + @overload + def __le__(self, other: list | tuple) -> BooleanVar: ... + + def __le__(self, other: Any): + """Check if the array is less than or equal to another array. + + Args: + other: The other array. + + Returns: + The array less than or equal operation. + """ + if not isinstance(other, (ArrayVar, list, tuple)): + raise_unsupported_operand_types("<=", (type(self), type(other))) + + return array_le_operation(self, other) + + @overload + def __ge__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> BooleanVar: ... + + @overload + def __ge__(self, other: list | tuple) -> BooleanVar: ... + + def __ge__(self, other: Any): + """Check if the array is greater than or equal to another array. + + Args: + other: The other array. + + Returns: + The array greater than or equal operation. + """ + if not isinstance(other, (ArrayVar, list, tuple)): + raise_unsupported_operand_types(">=", (type(self), type(other))) + + return array_ge_operation(self, other) + + def foreach(self, fn: Any): + """Apply a function to each element of the array. + + Args: + fn: The function to apply. + + Returns: + The array after applying the function. + + Raises: + VarTypeError: If the function takes more than one argument. + """ + from .function import ArgsFunctionOperation + + if not callable(fn): + raise_unsupported_operand_types("foreach", (type(self), type(fn))) + # get the number of arguments of the function + num_args = len(inspect.signature(fn).parameters) + if num_args > 1: + msg = "The function passed to foreach should take at most one argument." + raise VarTypeError(msg) + + if num_args == 0: + return_value = fn() + function_var = ArgsFunctionOperation.create((), return_value) + else: + # generic number var + number_var = Var("").to(NumberVar, int) + + first_arg_type = self[number_var]._var_type + + arg_name = get_unique_variable_name() + + # get first argument type + first_arg = Var( + _js_expr=arg_name, + _var_type=first_arg_type, + ).guess_type() + + function_var = ArgsFunctionOperation.create( + (arg_name,), + Var.create(fn(first_arg)), + ) + + return map_array_operation(self, function_var) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralArrayVar(CachedVarOperation, LiteralVar, ArrayVar[ARRAY_VAR_TYPE]): + """Base class for immutable literal array vars.""" + + _var_value: Sequence[Var | Any] = dataclasses.field(default=()) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + return ( + "[" + + ", ".join([ + str(LiteralVar.create(element)) for element in self._var_value + ]) + + "]" + ) + + @classmethod + def _get_all_var_data_without_creating_var(cls, value: Iterable) -> VarData | None: + """Get all the VarData associated with the Var without creating a Var. + + Args: + value: The value to get the VarData for. + + Returns: + The VarData associated with the Var. + """ + return VarData.merge(*[ + LiteralVar._get_all_var_data_without_creating_var_dispatch(element) + for element in value + ]) + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> VarData | None: + """Get all the VarData associated with the Var. + + Returns: + The VarData associated with the Var. + """ + return VarData.merge( + *[ + LiteralVar._get_all_var_data_without_creating_var_dispatch(element) + for element in self._var_value + ], + self._var_data, + ) + + def __hash__(self) -> int: + """Get the hash of the var. + + Returns: + The hash of the var. + """ + return hash((self.__class__.__name__, self._js_expr)) + + def json(self) -> str: + """Get the JSON representation of the var. + + Returns: + The JSON representation of the var. + + Raises: + TypeError: If the array elements are not of type LiteralVar. + """ + elements = [] + for element in self._var_value: + element_var = LiteralVar.create(element) + if not isinstance(element_var, LiteralVar): + msg = f"Array elements must be of type LiteralVar, not {type(element_var)}" + raise TypeError(msg) + elements.append(element_var.json()) + + return "[" + ", ".join(elements) + "]" + + @classmethod + def create( + cls, + value: OTHER_ARRAY_VAR_TYPE, + _var_type: type[OTHER_ARRAY_VAR_TYPE] | None = None, + _var_data: VarData | None = None, + ) -> LiteralArrayVar[OTHER_ARRAY_VAR_TYPE]: + """Create a var from a string value. + + Args: + value: The value to create the var from. + _var_type: The type of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + return LiteralArrayVar( + _js_expr="", + _var_type=figure_out_type(value) if _var_type is None else _var_type, + _var_data=_var_data, + _var_value=value, + ) + + +STRING_TYPE = TypingExtensionsTypeVar("STRING_TYPE", default=str) + + +class StringVar(Var[STRING_TYPE], python_types=str): + """Base class for immutable string vars.""" + + def __add__(self, other: StringVar | str) -> ConcatVarOperation: + """Concatenate two strings. + + Args: + other: The other string. + + Returns: + The string concatenation operation. + """ + if not isinstance(other, (StringVar, str)): + raise_unsupported_operand_types("+", (type(self), type(other))) + + return ConcatVarOperation.create(self, other) + + def __radd__(self, other: StringVar | str) -> ConcatVarOperation: + """Concatenate two strings. + + Args: + other: The other string. + + Returns: + The string concatenation operation. + """ + if not isinstance(other, (StringVar, str)): + raise_unsupported_operand_types("+", (type(other), type(self))) + + return ConcatVarOperation.create(other, self) + + def __mul__(self, other: NumberVar | int) -> StringVar: + """Multiply the sequence by a number or an integer. + + Args: + other: The number or integer to multiply the sequence by. + + Returns: + StringVar: The resulting sequence after multiplication. + """ + if not isinstance(other, (NumberVar, int)): + raise_unsupported_operand_types("*", (type(self), type(other))) + + return (self.split() * other).join() + + def __rmul__(self, other: NumberVar | int) -> StringVar: + """Multiply the sequence by a number or an integer. + + Args: + other: The number or integer to multiply the sequence by. + + Returns: + StringVar: The resulting sequence after multiplication. + """ + if not isinstance(other, (NumberVar, int)): + raise_unsupported_operand_types("*", (type(other), type(self))) + + return (self.split() * other).join() + + @overload + def __getitem__(self, i: slice) -> StringVar: ... + + @overload + def __getitem__(self, i: int | NumberVar) -> StringVar: ... + + def __getitem__(self, i: Any) -> StringVar: + """Get a slice of the string. + + Args: + i: The slice. + + Returns: + The string slice operation. + """ + if isinstance(i, slice): + return self.split()[i].join() + if not isinstance(i, (int, NumberVar)) or ( + isinstance(i, NumberVar) and i._is_strict_float() + ): + raise_unsupported_operand_types("[]", (type(self), type(i))) + return string_item_operation(self, i) + + def length(self) -> NumberVar: + """Get the length of the string. + + Returns: + The string length operation. + """ + return self.split().length() + + def lower(self) -> StringVar: + """Convert the string to lowercase. + + Returns: + The string lower operation. + """ + return string_lower_operation(self) + + def upper(self) -> StringVar: + """Convert the string to uppercase. + + Returns: + The string upper operation. + """ + return string_upper_operation(self) + + def title(self) -> StringVar: + """Convert the string to title case. + + Returns: + The string title operation. + """ + return string_title_operation(self) + + def capitalize(self) -> StringVar: + """Capitalize the string. + + Returns: + The string capitalize operation. + """ + return string_capitalize_operation(self) + + def strip(self) -> StringVar: + """Strip the string. + + Returns: + The string strip operation. + """ + return string_strip_operation(self) + + def reversed(self) -> StringVar: + """Reverse the string. + + Returns: + The string reverse operation. + """ + return self.split().reverse().join() + + def contains( + self, other: StringVar | str, field: StringVar | str | None = None + ) -> BooleanVar: + """Check if the string contains another string. + + Args: + other: The other string. + field: The field to check. + + Returns: + The string contains operation. + """ + if not isinstance(other, (StringVar, str)): + raise_unsupported_operand_types("contains", (type(self), type(other))) + if field is not None: + if not isinstance(field, (StringVar, str)): + raise_unsupported_operand_types("contains", (type(self), type(field))) + return string_contains_field_operation(self, other, field) + return string_contains_operation(self, other) + + def split(self, separator: StringVar | str = "") -> ArrayVar[list[str]]: + """Split the string. + + Args: + separator: The separator. + + Returns: + The string split operation. + """ + if not isinstance(separator, (StringVar, str)): + raise_unsupported_operand_types("split", (type(self), type(separator))) + return string_split_operation(self, separator) + + def startswith(self, prefix: StringVar | str) -> BooleanVar: + """Check if the string starts with a prefix. + + Args: + prefix: The prefix. + + Returns: + The string starts with operation. + """ + if not isinstance(prefix, (StringVar, str)): + raise_unsupported_operand_types("startswith", (type(self), type(prefix))) + return string_starts_with_operation(self, prefix) + + def endswith(self, suffix: StringVar | str) -> BooleanVar: + """Check if the string ends with a suffix. + + Args: + suffix: The suffix. + + Returns: + The string ends with operation. + """ + if not isinstance(suffix, (StringVar, str)): + raise_unsupported_operand_types("endswith", (type(self), type(suffix))) + return string_ends_with_operation(self, suffix) + + def __lt__(self, other: StringVar | str) -> BooleanVar: + """Check if the string is less than another string. + + Args: + other: The other string. + + Returns: + The string less than operation. + """ + if not isinstance(other, (StringVar, str)): + raise_unsupported_operand_types("<", (type(self), type(other))) + + return string_lt_operation(self, other) + + def __gt__(self, other: StringVar | str) -> BooleanVar: + """Check if the string is greater than another string. + + Args: + other: The other string. + + Returns: + The string greater than operation. + """ + if not isinstance(other, (StringVar, str)): + raise_unsupported_operand_types(">", (type(self), type(other))) + + return string_gt_operation(self, other) + + def __le__(self, other: StringVar | str) -> BooleanVar: + """Check if the string is less than or equal to another string. + + Args: + other: The other string. + + Returns: + The string less than or equal operation. + """ + if not isinstance(other, (StringVar, str)): + raise_unsupported_operand_types("<=", (type(self), type(other))) + + return string_le_operation(self, other) + + def __ge__(self, other: StringVar | str) -> BooleanVar: + """Check if the string is greater than or equal to another string. + + Args: + other: The other string. + + Returns: + The string greater than or equal operation. + """ + if not isinstance(other, (StringVar, str)): + raise_unsupported_operand_types(">=", (type(self), type(other))) + + return string_ge_operation(self, other) + + @overload + def replace( # pyright: ignore [reportOverlappingOverload] + self, search_value: StringVar | str, new_value: StringVar | str + ) -> StringVar: ... + + @overload + def replace( + self, search_value: Any, new_value: Any + ) -> CustomVarOperationReturn[StringVar]: ... + + def replace(self, search_value: Any, new_value: Any) -> StringVar: # pyright: ignore [reportInconsistentOverload] + """Replace a string with a value. + + Args: + search_value: The string to search. + new_value: The value to be replaced with. + + Returns: + The string replace operation. + """ + if not isinstance(search_value, (StringVar, str)): + raise_unsupported_operand_types("replace", (type(self), type(search_value))) + if not isinstance(new_value, (StringVar, str)): + raise_unsupported_operand_types("replace", (type(self), type(new_value))) + + return string_replace_operation(self, search_value, new_value) + + +@var_operation +def string_lt_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): + """Check if a string is less than another string. + + Args: + lhs: The left-hand side string. + rhs: The right-hand side string. + + Returns: + The string less than operation. + """ + return var_operation_return(js_expression=f"{lhs} < {rhs}", var_type=bool) + + +@var_operation +def string_gt_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): + """Check if a string is greater than another string. + + Args: + lhs: The left-hand side string. + rhs: The right-hand side string. + + Returns: + The string greater than operation. + """ + return var_operation_return(js_expression=f"{lhs} > {rhs}", var_type=bool) + + +@var_operation +def string_le_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): + """Check if a string is less than or equal to another string. + + Args: + lhs: The left-hand side string. + rhs: The right-hand side string. + + Returns: + The string less than or equal operation. + """ + return var_operation_return(js_expression=f"{lhs} <= {rhs}", var_type=bool) + + +@var_operation +def string_ge_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): + """Check if a string is greater than or equal to another string. + + Args: + lhs: The left-hand side string. + rhs: The right-hand side string. + + Returns: + The string greater than or equal operation. + """ + return var_operation_return(js_expression=f"{lhs} >= {rhs}", var_type=bool) + + +@var_operation +def string_lower_operation(string: StringVar[Any]): + """Convert a string to lowercase. + + Args: + string: The string to convert. + + Returns: + The lowercase string. + """ + return var_operation_return(js_expression=f"{string}.toLowerCase()", var_type=str) + + +@var_operation +def string_upper_operation(string: StringVar[Any]): + """Convert a string to uppercase. + + Args: + string: The string to convert. + + Returns: + The uppercase string. + """ + return var_operation_return(js_expression=f"{string}.toUpperCase()", var_type=str) + + +@var_operation +def string_title_operation(string: StringVar[Any]): + """Convert a string to title case. + + Args: + string: The string to convert. + + Returns: + The title case string. + """ + return var_operation_return( + js_expression=f"{string}.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ')", + var_type=str, + ) + + +@var_operation +def string_capitalize_operation(string: StringVar[Any]): + """Capitalize a string. + + Args: + string: The string to capitalize. + + Returns: + The capitalized string. + """ + return var_operation_return( + js_expression=f"(((s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())({string}))", + var_type=str, + ) + + +@var_operation +def string_strip_operation(string: StringVar[Any]): + """Strip a string. + + Args: + string: The string to strip. + + Returns: + The stripped string. + """ + return var_operation_return(js_expression=f"{string}.trim()", var_type=str) + + +@var_operation +def string_contains_field_operation( + haystack: StringVar[Any], needle: StringVar[Any] | str, field: StringVar[Any] | str +): + """Check if a string contains another string. + + Args: + haystack: The haystack. + needle: The needle. + field: The field to check. + + Returns: + The string contains operation. + """ + return var_operation_return( + js_expression=f"{haystack}.some(obj => obj[{field}] === {needle})", + var_type=bool, + ) + + +@var_operation +def string_contains_operation(haystack: StringVar[Any], needle: StringVar[Any] | str): + """Check if a string contains another string. + + Args: + haystack: The haystack. + needle: The needle. + + Returns: + The string contains operation. + """ + return var_operation_return( + js_expression=f"{haystack}.includes({needle})", var_type=bool + ) + + +@var_operation +def string_starts_with_operation( + full_string: StringVar[Any], prefix: StringVar[Any] | str +): + """Check if a string starts with a prefix. + + Args: + full_string: The full string. + prefix: The prefix. + + Returns: + Whether the string starts with the prefix. + """ + return var_operation_return( + js_expression=f"{full_string}.startsWith({prefix})", var_type=bool + ) + + +@var_operation +def string_ends_with_operation( + full_string: StringVar[Any], suffix: StringVar[Any] | str +): + """Check if a string ends with a suffix. + + Args: + full_string: The full string. + suffix: The suffix. + + Returns: + Whether the string ends with the suffix. + """ + return var_operation_return( + js_expression=f"{full_string}.endsWith({suffix})", var_type=bool + ) + + +@var_operation +def string_item_operation(string: StringVar[Any], index: NumberVar | int): + """Get an item from a string. + + Args: + string: The string. + index: The index of the item. + + Returns: + The item from the string. + """ + return var_operation_return(js_expression=f"{string}?.at?.({index})", var_type=str) + + +@var_operation +def array_join_operation(array: ArrayVar, sep: StringVar[Any] | str = ""): + """Join the elements of an array. + + Args: + array: The array. + sep: The separator. + + Returns: + The joined elements. + """ + return var_operation_return(js_expression=f"{array}.join({sep})", var_type=str) + + +@var_operation +def string_replace_operation( + string: StringVar[Any], search_value: StringVar | str, new_value: StringVar | str +): + """Replace a string with a value. + + Args: + string: The string. + search_value: The string to search. + new_value: The value to be replaced with. + + Returns: + The string replace operation. + """ + return var_operation_return( + js_expression=f"{string}.replaceAll({search_value}, {new_value})", + var_type=str, + ) + + +@var_operation +def get_decimal_string_separator_operation(value: NumberVar, separator: StringVar): + """Get the decimal string separator. + + Args: + value: The number. + separator: The separator. + + Returns: + The decimal string separator. + """ + return var_operation_return( + js_expression=f"({value}.toLocaleString('en-US').replaceAll(',', {separator}))", + var_type=str, + ) + + +@var_operation +def get_decimal_string_operation( + value: NumberVar, decimals: NumberVar, separator: StringVar +): + """Get the decimal string of the number. + + Args: + value: The number. + decimals: The number of decimals. + separator: The separator. + + Returns: + The decimal string of the number. + """ + return var_operation_return( + js_expression=f"({value}.toLocaleString('en-US', ((decimals) => ({{minimumFractionDigits: decimals, maximumFractionDigits: decimals}}))({decimals})).replaceAll(',', {separator}))", + var_type=str, + ) + + +# Compile regex for finding reflex var tags. +_decode_var_pattern_re = ( + rf"{constants.REFLEX_VAR_OPENING_TAG}(.*?){constants.REFLEX_VAR_CLOSING_TAG}" +) +_decode_var_pattern = re.compile(_decode_var_pattern_re, flags=re.DOTALL) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralStringVar(LiteralVar, StringVar[str]): + """Base class for immutable literal string vars.""" + + _var_value: str = dataclasses.field(default="") + + @classmethod + def _get_all_var_data_without_creating_var(cls, value: str) -> VarData | None: + """Get all the VarData associated with the Var without creating a Var. + + Args: + value: The value to get the VarData for. + + Returns: + The VarData associated with the Var. + """ + if REFLEX_VAR_OPENING_TAG not in value: + return None + return cls.create(value)._get_all_var_data() + + @classmethod + def create( + cls, + value: str, + _var_type: GenericType | None = None, + _var_data: VarData | None = None, + ) -> StringVar: + """Create a var from a string value. + + Args: + value: The value to create the var from. + _var_type: The type of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + # Determine var type in case the value is inherited from str. + _var_type = _var_type or type(value) or str + + if REFLEX_VAR_OPENING_TAG in value: + strings_and_vals: list[Var | str] = [] + offset = 0 + + # Find all tags + while m := _decode_var_pattern.search(value): + start, end = m.span() + + strings_and_vals.append(value[:start]) + + serialized_data = m.group(1) + + if serialized_data.isnumeric() or ( + serialized_data[0] == "-" and serialized_data[1:].isnumeric() + ): + # This is a global immutable var. + var = _global_vars[int(serialized_data)] + strings_and_vals.append(var) + value = value[(end + len(var._js_expr)) :] + + offset += end - start + + strings_and_vals.append(value) + + filtered_strings_and_vals = [ + s for s in strings_and_vals if isinstance(s, Var) or s + ] + if len(filtered_strings_and_vals) == 1: + only_string = filtered_strings_and_vals[0] + if isinstance(only_string, str): + return LiteralVar.create(only_string).to(StringVar, _var_type) + return only_string.to(StringVar, only_string._var_type) + + if len( + literal_strings := [ + s + for s in filtered_strings_and_vals + if isinstance(s, (str, LiteralStringVar)) + ] + ) == len(filtered_strings_and_vals): + return LiteralStringVar.create( + "".join( + s._var_value if isinstance(s, LiteralStringVar) else s + for s in literal_strings + ), + _var_type=_var_type, + _var_data=VarData.merge( + _var_data, + *( + s._get_all_var_data() + for s in filtered_strings_and_vals + if isinstance(s, Var) + ), + ), + ) + + concat_result = ConcatVarOperation.create( + *filtered_strings_and_vals, + _var_data=_var_data, + ) + + return ( + concat_result + if _var_type is str + else concat_result.to(StringVar, _var_type) + ) + + return LiteralStringVar( + _js_expr=json.dumps(value), + _var_type=_var_type, + _var_data=_var_data, + _var_value=value, + ) + + def __hash__(self) -> int: + """Get the hash of the var. + + Returns: + The hash of the var. + """ + return hash((type(self).__name__, self._var_value)) + + def json(self) -> str: + """Get the JSON representation of the var. + + Returns: + The JSON representation of the var. + """ + return json.dumps(self._var_value) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class ConcatVarOperation(CachedVarOperation, StringVar[str]): + """Representing a concatenation of literal string vars.""" + + _var_value: tuple[Var, ...] = dataclasses.field(default_factory=tuple) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + list_of_strs: list[str | Var] = [] + last_string = "" + for var in self._var_value: + if isinstance(var, LiteralStringVar): + last_string += var._var_value + else: + if last_string: + list_of_strs.append(last_string) + last_string = "" + list_of_strs.append(var) + + if last_string: + list_of_strs.append(last_string) + + list_of_strs_filtered = [ + str(LiteralVar.create(s)) for s in list_of_strs if isinstance(s, Var) or s + ] + + if len(list_of_strs_filtered) == 1: + return list_of_strs_filtered[0] + + return "(" + "+".join(list_of_strs_filtered) + ")" + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> VarData | None: + """Get all the VarData asVarDatae Var. + + Returns: + The VarData associated with the Var. + """ + return VarData.merge( + *[ + var._get_all_var_data() + for var in self._var_value + if isinstance(var, Var) + ], + self._var_data, + ) + + @classmethod + def create( + cls, + *value: Var | str, + _var_data: VarData | None = None, + ) -> ConcatVarOperation: + """Create a var from a string value. + + Args: + *value: The values to concatenate. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + return cls( + _js_expr="", + _var_type=str, + _var_data=_var_data, + _var_value=tuple(map(LiteralVar.create, value)), + ) + + +@var_operation +def string_split_operation(string: StringVar[Any], sep: StringVar | str = ""): + """Split a string. + + Args: + string: The string to split. + sep: The separator. + + Returns: + The split string. + """ + return var_operation_return( + js_expression=f"{string}.split({sep})", var_type=list[str] + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class ArraySliceOperation(CachedVarOperation, ArrayVar): + """Base class for immutable string vars that are the result of a string slice operation.""" + + _array: ArrayVar = dataclasses.field( + default_factory=lambda: LiteralArrayVar.create([]) + ) + _start: NumberVar | int = dataclasses.field(default_factory=lambda: 0) + _stop: NumberVar | int = dataclasses.field(default_factory=lambda: 0) + _step: NumberVar | int = dataclasses.field(default_factory=lambda: 1) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + + Raises: + ValueError: If the slice step is zero. + """ + start, end, step = self._start, self._stop, self._step + + normalized_start = ( + LiteralVar.create(start) if start is not None else Var(_js_expr="undefined") + ) + normalized_end = ( + LiteralVar.create(end) if end is not None else Var(_js_expr="undefined") + ) + if step is None: + return f"{self._array!s}.slice({normalized_start!s}, {normalized_end!s})" + if not isinstance(step, Var): + if step < 0: + actual_start = end + 1 if end is not None else 0 + actual_end = start + 1 if start is not None else self._array.length() + return str(self._array[actual_start:actual_end].reverse()[::-step]) + if step == 0: + msg = "slice step cannot be zero" + raise ValueError(msg) + return f"{self._array!s}.slice({normalized_start!s}, {normalized_end!s}).filter((_, i) => i % {step!s} === 0)" + + actual_start_reverse = end + 1 if end is not None else 0 + actual_end_reverse = start + 1 if start is not None else self._array.length() + + return f"{self.step!s} > 0 ? {self._array!s}.slice({normalized_start!s}, {normalized_end!s}).filter((_, i) => i % {step!s} === 0) : {self._array!s}.slice({actual_start_reverse!s}, {actual_end_reverse!s}).reverse().filter((_, i) => i % {-step!s} === 0)" + + @classmethod + def create( + cls, + array: ArrayVar, + slice: slice, + _var_data: VarData | None = None, + ) -> ArraySliceOperation: + """Create a var from a string value. + + Args: + array: The array. + slice: The slice. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + return cls( + _js_expr="", + _var_type=array._var_type, + _var_data=_var_data, + _array=array, + _start=slice.start, + _stop=slice.stop, + _step=slice.step, + ) + + +@var_operation +def array_pluck_operation( + array: ArrayVar[ARRAY_VAR_TYPE], + field: StringVar | str, +) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: + """Pluck a field from an array of objects. + + Args: + array: The array to pluck from. + field: The field to pluck from the objects in the array. + + Returns: + The reversed array. + """ + return var_operation_return( + js_expression=f"{array}.map(e=>e?.[{field}])", + var_type=array._var_type, + ) + + +@var_operation +def array_reverse_operation( + array: ArrayVar[ARRAY_VAR_TYPE], +) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: + """Reverse an array. + + Args: + array: The array to reverse. + + Returns: + The reversed array. + """ + return var_operation_return( + js_expression=f"{array}.slice().reverse()", + var_type=array._var_type, + ) + + +@var_operation +def array_lt_operation(lhs: ArrayVar | list | tuple, rhs: ArrayVar | list | tuple): + """Check if an array is less than another array. + + Args: + lhs: The left-hand side array. + rhs: The right-hand side array. + + Returns: + The array less than operation. + """ + return var_operation_return(js_expression=f"{lhs} < {rhs}", var_type=bool) + + +@var_operation +def array_gt_operation(lhs: ArrayVar | list | tuple, rhs: ArrayVar | list | tuple): + """Check if an array is greater than another array. + + Args: + lhs: The left-hand side array. + rhs: The right-hand side array. + + Returns: + The array greater than operation. + """ + return var_operation_return(js_expression=f"{lhs} > {rhs}", var_type=bool) + + +@var_operation +def array_le_operation(lhs: ArrayVar | list | tuple, rhs: ArrayVar | list | tuple): + """Check if an array is less than or equal to another array. + + Args: + lhs: The left-hand side array. + rhs: The right-hand side array. + + Returns: + The array less than or equal operation. + """ + return var_operation_return(js_expression=f"{lhs} <= {rhs}", var_type=bool) + + +@var_operation +def array_ge_operation(lhs: ArrayVar | list | tuple, rhs: ArrayVar | list | tuple): + """Check if an array is greater than or equal to another array. + + Args: + lhs: The left-hand side array. + rhs: The right-hand side array. + + Returns: + The array greater than or equal operation. + """ + return var_operation_return(js_expression=f"{lhs} >= {rhs}", var_type=bool) + + +@var_operation +def array_length_operation(array: ArrayVar): + """Get the length of an array. + + Args: + array: The array. + + Returns: + The length of the array. + """ + return var_operation_return( + js_expression=f"{array}.length", + var_type=int, + ) + + +def is_tuple_type(t: GenericType) -> bool: + """Check if a type is a tuple type. + + Args: + t: The type to check. + + Returns: + Whether the type is a tuple type. + """ + return get_origin(t) is tuple + + +def _determine_value_of_array_index( + var_type: GenericType, index: int | float | decimal.Decimal | None = None +): + """Determine the value of an array index. + + Args: + var_type: The type of the array. + index: The index of the array. + + Returns: + The value of the array index. + """ + origin_var_type = get_origin(var_type) or var_type + if origin_var_type in types.UnionTypes: + return unionize(*[ + _determine_value_of_array_index(t, index) + for t in get_args(var_type) + if t is not type(None) + ]) + if origin_var_type is range: + return int + if origin_var_type in [ + Sequence, + Iterable, + list, + set, + collections.abc.Sequence, + collections.abc.Iterable, + ]: + args = get_args(var_type) + return args[0] if args else Any + if origin_var_type is tuple: + args = get_args(var_type) + if len(args) == 2 and args[1] is ...: + return args[0] + return ( + args[int(index) % len(args)] + if args and index is not None + else (unionize(*args) if args else Any) + ) + return Any + + +@var_operation +def array_item_operation(array: ArrayVar, index: NumberVar | int): + """Get an item from an array. + + Args: + array: The array. + index: The index of the item. + + Returns: + The item from the array. + """ + element_type = _determine_value_of_array_index( + array._var_type, + ( + index + if isinstance(index, int) + else (index._var_value if isinstance(index, LiteralNumberVar) else None) + ), + ) + + return var_operation_return( + js_expression=f"{array!s}?.at?.({index!s})", + var_type=element_type, + ) + + +@var_operation +def array_range_operation( + start: NumberVar | int, stop: NumberVar | int, step: NumberVar | int +): + """Create a range of numbers. + + Args: + start: The start of the range. + stop: The end of the range. + step: The step of the range. + + Returns: + The range of numbers. + """ + return var_operation_return( + js_expression=f"Array.from({{ length: Math.ceil(({stop!s} - {start!s}) / {step!s}) }}, (_, i) => {start!s} + i * {step!s})", + var_type=list[int], + ) + + +@var_operation +def array_contains_field_operation( + haystack: ArrayVar, needle: Any | Var, field: StringVar | str +): + """Check if an array contains an element. + + Args: + haystack: The array to check. + needle: The element to check for. + field: The field to check. + + Returns: + The array contains operation. + """ + return var_operation_return( + js_expression=f"{haystack}.some(obj => obj[{field}] === {needle})", + var_type=bool, + ) + + +@var_operation +def array_contains_operation( + haystack: ArrayVar, needle: Any | Var +) -> CustomVarOperationReturn[bool]: + """Check if an array contains an element. + + Args: + haystack: The array to check. + needle: The element to check for. + + Returns: + The array contains operation. + """ + return var_operation_return( + js_expression=f"{haystack}.includes({needle})", + var_type=bool, + ) + + +@var_operation +def repeat_array_operation( + array: ArrayVar[ARRAY_VAR_TYPE], count: NumberVar | int +) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: + """Repeat an array a number of times. + + Args: + array: The array to repeat. + count: The number of times to repeat the array. + + Returns: + The repeated array. + """ + return var_operation_return( + js_expression=f"Array.from({{ length: {count} }}).flatMap(() => {array})", + var_type=array._var_type, + ) + + +@var_operation +def map_array_operation( + array: ArrayVar[ARRAY_VAR_TYPE], + function: FunctionVar, +) -> CustomVarOperationReturn[list[Any]]: + """Map a function over an array. + + Args: + array: The array. + function: The function to map. + + Returns: + The mapped array. + """ + return var_operation_return( + js_expression=f"{array}.map({function})", var_type=list[Any] + ) + + +@var_operation +def array_concat_operation( + lhs: ArrayVar[ARRAY_VAR_TYPE], rhs: ArrayVar[ARRAY_VAR_TYPE] +) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: + """Concatenate two arrays. + + Args: + lhs: The left-hand side array. + rhs: The right-hand side array. + + Returns: + The concatenated array. + """ + return var_operation_return( + js_expression=f"[...{lhs}, ...{rhs}]", + var_type=lhs._var_type | rhs._var_type, + ) + + +class RangeVar(ArrayVar[Sequence[int]], python_types=range): + """Base class for immutable range vars.""" + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralRangeVar(CachedVarOperation, LiteralVar, RangeVar): + """Base class for immutable literal range vars.""" + + _var_value: range = dataclasses.field(default_factory=lambda: range(0)) + + @classmethod + def create( + cls, + value: range, + _var_type: type[range] | None = None, + _var_data: VarData | None = None, + ) -> RangeVar: + """Create a var from a string value. + + Args: + value: The value to create the var from. + _var_type: The type of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The var. + """ + return cls( + _js_expr="", + _var_type=_var_type or range, + _var_data=_var_data, + _var_value=value, + ) + + def __hash__(self) -> int: + """Get the hash of the var. + + Returns: + The hash of the var. + """ + return hash(( + self.__class__.__name__, + self._var_value.start, + self._var_value.stop, + self._var_value.step, + )) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """The name of the var. + + Returns: + The name of the var. + """ + return f"Array.from({{ length: Math.ceil(({self._var_value.stop!s} - {self._var_value.start!s}) / {self._var_value.step!s}) }}, (_, i) => {self._var_value.start!s} + i * {self._var_value.step!s})" + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> VarData | None: + """Get all the var data. + + Returns: + The var data. + """ + return self._var_data + + def json(self) -> str: + """Get the JSON representation of the var. + + Returns: + The JSON representation of the var. + """ + return json.dumps( + list(self._var_value), + ) diff --git a/packages/reflex-docgen/README.md b/packages/reflex-docgen/README.md index e69de29bb2d..40d2d955dcb 100644 --- a/packages/reflex-docgen/README.md +++ b/packages/reflex-docgen/README.md @@ -0,0 +1,3 @@ +# reflex-docgen + +Generate documentation for Reflex components. diff --git a/packages/reflex-docgen/pyproject.toml b/packages/reflex-docgen/pyproject.toml index 21d9d7c39c1..fcd2e920d7e 100644 --- a/packages/reflex-docgen/pyproject.toml +++ b/packages/reflex-docgen/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "reflex-docgen" -version = "0.0.1" +dynamic = ["version"] description = "Generate documentation for Reflex components." readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] @@ -8,6 +8,8 @@ maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ "griffelib>=2.0.1", + "mistletoe>=1.4.0", + "pyyaml>=6.0", "reflex", "typing-extensions", "typing-inspection>=0.4.2", @@ -16,6 +18,13 @@ dependencies = [ [tool.uv.sources] reflex = { workspace = true } +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-docgen-" +fallback-version = "0.0.0dev0" + [build-system] -requires = ["uv_build>=0.10.11,<0.11.0"] -build-backend = "uv_build" +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/packages/reflex-docgen/src/reflex_docgen/__init__.py b/packages/reflex-docgen/src/reflex_docgen/__init__.py index acf408b8aa9..bd22e88d501 100644 --- a/packages/reflex-docgen/src/reflex_docgen/__init__.py +++ b/packages/reflex-docgen/src/reflex_docgen/__init__.py @@ -1,5 +1,6 @@ """Module for generating documentation for Reflex components and classes.""" +from reflex_docgen import markdown as markdown from reflex_docgen._class import ClassDocumentation as ClassDocumentation from reflex_docgen._class import FieldDocumentation as FieldDocumentation from reflex_docgen._class import MethodDocumentation as MethodDocumentation @@ -28,4 +29,5 @@ "generate_documentation", "get_component_event_handlers", "get_component_props", + "markdown", ] diff --git a/packages/reflex-docgen/src/reflex_docgen/_class.py b/packages/reflex-docgen/src/reflex_docgen/_class.py index 3526f6e8abc..a9b7a7cca66 100644 --- a/packages/reflex-docgen/src/reflex_docgen/_class.py +++ b/packages/reflex-docgen/src/reflex_docgen/_class.py @@ -5,10 +5,9 @@ from dataclasses import dataclass from typing import Any, get_args, get_type_hints +from reflex_core.vars.base import BaseStateMeta from typing_inspection.introspection import AnnotationSource, inspect_annotation -from reflex.vars.base import BaseStateMeta - @dataclass(frozen=True, slots=True, kw_only=True) class FieldDocumentation: @@ -316,22 +315,31 @@ def generate_class_documentation(cls: type) -> ClassDocumentation: Returns: The generated documentation for the class. """ - description = inspect.cleandoc(cls.__doc__) if cls.__doc__ else None + try: + description = inspect.cleandoc(cls.__doc__) if cls.__doc__ else None - if dataclasses.is_dataclass(cls): - fields = _get_dataclass_fields(cls) - elif isinstance(cls, BaseStateMeta): - fields = _get_state_fields(cls) - else: - fields = () + if dataclasses.is_dataclass(cls): + fields = _get_dataclass_fields(cls) + elif isinstance(cls, BaseStateMeta): + fields = _get_state_fields(cls) + else: + fields = () - class_fields = _get_class_vars(cls) - methods = _get_methods(cls) + class_fields = _get_class_vars(cls) + methods = _get_methods(cls) - return ClassDocumentation( - name=f"{cls.__module__}.{cls.__qualname__}", - description=description, - fields=fields, - class_fields=class_fields, - methods=methods, - ) + return ClassDocumentation( + name=f"{cls.__module__}.{cls.__qualname__}", + description=description, + fields=fields, + class_fields=class_fields, + methods=methods, + ) + except Exception as e: + import sys + + if sys.version_info >= (3, 11): + e.add_note( + f"Error generating documentation for class {cls.__module__}.{cls.__qualname__}" + ) + raise diff --git a/packages/reflex-docgen/src/reflex_docgen/_component.py b/packages/reflex-docgen/src/reflex_docgen/_component.py index 315f7a0002d..1e1df8e9a2b 100644 --- a/packages/reflex-docgen/src/reflex_docgen/_component.py +++ b/packages/reflex-docgen/src/reflex_docgen/_component.py @@ -3,8 +3,8 @@ from dataclasses import dataclass from typing import Any -from reflex.components.component import DEFAULT_TRIGGERS_AND_DESC, Component -from reflex.event import EventHandler +from reflex_core.components.component import DEFAULT_TRIGGERS_AND_DESC, Component +from reflex_core.event import EventHandler @dataclass(frozen=True, slots=True, kw_only=True) diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py b/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py new file mode 100644 index 00000000000..3b8ca760cc9 --- /dev/null +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py @@ -0,0 +1,55 @@ +"""Markdown parsing and types for Reflex documentation.""" + +from reflex_docgen.markdown._parser import parse_document as parse_document +from reflex_docgen.markdown._types import Block as Block +from reflex_docgen.markdown._types import BoldSpan as BoldSpan +from reflex_docgen.markdown._types import CodeBlock as CodeBlock +from reflex_docgen.markdown._types import CodeSpan as CodeSpan +from reflex_docgen.markdown._types import ComponentPreview as ComponentPreview +from reflex_docgen.markdown._types import DirectiveBlock as DirectiveBlock +from reflex_docgen.markdown._types import Document as Document +from reflex_docgen.markdown._types import FrontMatter as FrontMatter +from reflex_docgen.markdown._types import HeadingBlock as HeadingBlock +from reflex_docgen.markdown._types import ImageSpan as ImageSpan +from reflex_docgen.markdown._types import ItalicSpan as ItalicSpan +from reflex_docgen.markdown._types import LineBreakSpan as LineBreakSpan +from reflex_docgen.markdown._types import LinkSpan as LinkSpan +from reflex_docgen.markdown._types import ListBlock as ListBlock +from reflex_docgen.markdown._types import ListItem as ListItem +from reflex_docgen.markdown._types import QuoteBlock as QuoteBlock +from reflex_docgen.markdown._types import Span as Span +from reflex_docgen.markdown._types import StrikethroughSpan as StrikethroughSpan +from reflex_docgen.markdown._types import TableBlock as TableBlock +from reflex_docgen.markdown._types import TableCell as TableCell +from reflex_docgen.markdown._types import TableRow as TableRow +from reflex_docgen.markdown._types import TextBlock as TextBlock +from reflex_docgen.markdown._types import TextSpan as TextSpan +from reflex_docgen.markdown._types import ThematicBreakBlock as ThematicBreakBlock + +__all__ = [ + "Block", + "BoldSpan", + "CodeBlock", + "CodeSpan", + "ComponentPreview", + "DirectiveBlock", + "Document", + "FrontMatter", + "HeadingBlock", + "ImageSpan", + "ItalicSpan", + "LineBreakSpan", + "LinkSpan", + "ListBlock", + "ListItem", + "QuoteBlock", + "Span", + "StrikethroughSpan", + "TableBlock", + "TableCell", + "TableRow", + "TextBlock", + "TextSpan", + "ThematicBreakBlock", + "parse_document", +] diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py b/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py new file mode 100644 index 00000000000..a5c8346b2f9 --- /dev/null +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py @@ -0,0 +1,378 @@ +"""Parse Reflex documentation markdown files using mistletoe.""" + +from __future__ import annotations + +import re +from collections.abc import Sequence +from typing import TYPE_CHECKING, cast + +from reflex_docgen.markdown._types import ( + Block, + BoldSpan, + CodeBlock, + CodeSpan, + ComponentPreview, + DirectiveBlock, + Document, + FrontMatter, + HeadingBlock, + ImageSpan, + ItalicSpan, + LineBreakSpan, + LinkSpan, + ListBlock, + ListItem, + QuoteBlock, + Span, + StrikethroughSpan, + TableBlock, + TableCell, + TableRow, + TextBlock, + TextSpan, + ThematicBreakBlock, +) + +if TYPE_CHECKING: + from mistletoe.block_token import BlockToken + +_FRONTMATTER_RE = re.compile(r"\A---\n(.*?\n)---\n", re.DOTALL) + + +#: Known frontmatter keys that are not component preview lambdas. +_KNOWN_KEYS = frozenset({"components", "only_low_level", "title"}) + + +def _extract_frontmatter(source: str) -> tuple[FrontMatter | None, str]: + """Extract YAML frontmatter from the beginning of a markdown string. + + Args: + source: The raw markdown source. + + Returns: + A tuple of (FrontMatter or None, remaining source). + """ + m = _FRONTMATTER_RE.match(source) + if m is None: + return None, source + + import yaml + + data = yaml.safe_load(m.group(1)) + if not isinstance(data, dict): + data = {} + + # components + raw_components = data.get("components", []) + if not isinstance(raw_components, list): + raw_components = [] + components = tuple(str(c) for c in raw_components) + + # only_low_level + raw_oll = data.get("only_low_level", []) + if isinstance(raw_oll, list): + only_low_level = bool(raw_oll and raw_oll[0]) + else: + only_low_level = bool(raw_oll) + + # title + raw_title = data.get("title") + title = str(raw_title) if raw_title is not None else None + + # component previews — any key not in _KNOWN_KEYS with a string value + previews: list[ComponentPreview] = [] + for key, value in data.items(): + if key not in _KNOWN_KEYS and isinstance(value, str): + previews.append(ComponentPreview(name=key, source=value.strip())) + + return ( + FrontMatter( + components=components, + only_low_level=only_low_level, + title=title, + component_previews=tuple(previews), + ), + source[m.end() :], + ) + + +def _convert_span(token: object) -> Span: + """Convert a mistletoe span token into a Span. + + Args: + token: A mistletoe span token. + + Returns: + The corresponding Span. + """ + from mistletoe.span_token import ( + Emphasis, + EscapeSequence, + Image, + InlineCode, + LineBreak, + Link, + RawText, + Strikethrough, + Strong, + ) + + if isinstance(token, RawText): + return TextSpan(text=token.content) + + if isinstance(token, InlineCode): + # InlineCode.children is a tuple of one RawText element. + children = token.children + if ( + not isinstance(children, tuple) + or len(children) != 1 + or not isinstance(children[0], RawText) + ): + msg = ( + f"Expected InlineCode to have a single RawText child, got {children!r}" + ) + raise TypeError(msg) + return CodeSpan(code=children[0].content) + + if isinstance(token, Strong): + return BoldSpan(children=_convert_children(token)) + + if isinstance(token, Emphasis): + return ItalicSpan(children=_convert_children(token)) + + if isinstance(token, Strikethrough): + return StrikethroughSpan(children=_convert_children(token)) + + if isinstance(token, Link): + return LinkSpan(children=_convert_children(token), target=token.target) + + if isinstance(token, Image): + return ImageSpan(children=_convert_children(token), src=token.src) + + if isinstance(token, LineBreak): + return LineBreakSpan(soft=token.soft) + + if isinstance(token, EscapeSequence): + # EscapeSequence.children is a tuple of one RawText with the escaped char. + children = token.children + if ( + not isinstance(children, tuple) + or len(children) != 1 + or not isinstance(children[0], RawText) + ): + msg = f"Expected EscapeSequence to have a single RawText child, got {children!r}" + raise TypeError(msg) + return TextSpan(text=children[0].content) + + msg = f"Unsupported span token type: {type(token).__name__}" + raise TypeError(msg) + + +def _convert_children(token: object) -> tuple[Span, ...]: + """Convert the children of a mistletoe token into Spans. + + Args: + token: A mistletoe token with a children attribute. + + Returns: + A tuple of Span objects. + """ + children = getattr(token, "children", None) + if children is None: + return () + return tuple(_convert_span(child) for child in children) + + +def _parse_info_string(info: str) -> tuple[str | None, tuple[str, ...]]: + """Parse a fenced code block info string into language and flags. + + Args: + info: The info string (e.g. "python demo exec"). + + Returns: + A tuple of (language, flags). + """ + parts = info.strip().split() + if not parts: + return None, () + return parts[0], tuple(parts[1:]) + + +def _convert_block(token: BlockToken) -> Block | None: + """Convert a mistletoe block token into a docgen Block. + + Args: + token: The mistletoe block token. + + Returns: + A Block, or None if the token should be skipped. + """ + from mistletoe.block_token import BlockCode as MistletoeBlockCode + from mistletoe.block_token import CodeFence as MistletoeCodeFence + from mistletoe.block_token import Heading as MistletoeHeading + from mistletoe.block_token import List as MistletoeList + from mistletoe.block_token import ListItem as MistletoeListItem + from mistletoe.block_token import Paragraph as MistletoeParagraph + from mistletoe.block_token import Quote as MistletoeQuote + from mistletoe.block_token import SetextHeading as MistletoeSetextHeading + from mistletoe.block_token import Table as MistletoeTable + from mistletoe.block_token import ThematicBreak as MistletoeThematicBreak + from mistletoe.span_token import RawText as MistletoeRawText + + if isinstance(token, (MistletoeHeading, MistletoeSetextHeading)): + return HeadingBlock(level=token.level, children=_convert_children(token)) + + if isinstance(token, (MistletoeBlockCode, MistletoeCodeFence)): + if isinstance(token, MistletoeCodeFence): + # CodeFence.info_string contains the full info string including language. + info = token.info_string or "" + else: + info = getattr(token, "language", "") or "" + language, flags = _parse_info_string(info) + + # CodeFence/BlockCode.children is a tuple of one RawText element. + children = token.children + if not children: + content = "" + elif ( + not isinstance(children, tuple) + or len(children) != 1 + or not isinstance(children[0], MistletoeRawText) + ): + msg = ( + f"Expected code block to have a single RawText child, got {children!r}" + ) + raise TypeError(msg) + else: + content = children[0].content + # Strip trailing newline that mistletoe adds. + content = content.rstrip("\n") + + # ```md [args...]``` blocks become DirectiveBlocks. + if language == "md" and flags: + return DirectiveBlock( + name=flags[0], + args=flags[1:], + content=content, + ) + + return CodeBlock(language=language, flags=flags, content=content) + + if isinstance(token, MistletoeParagraph): + spans = _convert_children(token) + if spans: + return TextBlock(children=spans) + return None + + if isinstance(token, MistletoeList): + items: list[ListItem] = [] + if token.children: + for item_token in token.children: + if not isinstance(item_token, MistletoeListItem): + msg = f"Expected ListItem, got {type(item_token).__name__}" + raise TypeError(msg) + item_blocks = _convert_block_children(item_token) + items.append(ListItem(children=item_blocks)) + # List.start is an instance attribute (int | None) but pyright sees + # the classmethod start(cls, line) instead. + list_start = cast("int | None", token.start) # pyright: ignore[reportAttributeAccessIssue] + return ListBlock( + ordered=list_start is not None, + start=list_start, + items=tuple(items), + ) + + if isinstance(token, MistletoeQuote): + return QuoteBlock(children=_convert_block_children(token)) + + if isinstance(token, MistletoeTable): + header = _convert_table_row(token.header, token.column_align) + rows = [ + _convert_table_row(row_token, token.column_align) + for row_token in (token.children or ()) + ] + return TableBlock(header=header, rows=tuple(rows)) + + if isinstance(token, MistletoeThematicBreak): + return ThematicBreakBlock() + + msg = f"Unsupported block token type: {type(token).__name__}" + raise TypeError(msg) + + +def _convert_block_children(token: BlockToken) -> tuple[Block, ...]: + """Convert the block-level children of a container token. + + Args: + token: A mistletoe container block token. + + Returns: + A tuple of Block objects. + """ + from mistletoe.block_token import BlockToken + + if not token.children: + return () + result: list[Block] = [] + for child in token.children: + if not isinstance(child, BlockToken): + msg = f"Expected BlockToken, got {type(child).__name__}" + raise TypeError(msg) + block = _convert_block(child) + if block is not None: + result.append(block) + return tuple(result) + + +def _convert_table_row( + row_token: object, column_align: Sequence[int | None] +) -> TableRow: + """Convert a mistletoe TableRow into a TableRow. + + Args: + row_token: A mistletoe TableRow token. + column_align: The column alignment list from the Table. + + Returns: + A TableRow. + """ + align_map = {None: None, 0: None, 1: "left", 2: "right", 3: "center"} + children = getattr(row_token, "children", None) or () + cells = [ + TableCell( + children=_convert_children(cell_token), + align=align_map.get(column_align[i] if i < len(column_align) else None), + ) + for i, cell_token in enumerate(children) + ] + return TableRow(cells=tuple(cells)) + + +def parse_document(source: str) -> Document: + """Parse a Reflex documentation markdown file into a Document. + + Args: + source: The raw markdown source string. + + Returns: + A parsed Document with frontmatter and content blocks. + """ + from mistletoe.block_token import BlockToken + from mistletoe.block_token import Document as MistletoeDocument + + frontmatter, remaining = _extract_frontmatter(source) + doc = MistletoeDocument(remaining) + + blocks: list[Block] = [] + if doc.children: + for child in doc.children: + # mistletoe.block_token.Document guarantees children are BlockToken + # instances (see its docstring: "Its children are block tokens"). + if not isinstance(child, BlockToken): + msg = f"Expected BlockToken, got {type(child).__name__}" + raise TypeError(msg) + block = _convert_block(child) + if block is not None: + blocks.append(block) + + return Document(frontmatter=frontmatter, blocks=tuple(blocks)) diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py b/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py new file mode 100644 index 00000000000..152689718a8 --- /dev/null +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py @@ -0,0 +1,558 @@ +"""Markdown document types — spans, blocks, and document.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass + +# --------------------------------------------------------------------------- +# Span types — inline content without exposing mistletoe +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TextSpan: + """Plain text. + + Attributes: + text: The text content. + """ + + text: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return self.text + + +@dataclass(frozen=True, slots=True, kw_only=True) +class BoldSpan: + """Bold (strong) text. + + Attributes: + children: The inline spans inside the bold. + """ + + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"**{inner}**" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ItalicSpan: + """Italic (emphasis) text. + + Attributes: + children: The inline spans inside the italic. + """ + + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"*{inner}*" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class StrikethroughSpan: + """Strikethrough text. + + Attributes: + children: The inline spans inside the strikethrough. + """ + + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"~~{inner}~~" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class CodeSpan: + """Inline code. + + Attributes: + code: The code text. + """ + + code: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return f"`{self.code}`" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class LinkSpan: + """A hyperlink. + + Attributes: + children: The inline spans forming the link text. + target: The URL target. + """ + + children: tuple[Span, ...] + target: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"[{inner}]({self.target})" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ImageSpan: + """An inline image. + + Attributes: + children: The inline spans forming the alt text. + src: The image source URL. + """ + + children: tuple[Span, ...] + src: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"![{inner}]({self.src})" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class LineBreakSpan: + """A line break. + + Attributes: + soft: Whether this is a soft line break. + """ + + soft: bool + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return "\n" if self.soft else " \n" + + +#: Union of all inline span types. +Span = ( + TextSpan + | BoldSpan + | ItalicSpan + | StrikethroughSpan + | CodeSpan + | LinkSpan + | ImageSpan + | LineBreakSpan +) + + +def _spans_as_markdown(spans: tuple[Span, ...]) -> str: + """Render a sequence of spans back to markdown. + + Args: + spans: The inline spans to render. + + Returns: + A markdown string. + """ + return "".join(s.as_markdown() for s in spans) + + +_BACKTICK_FENCE_RE = re.compile(r"^(`{3,})", re.MULTILINE) + + +def _fence_for(content: str) -> str: + """Return a backtick fence long enough to wrap *content* safely. + + Args: + content: The code block content that may contain backtick fences. + + Returns: + A backtick fence string (at least 3 backticks). + """ + max_run = 3 + for m in _BACKTICK_FENCE_RE.finditer(content): + max_run = max(max_run, len(m.group(1)) + 1) + return "`" * max_run + + +# --------------------------------------------------------------------------- +# Block types +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ComponentPreview: + """A component preview lambda from frontmatter. + + Attributes: + name: The component class name (e.g. "Button", "DialogRoot"). + source: The lambda source string. + """ + + name: str + source: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class FrontMatter: + """YAML frontmatter extracted from a markdown document. + + Attributes: + components: Component paths to document (e.g. ``["rx.button"]``). + only_low_level: Whether to show only low-level component variants. + title: An optional page title. + component_previews: Preview lambdas keyed by component class name. + """ + + components: tuple[str, ...] + only_low_level: bool + title: str | None + component_previews: tuple[ComponentPreview, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + import yaml + + data: dict[str, object] = {} + if self.components: + data["components"] = list(self.components) + if self.only_low_level: + data["only_low_level"] = [True] + if self.title is not None: + data["title"] = self.title + for preview in self.component_previews: + data[preview.name] = preview.source + return f"---\n{yaml.dump(data, default_flow_style=False, sort_keys=False).rstrip()}\n---" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class CodeBlock: + """A fenced code block with optional language and flags. + + Attributes: + language: The language identifier (e.g. "python"). + flags: Additional flags after the language (e.g. ("demo", "exec")). + content: The code content inside the block. + """ + + language: str | None + flags: tuple[str, ...] + content: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + info = self.language or "" + if self.flags: + info = f"{info} {' '.join(self.flags)}" if info else " ".join(self.flags) + fence = _fence_for(self.content) + return f"{fence}{info}\n{self.content}\n{fence}" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DirectiveBlock: + """A markdown directive block (```md [args...] ```). + + Covers alert, video, definition, section, and any future md directives. + + Attributes: + name: The directive name (e.g. "alert", "video", "definition", "section"). + args: Additional arguments after the name (e.g. ("info",) or ("https://...",)). + content: The raw content inside the block. + """ + + name: str + args: tuple[str, ...] + content: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + info_parts = ["md", self.name, *self.args] + fence = _fence_for(self.content) + return f"{fence}{' '.join(info_parts)}\n{self.content}\n{fence}" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class HeadingBlock: + """A markdown heading. + + Attributes: + level: The heading level (1-6). + children: The inline spans forming the heading text. + """ + + level: int + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return f"{'#' * self.level} {_spans_as_markdown(self.children)}" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TextBlock: + """A block of markdown text (paragraph or other inline content). + + Attributes: + children: The inline spans forming the text content. + """ + + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return _spans_as_markdown(self.children) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ListItem: + """A single item in a list. + + Attributes: + children: The block-level content of the list item. + """ + + children: tuple[Block, ...] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ListBlock: + """An ordered or unordered list. + + Attributes: + ordered: Whether the list is ordered. + start: The starting number for ordered lists, or None for unordered. + items: The list items. + """ + + ordered: bool + start: int | None + items: tuple[ListItem, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + lines: list[str] = [] + for i, item in enumerate(self.items): + prefix = f"{(self.start or 1) + i}. " if self.ordered else "- " + item_md = "\n\n".join(child.as_markdown() for child in item.children) + first, *rest = item_md.split("\n") + lines.append(f"{prefix}{first}") + indent = " " * len(prefix) + lines.extend(f"{indent}{line}" if line else "" for line in rest) + return "\n".join(lines) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class QuoteBlock: + """A block quote. + + Attributes: + children: The block-level content inside the quote. + """ + + children: tuple[Block, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "\n\n".join(child.as_markdown() for child in self.children) + return "\n".join(f"> {line}" if line else ">" for line in inner.split("\n")) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TableCell: + """A single cell in a table. + + Attributes: + children: The inline spans forming the cell content. + align: The column alignment ("left", "right", "center", or None). + """ + + children: tuple[Span, ...] + align: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TableRow: + """A row in a table. + + Attributes: + cells: The cells in the row. + """ + + cells: tuple[TableCell, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + cells = " | ".join(_spans_as_markdown(cell.children) for cell in self.cells) + return f"| {cells} |" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TableBlock: + """A table. + + Attributes: + header: The header row. + rows: The body rows. + """ + + header: TableRow + rows: tuple[TableRow, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + lines = [self.header.as_markdown()] + sep_parts: list[str] = [] + for cell in self.header.cells: + if cell.align == "left": + sep_parts.append(":---") + elif cell.align == "right": + sep_parts.append("---:") + elif cell.align == "center": + sep_parts.append(":---:") + else: + sep_parts.append("---") + lines.append(f"| {' | '.join(sep_parts)} |") + lines.extend(row.as_markdown() for row in self.rows) + return "\n".join(lines) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ThematicBreakBlock: + """A thematic break (horizontal rule).""" + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return "---" + + +#: Union of all block types that can appear in a parsed document. +Block = ( + FrontMatter + | CodeBlock + | DirectiveBlock + | HeadingBlock + | TextBlock + | ListBlock + | QuoteBlock + | TableBlock + | ThematicBreakBlock +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Document: + """A parsed Reflex documentation markdown file. + + Attributes: + frontmatter: The YAML frontmatter, if present. + blocks: The sequence of content blocks in document order. + """ + + frontmatter: FrontMatter | None + blocks: tuple[Block, ...] + + @property + def headings(self) -> tuple[HeadingBlock, ...]: + """Return all headings in the document.""" + return tuple(b for b in self.blocks if isinstance(b, HeadingBlock)) + + @property + def code_blocks(self) -> tuple[CodeBlock, ...]: + """Return all code blocks in the document.""" + return tuple(b for b in self.blocks if isinstance(b, CodeBlock)) + + @property + def directives(self) -> tuple[DirectiveBlock, ...]: + """Return all directive blocks in the document.""" + return tuple(b for b in self.blocks if isinstance(b, DirectiveBlock)) + + def as_markdown(self) -> str: + """Render the full document back to markdown. + + Returns: + A markdown string. + """ + parts: list[str] = [] + if self.frontmatter: + parts.append(self.frontmatter.as_markdown()) + parts.extend(block.as_markdown() for block in self.blocks) + return "\n\n".join(parts) + "\n" diff --git a/pyi_hashes.json b/pyi_hashes.json index d068395f5a0..62611f50a6d 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -1,123 +1,124 @@ { - "reflex/__init__.pyi": "9823934f0e3fca36228004a6fbb1d8df", - "reflex/components/__init__.pyi": "ac05995852baa81062ba3d18fbc489fb", - "reflex/components/base/__init__.pyi": "16e47bf19e0d62835a605baa3d039c5a", - "reflex/components/base/app_wrap.pyi": "22e94feaa9fe675bcae51c412f5b67f1", - "reflex/components/base/body.pyi": "fa8e4343880c7b3ac09b056bd3c253ee", - "reflex/components/base/document.pyi": "1c90cb8f1981a0f7f6087c702feb23a3", - "reflex/components/base/error_boundary.pyi": "53deea0de4b36a6e0af7067e3c05e695", - "reflex/components/base/fragment.pyi": "d3da5a5bff969ea682a86eaa6658b7ec", - "reflex/components/base/link.pyi": "7ffdf9c70724da5781284641f3ce8370", - "reflex/components/base/meta.pyi": "ceb1b79d42e7b3e115e1c3ca6f7121c2", - "reflex/components/base/script.pyi": "c136448d69727928443f8ced053c7210", - "reflex/components/base/strict_mode.pyi": "e7e7358393ff81e9283e2e67f7c2aaa8", - "reflex/components/core/__init__.pyi": "007170b97e58bdf28b2aee381d91c0c7", - "reflex/components/core/auto_scroll.pyi": "a4db8095e145926992a8272379d27257", - "reflex/components/core/banner.pyi": "66f4fd0cd78a9d6071caa31c51394a49", - "reflex/components/core/clipboard.pyi": "c8a7834ea5f6202c760065b2534ea59d", - "reflex/components/core/debounce.pyi": "1722c092d6e17406a9bf047353d05ea6", - "reflex/components/core/helmet.pyi": "cb5ac1be02c6f82fcc78ba74651be593", - "reflex/components/core/html.pyi": "4ebe946f3fc097fc2e31dddf7040ec1c", - "reflex/components/core/sticky.pyi": "cb763b986a9b0654d1a3f33440dfcf60", - "reflex/components/core/upload.pyi": "ca9f7424f3b74b1b56f5c819e8654eeb", - "reflex/components/core/window_events.pyi": "e7af4bf5341c4afaf60c4a534660f68f", - "reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e", - "reflex/components/datadisplay/code.pyi": "1d123d19ef08f085422f3023540e7bb1", - "reflex/components/datadisplay/dataeditor.pyi": "93309b17a4c12b2216e2d863d325a107", - "reflex/components/datadisplay/shiki_code_block.pyi": "570c1a03ad509da982b90de42c69fd47", - "reflex/components/el/__init__.pyi": "0adfd001a926a2a40aee94f6fa725ecc", - "reflex/components/el/element.pyi": "62431eed73f5a2b0536036ce05fb84bd", - "reflex/components/el/elements/__init__.pyi": "29512d7a6b29c6dc5ff68d3b31f26528", - "reflex/components/el/elements/base.pyi": "705e5555b86b2fff64cc77baf6ed02f1", - "reflex/components/el/elements/forms.pyi": "5a1fde0f8fee4d1bec938577781d6e53", - "reflex/components/el/elements/inline.pyi": "c7340e5c60344c61aa7a1c30b3e1b92f", - "reflex/components/el/elements/media.pyi": "fa92cf81b560466b310ad9f5795e9fcf", - "reflex/components/el/elements/metadata.pyi": "67ac22ad50139f545bc0e2e26fe31c04", - "reflex/components/el/elements/other.pyi": "b4bc739f1338acd430263b5bc284b6ab", - "reflex/components/el/elements/scripts.pyi": "00731f16568572131dcc3e02d2a7aa66", - "reflex/components/el/elements/sectioning.pyi": "c8bc889c30bcb54a270fb07ff1d2c7cc", - "reflex/components/el/elements/tables.pyi": "7c76a00f319dbbc27ea1a4d81695e4be", - "reflex/components/el/elements/typography.pyi": "b6a5b6a0a23a03cd81a96ddb4769076a", - "reflex/components/gridjs/datatable.pyi": "761a2472cef297005f8d7ce63a06ee27", - "reflex/components/lucide/icon.pyi": "1f0449a8dc8ea7016334f4d51a42ce1a", - "reflex/components/markdown/markdown.pyi": "9e7316a36a36409d718700609652e570", - "reflex/components/moment/moment.pyi": "b63ea2a7e91f4caf8db86e438caead5a", - "reflex/components/plotly/plotly.pyi": "af31e1963b6788f3dec3238310269b7c", - "reflex/components/radix/__init__.pyi": "5d8e3579912473e563676bfc71f29191", - "reflex/components/radix/primitives/__init__.pyi": "01c388fe7a1f5426a16676404344edf6", - "reflex/components/radix/primitives/accordion.pyi": "a0b2f56c5a726057596b77b336879bd9", - "reflex/components/radix/primitives/base.pyi": "e64ef8c92e94e02baa2193c6b3da8300", - "reflex/components/radix/primitives/dialog.pyi": "2995a7323d8a016db9945955db2d2070", - "reflex/components/radix/primitives/drawer.pyi": "0041ba408b1171d6a733335d5dfb2f5d", - "reflex/components/radix/primitives/form.pyi": "7eb90bcdb45a9de263eea722733d7704", - "reflex/components/radix/primitives/progress.pyi": "959e0540affc4967c457da7f6642b7ae", - "reflex/components/radix/primitives/slider.pyi": "3892b9e10cd424a4ea40f166737eeaf8", - "reflex/components/radix/themes/__init__.pyi": "582b4a7ead62b2ae8605e17fa084c063", - "reflex/components/radix/themes/base.pyi": "fa9d8fa28255b259b91cebe363881b6c", - "reflex/components/radix/themes/color_mode.pyi": "dfbe926e30f4f1b013086c59def6a298", - "reflex/components/radix/themes/components/__init__.pyi": "efa279ee05479d7bb8a64d49da808d03", - "reflex/components/radix/themes/components/alert_dialog.pyi": "fd831007e357a398de254a7a67569394", - "reflex/components/radix/themes/components/aspect_ratio.pyi": "7087ecde3b3484a5e3cc77388228fc67", - "reflex/components/radix/themes/components/avatar.pyi": "093195106ff1a94b086fc98845d827e5", - "reflex/components/radix/themes/components/badge.pyi": "1ff3402150492bdbc1667a06f78de274", - "reflex/components/radix/themes/components/button.pyi": "1b919088eedbf5c5bae929c52b05fbd4", - "reflex/components/radix/themes/components/callout.pyi": "1b96bd57f15b28d6f371548452befde4", - "reflex/components/radix/themes/components/card.pyi": "c19d311c41f547b25afb6a224c3f885b", - "reflex/components/radix/themes/components/checkbox.pyi": "f7835e065a1c2961290320b5bb766d22", - "reflex/components/radix/themes/components/checkbox_cards.pyi": "339b3ed08900e4cb5e9786de6aa17fb8", - "reflex/components/radix/themes/components/checkbox_group.pyi": "1bdd96f70bcf9289db2a1f210de14b8b", - "reflex/components/radix/themes/components/context_menu.pyi": "84008b434ef1d349db155966e58c5f5a", - "reflex/components/radix/themes/components/data_list.pyi": "37df138ba88fe43f61648646f9d94616", - "reflex/components/radix/themes/components/dialog.pyi": "0d414930a1768b80cdbbf4c0b51c296a", - "reflex/components/radix/themes/components/dropdown_menu.pyi": "3aee89946b2ac137e536b9eb88c335bc", - "reflex/components/radix/themes/components/hover_card.pyi": "3ddc1bdf79f98d291c86553e275207aa", - "reflex/components/radix/themes/components/icon_button.pyi": "09f16042ed75fb605ba89df069f439de", - "reflex/components/radix/themes/components/inset.pyi": "2088b78fafc9256809b245d2544bb35a", - "reflex/components/radix/themes/components/popover.pyi": "80b410f0fcdfdd5815b06d17e8feff94", - "reflex/components/radix/themes/components/progress.pyi": "e4043f18f1eb33c0f675b827318d45ac", - "reflex/components/radix/themes/components/radio.pyi": "afc737e2f4c34eb9520361b1677c889f", - "reflex/components/radix/themes/components/radio_cards.pyi": "18775b80e37bfb7da81457f8068a83c3", - "reflex/components/radix/themes/components/radio_group.pyi": "b2b1491d0461acdd1980bf1b0ff9d0bc", - "reflex/components/radix/themes/components/scroll_area.pyi": "9aec5400dc3795f2430010bc288e9c03", - "reflex/components/radix/themes/components/segmented_control.pyi": "ed256e9e239c3378372dc5e25efe37e4", - "reflex/components/radix/themes/components/select.pyi": "58ca47be618155d427dbadefd5d72471", - "reflex/components/radix/themes/components/separator.pyi": "c84161d5924b4038289a3419f516582a", - "reflex/components/radix/themes/components/skeleton.pyi": "bde931806e05805957f06a618c675fc9", - "reflex/components/radix/themes/components/slider.pyi": "aa45010f4674390f055da20edd2008bb", - "reflex/components/radix/themes/components/spinner.pyi": "6080e9ba414077f78e52fd01c9061da6", - "reflex/components/radix/themes/components/switch.pyi": "fcb3767bda070ef0af4ee664f2f068a4", - "reflex/components/radix/themes/components/table.pyi": "1c5aca792b0acb1a79d8365f64e76e59", - "reflex/components/radix/themes/components/tabs.pyi": "94029d5505bc14a7059633472a4908e0", - "reflex/components/radix/themes/components/text_area.pyi": "ccc7f3f38218ee3189baf1350ec64b40", - "reflex/components/radix/themes/components/text_field.pyi": "87526c9942ec517140bd4fbb86f89600", - "reflex/components/radix/themes/components/tooltip.pyi": "6ada2d6bc3abc9aa5e55d66f81ae95de", - "reflex/components/radix/themes/layout/__init__.pyi": "73eefc509a49215b1797b5b5d28d035e", - "reflex/components/radix/themes/layout/base.pyi": "091d8353514b73ef0dc4a5a9347b0fdf", - "reflex/components/radix/themes/layout/box.pyi": "1467e1ef4b6722b728596f42806d693b", - "reflex/components/radix/themes/layout/center.pyi": "a0c20b842b2fb7407b7a3d079aaa1dae", - "reflex/components/radix/themes/layout/container.pyi": "6a8a5581f2bad13bbac35d6e0999074f", - "reflex/components/radix/themes/layout/flex.pyi": "446e10f5fb7c55855eb240d6e3f73dfa", - "reflex/components/radix/themes/layout/grid.pyi": "f2979297d7e4263b578dab47b71aa5c2", - "reflex/components/radix/themes/layout/list.pyi": "d14e480c72efe8c64d37408b789b9472", - "reflex/components/radix/themes/layout/section.pyi": "23ae5def80148fed4b5e729c9d931326", - "reflex/components/radix/themes/layout/spacer.pyi": "ce2020374844384dac1b7c18e00fd0a5", - "reflex/components/radix/themes/layout/stack.pyi": "6271d05ef431208d8db0418f04c6987c", - "reflex/components/radix/themes/typography/__init__.pyi": "b8ef970530397e9984004961f3aaee62", - "reflex/components/radix/themes/typography/blockquote.pyi": "dbdf3fdf7cdca84447a8007dd43b9d78", - "reflex/components/radix/themes/typography/code.pyi": "c5b95158b8328a199120e4795bf27f44", - "reflex/components/radix/themes/typography/heading.pyi": "a5cf5efcf485fd8a6f1b38a64e27a52d", - "reflex/components/radix/themes/typography/link.pyi": "64357025c9d5bae85ea206dcd3ebfa2d", - "reflex/components/radix/themes/typography/text.pyi": "0e1434318bb482157b8d1b276550019b", - "reflex/components/react_player/audio.pyi": "0e1690ff1f1f39bc748278d292238350", - "reflex/components/react_player/react_player.pyi": "5289c57db568ee3ae01f97789c445f37", - "reflex/components/react_player/video.pyi": "998671c06103d797c554d9278eb3b2a0", - "reflex/components/react_router/dom.pyi": "2198359c2d8f3d1856f4391978a4e2de", - "reflex/components/recharts/__init__.pyi": "6ee7f1ca2c0912f389ba6f3251a74d99", - "reflex/components/recharts/cartesian.pyi": "4dc01da3195f80b9408d84373b874c41", - "reflex/components/recharts/charts.pyi": "16cd435d77f06f0315b595b7e62cf44b", - "reflex/components/recharts/general.pyi": "9abf71810a5405fd45b13804c0a7fd1a", - "reflex/components/recharts/polar.pyi": "ea4743e8903365ba95bc4b653c47cc4a", - "reflex/components/recharts/recharts.pyi": "b3d93d085d51053bbb8f65326f34a299", - "reflex/components/sonner/toast.pyi": "636050fcc919f8ab0903c30dceaa18f1", - "reflex/experimental/memo.pyi": "78b1968972194785f72eab32476bc61d" + "packages/reflex-components-code/src/reflex_components_code/code.pyi": "a252d3efb9c621216c3ac32327158a83", + "packages/reflex-components-code/src/reflex_components_code/shiki_code_block.pyi": "2ae0bc697886c5a735afbe232a84f022", + "packages/reflex-components-core/src/reflex_components_core/__init__.pyi": "e4f253225cf70b62900e25d0a5c16436", + "packages/reflex-components-core/src/reflex_components_core/base/__init__.pyi": "407342f78a72e87489c8b22e40de68b9", + "packages/reflex-components-core/src/reflex_components_core/base/app_wrap.pyi": "8a0b6dcdf622b96be65311b7803c8ce9", + "packages/reflex-components-core/src/reflex_components_core/base/body.pyi": "aa734326f57b0fee9caed75bd318762e", + "packages/reflex-components-core/src/reflex_components_core/base/document.pyi": "8edb8967aa628329c4d1b7cfa3705f3a", + "packages/reflex-components-core/src/reflex_components_core/base/error_boundary.pyi": "c02999fa5d121904a242a83d2221f069", + "packages/reflex-components-core/src/reflex_components_core/base/fragment.pyi": "ba9a750fa1036dd4f454e7f3235aa4aa", + "packages/reflex-components-core/src/reflex_components_core/base/link.pyi": "fbae966c13c0da651a1e35f7045799c1", + "packages/reflex-components-core/src/reflex_components_core/base/meta.pyi": "9c1df9038ff6394cac77dbac6b3175c5", + "packages/reflex-components-core/src/reflex_components_core/base/script.pyi": "434ca63fb809077642112d53879380f5", + "packages/reflex-components-core/src/reflex_components_core/base/strict_mode.pyi": "4002f8ac81d1b38177c3b837cbc3b44d", + "packages/reflex-components-core/src/reflex_components_core/core/__init__.pyi": "3c22950d97f6017b8e6cc6a6c83cb4b3", + "packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.pyi": "3f8b625f5b38a9351b01201c7adb2ca0", + "packages/reflex-components-core/src/reflex_components_core/core/banner.pyi": "b2e4b26b13f33d8900550fedd2d5f447", + "packages/reflex-components-core/src/reflex_components_core/core/clipboard.pyi": "4164c841934cab71b1c4b132d15663f5", + "packages/reflex-components-core/src/reflex_components_core/core/debounce.pyi": "d01e9934bcfd81b5fc969d82e362ac20", + "packages/reflex-components-core/src/reflex_components_core/core/helmet.pyi": "3bf7bee5665293f7583009f651ea3cb1", + "packages/reflex-components-core/src/reflex_components_core/core/html.pyi": "7209d1607545e412ed38dbe2a129321c", + "packages/reflex-components-core/src/reflex_components_core/core/sticky.pyi": "8241c75ca16a0960b7dea6d6e7aff52e", + "packages/reflex-components-core/src/reflex_components_core/core/upload.pyi": "73e38c074d7e6ca2fda8eaad820f177e", + "packages/reflex-components-core/src/reflex_components_core/core/window_events.pyi": "4407cecb1825dc359bcc7b2bea011a8e", + "packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.pyi": "aaab42816119ac0f308841dc5482b3f1", + "packages/reflex-components-core/src/reflex_components_core/el/__init__.pyi": "e27fddec8de079db37d6699e136411d1", + "packages/reflex-components-core/src/reflex_components_core/el/element.pyi": "6add8b77380ea3702031b07330fc7d60", + "packages/reflex-components-core/src/reflex_components_core/el/elements/__init__.pyi": "721b328e94510f8328728be1657abbb8", + "packages/reflex-components-core/src/reflex_components_core/el/elements/base.pyi": "7c427fcd82fc6ccf86b4d2b5c4756426", + "packages/reflex-components-core/src/reflex_components_core/el/elements/forms.pyi": "5912179a169da4dc3b152042558be2cf", + "packages/reflex-components-core/src/reflex_components_core/el/elements/inline.pyi": "6bf366f345e14a556dbb3c0f230e1355", + "packages/reflex-components-core/src/reflex_components_core/el/elements/media.pyi": "f8d2a995e488ebc5e8633977151758ce", + "packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.pyi": "20d803fcc05d4c378547ceaa0e1bcc70", + "packages/reflex-components-core/src/reflex_components_core/el/elements/other.pyi": "1cf906cbc2751f87adbcd85e03b72d2e", + "packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.pyi": "b4b5bb69e6ce8d08c0df51301e132af4", + "packages/reflex-components-core/src/reflex_components_core/el/elements/sectioning.pyi": "4ce119b25459a01d128bdb5b79b0d128", + "packages/reflex-components-core/src/reflex_components_core/el/elements/tables.pyi": "9e58353a97dc006d37d2c7c50506fac4", + "packages/reflex-components-core/src/reflex_components_core/el/elements/typography.pyi": "3325f8a4af0aadb70cbfc50558e2f3b2", + "packages/reflex-components-core/src/reflex_components_core/react_router/dom.pyi": "d77a80f688b29d2e1048007172d2b65f", + "packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.pyi": "caa83be6f97faa95588bfa9ae9e9331e", + "packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "32880736442800061a39ce4b55267eaf", + "packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "e55c023c9ecc907321f163955f4c4875", + "packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "bacf19a5b6904281d7238dbd51e6fc1c", + "packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "b997bdd994844f0e6ca923bbb2dc34a1", + "packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "65a93d778a0fde06975dac9244f51bb3", + "packages/reflex-components-radix/src/reflex_components_radix/__init__.pyi": "4843dd071acb073dc30028322c3d4023", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.pyi": "bc25cae0eca01c8684443d5dfd7b6455", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.pyi": "6dd30847af62ad7d50d5c5daf6c4a1d7", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/base.pyi": "6c12ef3d9f82926bf17d410b774d56f5", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/dialog.pyi": "810fa8c626b79035cdbb04f43b5bc5ad", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/drawer.pyi": "cb67e835f9be41f70ee2bae0f8c0a764", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/form.pyi": "ba31009535c078df0bc5a26bce6dfd2b", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/progress.pyi": "2356caa9e23f9c8888cccbbb41b57985", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.pyi": "f694033992ef188f2da04e865d5a7d77", + "packages/reflex-components-radix/src/reflex_components_radix/themes/__init__.pyi": "5d9a06872953d3e3df99e1ff154a4e0c", + "packages/reflex-components-radix/src/reflex_components_radix/themes/base.pyi": "d7c20bd180f28fdb4affcba37e2aa1ff", + "packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.pyi": "ba0ff3b00289cd1896e327fa2be99563", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/__init__.pyi": "2b0f9f472ba6dcc743c2df17642c4a4b", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/alert_dialog.pyi": "fad40b463a8ebb0d3ca3900dc8a91679", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/aspect_ratio.pyi": "0d22969c5407592a0bb36768e149f2b5", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/avatar.pyi": "6a6e9b8f6ca3428c45d62bd0e7f94693", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/badge.pyi": "d96b2048ae17a558d9eb3378ae98524e", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/button.pyi": "1c7f518e1881e98614eadff952da0844", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/callout.pyi": "be245c1c3796f695ac4b2d77c3b88a3a", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/card.pyi": "b56fa19913ed15d9e630951e70479b36", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.pyi": "f33d86a3bb176e3144570198ce5f93ae", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_cards.pyi": "10af49cf574b738d616803df2c055ad0", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_group.pyi": "3edbddceb585fd80d9e7959977ff276e", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/context_menu.pyi": "0a06f6fa5cf8a2590c302f618451ca65", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/data_list.pyi": "82508b83193afde0b3bc06911cb78f87", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/dialog.pyi": "8f21ba52183221d4cf0b8beaacd8e006", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/dropdown_menu.pyi": "b167c32571142878305d98c0bd656b09", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/hover_card.pyi": "8009f36c543c1407e2aa7ead41178ceb", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/icon_button.pyi": "3814bb2950e2bcc454d186d50d123e9f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/inset.pyi": "31af9b53ec38736ab7457ea731642869", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/popover.pyi": "2ca6dfe4f9e00f2647f0ad4fd131e6d3", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/progress.pyi": "7a874fa512ce2d8a490aa41531f5814b", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio.pyi": "465a6d6e9525ac909b4f193d2d788682", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_cards.pyi": "ddb2835ecbeaf90681e4030a14d74604", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_group.pyi": "d5333b59e6ba9ad30923d2b60d0e382e", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/scroll_area.pyi": "6b2d881a8ecdf4dd169b341418a703db", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.pyi": "79764047f53543d673d6e1b2c929d9b8", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/select.pyi": "f71a320b02ac8f1d6db07b9198b296ec", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/separator.pyi": "f8420b5196edb74275d2119d780d0031", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/skeleton.pyi": "574407d03b311ca9cdf0f98ab53a6fbe", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/slider.pyi": "9e26688af77fab944635e16e0bf7283f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/spinner.pyi": "3477cc5e00146eaa2cde5d35f9459ad6", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/switch.pyi": "37e0c8dc43c5a24bdba03429e3ca9052", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/table.pyi": "c910ebd02d7a78627f884e3431426552", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/tabs.pyi": "ead639a106a76cc0e3fd2c8f093f9f23", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.pyi": "1a03d9525a1544816392067499c3354d", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_field.pyi": "c2fbd8547de4993e03017844e8c4b477", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/tooltip.pyi": "4545b70fd0802f19993419ab0163d595", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/__init__.pyi": "417e490adc15e93dc2cdb854ee0361d2", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/base.pyi": "3195e198d92ff43644a09c277303b83b", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/box.pyi": "69d12b6c918a476ac4557f42fef73c27", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/center.pyi": "88880d197ff7347ec7d3f81d6e57de8e", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/container.pyi": "f329387a5d4a988bc195e6a487ff44db", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/flex.pyi": "855c9d0c3c2e79e7d3811cfec74d6379", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/grid.pyi": "bd2c31d4e3d61743b72327f071969e05", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/list.pyi": "eee53b418ff0e0660c8cf9d8a0a59386", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/section.pyi": "1aa57142797597d65d840eb1d3cc7de7", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/spacer.pyi": "c774f0a1384f983e6d73bde603c341ca", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/stack.pyi": "894dcd5945123c1c8aa34cb77602fead", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/__init__.pyi": "3926877c04f74fc2acf4a398bee9da06", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/blockquote.pyi": "305a8932078e4af48e44489e7ce74060", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/code.pyi": "fad43053747fb84229cc35296c7028b5", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/heading.pyi": "5901c7202a5b135f60cc1407878b4859", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/link.pyi": "d567c1242672d125015920f7ae6e6999", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/text.pyi": "76f100da40d0e18ad4f7b3387dec1d4a", + "packages/reflex-components-react-player/src/reflex_components_react_player/audio.pyi": "a981a6031015c3a384e6255be88885f1", + "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "1f66ea4fa34e8a8fa7473d312daf84b8", + "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "8c1ea5bf4ec27ec6ff2dce462021b094", + "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "2610c28416f80e2254bd10dde8c29bdf", + "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "597b9eb86c57f5293c13c128fb972c27", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "303d4b1dc72c08339154907b9b095365", + "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "6e9371bddea95f8e2491d9b3c7e250cd", + "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "1ce679c002336c7bdbdd6c8ff6f2413c", + "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "1b92135de4ea79cb7d94eaaec55b9ab7", + "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "f09c503c4ab880c13c13d6fa67d708b8", + "reflex/__init__.pyi": "7696c38fd9c04a598518b49c5185c414", + "reflex/components/__init__.pyi": "55bb242d5e5428db329b88b4923c2ba5", + "reflex/experimental/memo.pyi": "d16eccf33993c781e2f8bc2dd8bbd4d4" } diff --git a/pyproject.toml b/pyproject.toml index ea7de088842..3bc21a784a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "reflex" -version = "0.9.0dev1" +dynamic = ["version"] description = "Web apps in pure Python." license.text = "Apache-2.0" authors = [ @@ -25,9 +25,8 @@ dependencies = [ "granian[reload] >=2.5.5", "httpx >=0.23.3,<1.0", "packaging >=24.2,<27", - "platformdirs >=4.3.7,<5.0", "psutil >=7.0.0,<8.0; sys_platform == 'win32'", - "pydantic >=1.10.21,<3.0", + "pydantic >=2.12.0,<3.0", "python-multipart >=0.0.20,<1.0", "python-socketio >=5.12.0,<6.0", "redis >=5.2.1,<8.0", @@ -37,6 +36,19 @@ dependencies = [ "starlette >=0.47.0", "typing_extensions >=4.13.0", "wrapt >=1.17.0,<3.0", + "reflex-core", + "reflex-components-code", + "reflex-components-core", + "reflex-components-dataeditor", + "reflex-components-gridjs", + "reflex-components-lucide", + "reflex-components-markdown", + "reflex-components-moment", + "reflex-components-plotly", + "reflex-components-radix", + "reflex-components-react-player", + "reflex-components-recharts", + "reflex-components-sonner", ] classifiers = [ @@ -53,7 +65,7 @@ classifiers = [ [project.optional-dependencies] db = [ "alembic >=1.15.2,<2.0", - "pydantic >=1.10.21,<3.0", + "pydantic >=2.12.0,<3.0", "sqlmodel >=0.0.24,<0.1", ] @@ -102,9 +114,15 @@ dev = [ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "uv-dynamic-versioning"] build-backend = "hatchling.build" +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0dev0" + [tool.hatch.build] include = ["reflex", "scripts/hatch_build.py"] targets.sdist.artifacts = ["*.pyi"] @@ -116,6 +134,21 @@ dependencies = ["plotly", "ruff", "pre_commit", "toml"] require-runtime-dependencies = true [tool.pyright] +extraPaths = [ + "packages/reflex-code/src", + "packages/reflex-components/src", + "packages/reflex-dataeditor/src", + "packages/reflex-gridjs/src", + "packages/reflex-lucide/src", + "packages/reflex-markdown/src", + "packages/reflex-moment/src", + "packages/reflex-plotly/src", + "packages/reflex-radix/src", + "packages/reflex-react-player/src", + "packages/reflex-react-router/src", + "packages/reflex-recharts/src", + "packages/reflex-sonner/src", +] reportIncompatibleMethodOverride = false [tool.ruff] @@ -165,8 +198,8 @@ lint.ignore = [ ] lint.pydocstyle.convention = "google" lint.flake8-bugbear.extend-immutable-calls = [ - "reflex.utils.types.Unset", - "reflex.vars.base.Var.create", + "reflex_core.utils.types.Unset", + "reflex_core.vars.base.Var.create", ] [tool.ruff.lint.per-file-ignores] @@ -183,11 +216,11 @@ lint.flake8-bugbear.extend-immutable-calls = [ "N", ] "benchmarks/*.py" = ["ANN001", "D100", "D103", "D104", "B018", "PERF", "T", "N"] -"reflex/.templates/*.py" = ["D100", "D103", "D104"] +"*/.templates/*.py" = ["D100", "D103", "D104"] "*.pyi" = ["D301", "D415", "D417", "D418", "E742", "N", "PGH"] "pyi_generator.py" = ["N802"] -"reflex/constants/*.py" = ["N"] -"reflex/.templates/apps/blank/code/*" = ["INP001"] +"packages/reflex-core/src/reflex_core/constants/*.py" = ["N"] +"*/.templates/apps/blank/code/*" = ["INP001"] "*/blank.py" = ["I001"] [tool.pytest.ini_options] @@ -214,7 +247,7 @@ omit = [ [tool.coverage.report] show_missing = true # TODO bump back to 79 -fail_under = 70 +fail_under = 50 precision = 2 ignore_errors = true @@ -279,13 +312,38 @@ rev = "v1.1.408" hooks = [{ id = "pyright", args = ["reflex", "tests"], language = "system" }] [[tool.pre-commit.repos]] -repo = "https://github.com/pre-commit/mirrors-prettier" -rev = "f62a70a3a7114896b062de517d72829ea1c884b6" -hooks = [{ id = "prettier", require_serial = true }] +repo = "https://github.com/biomejs/pre-commit" +rev = "v0.6.1" +hooks = [ + { id = "biome-format", args = [ + "--indent-width", + "2", + "--indent-style", + "space", + ], additional_dependencies = [ + "@biomejs/biome@2.4.8", + ] }, +] [tool.uv] required-version = ">=0.7.0" -sources = { reflex-docgen = { workspace = true } } + +[tool.uv.sources] +hatch-reflex-pyi.workspace = true +reflex-core.workspace = true +reflex-components-code.workspace = true +reflex-components-core.workspace = true +reflex-components-dataeditor.workspace = true +reflex-components-gridjs.workspace = true +reflex-components-lucide.workspace = true +reflex-components-markdown.workspace = true +reflex-components-moment.workspace = true +reflex-components-plotly.workspace = true +reflex-components-radix.workspace = true +reflex-components-react-player.workspace = true +reflex-components-recharts.workspace = true +reflex-components-sonner.workspace = true +reflex-docgen.workspace = true [tool.uv.workspace] members = ["packages/*"] diff --git a/reflex/__init__.py b/reflex/__init__.py index a43df91ce70..598cd878598 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -86,10 +86,10 @@ import sys -from reflex.utils import lazy_loader +from reflex_core.utils import lazy_loader if sys.version_info < (3, 11): - from reflex.utils import console + from reflex_core.utils import console console.warn( "Reflex support for Python 3.10 is deprecated and will be removed in a future release. Please upgrade to Python 3.11 or higher for continued support." @@ -97,146 +97,29 @@ del console del sys -RADIX_THEMES_MAPPING: dict = { - "components.radix.themes.base": ["color_mode", "theme", "theme_panel"], - "components.radix.themes.color_mode": ["color_mode"], -} -RADIX_THEMES_COMPONENTS_MAPPING: dict = { - **{ - f"components.radix.themes.components.{mod}": [mod] - for mod in [ - "alert_dialog", - "aspect_ratio", - "avatar", - "badge", - "button", - "callout", - "card", - "checkbox", - "context_menu", - "data_list", - "dialog", - "hover_card", - "icon_button", - "input", - "inset", - "popover", - "scroll_area", - "select", - "skeleton", - "slider", - "spinner", - "switch", - "table", - "tabs", - "text_area", - "tooltip", - "segmented_control", - "radio_cards", - "checkbox_cards", - "checkbox_group", - ] - }, - "components.radix.themes.components.text_field": ["text_field", "input"], - "components.radix.themes.components.radio_group": ["radio", "radio_group"], - "components.radix.themes.components.dropdown_menu": ["menu", "dropdown_menu"], - "components.radix.themes.components.separator": ["divider", "separator"], - "components.radix.themes.components.progress": ["progress"], -} - -RADIX_THEMES_LAYOUT_MAPPING: dict = { - "components.radix.themes.layout.box": [ - "box", - ], - "components.radix.themes.layout.center": [ - "center", - ], - "components.radix.themes.layout.container": [ - "container", - ], - "components.radix.themes.layout.flex": [ - "flex", - ], - "components.radix.themes.layout.grid": [ - "grid", - ], - "components.radix.themes.layout.section": [ - "section", - ], - "components.radix.themes.layout.spacer": [ - "spacer", - ], - "components.radix.themes.layout.stack": [ - "stack", - "hstack", - "vstack", - ], - "components.radix.themes.layout.list": [ - ("list_ns", "list"), - "list_item", - "ordered_list", - "unordered_list", - ], -} - -RADIX_THEMES_TYPOGRAPHY_MAPPING: dict = { - "components.radix.themes.typography.blockquote": [ - "blockquote", - ], - "components.radix.themes.typography.code": [ - "code", - ], - "components.radix.themes.typography.heading": [ - "heading", - ], - "components.radix.themes.typography.link": [ - "link", - ], - "components.radix.themes.typography.text": [ - "text", - ], -} - -RADIX_PRIMITIVES_MAPPING: dict = { - "components.radix.primitives.accordion": [ - "accordion", - ], - "components.radix.primitives.drawer": [ - "drawer", - ], - "components.radix.primitives.form": [ - "form", - ], - "components.radix.primitives.progress": [ - "progress", - ], -} +from reflex_components_radix.mappings import RADIX_MAPPING # noqa: E402 -RADIX_PRIMITIVES_SHORTCUT_MAPPING: dict = { - k: v for k, v in RADIX_PRIMITIVES_MAPPING.items() if "progress" not in k -} - -COMPONENTS_CORE_MAPPING: dict = { - "components.core.banner": [ +_COMPONENTS_CORE_MAPPING: dict[str, list[str]] = { + "reflex_components_core.core.banner": [ "connection_banner", "connection_modal", ], - "components.core.cond": ["cond", "color_mode_cond"], - "components.core.foreach": ["foreach"], - "components.core.debounce": ["debounce_input"], - "components.core.html": ["html"], - "components.core.match": ["match"], - "components.core.clipboard": ["clipboard"], - "components.core.colors": ["color"], - "components.core.breakpoints": ["breakpoints"], - "components.core.responsive": [ + "reflex_components_core.core.cond": ["cond", "color_mode_cond"], + "reflex_components_core.core.foreach": ["foreach"], + "reflex_components_core.core.debounce": ["debounce_input"], + "reflex_components_core.core.html": ["html"], + "reflex_components_core.core.match": ["match"], + "reflex_components_core.core.clipboard": ["clipboard"], + "reflex_components_core.core.colors": ["color"], + "reflex_components_core.core.breakpoints": ["breakpoints"], + "reflex_components_core.core.responsive": [ "desktop_only", "mobile_and_tablet", "mobile_only", "tablet_and_desktop", "tablet_only", ], - "components.core.upload": [ + "reflex_components_core.core.upload": [ "cancel_upload", "clear_selected_files", "get_upload_dir", @@ -244,56 +127,60 @@ "selected_files", "upload", ], - "components.core.auto_scroll": ["auto_scroll"], - "components.core.window_events": ["window_event_listener"], -} - -COMPONENTS_BASE_MAPPING: dict = { - "components.base.fragment": ["fragment", "Fragment"], - "components.base.script": ["script", "Script"], + "reflex_components_core.core.auto_scroll": ["auto_scroll"], + "reflex_components_core.core.window_events": ["window_event_listener"], } -RADIX_MAPPING: dict = { - **RADIX_THEMES_MAPPING, - **RADIX_THEMES_COMPONENTS_MAPPING, - **RADIX_THEMES_TYPOGRAPHY_MAPPING, - **RADIX_THEMES_LAYOUT_MAPPING, - **RADIX_PRIMITIVES_SHORTCUT_MAPPING, +_COMPONENTS_BASE_MAPPING: dict[str, list[str]] = { + "reflex_components_core.base.fragment": ["fragment", "Fragment"], + "reflex_components_core.base.script": ["script", "Script"], } -_MAPPING: dict = { - "experimental": ["_x"], - "admin": ["AdminDash"], - "app": ["App", "UploadFile"], - "assets": ["asset"], - "base": ["Base"], - "components.component": [ +_ALL_COMPONENTS_MAPPING: dict[str, list[str]] = { + "reflex_core.components.component": [ "Component", "NoSSRComponent", "memo", "ComponentNamespace", ], - "components.el.elements.media": ["image"], - "components.lucide": ["icon"], - **COMPONENTS_BASE_MAPPING, - "components": ["el", "radix", "lucide", "recharts"], - "components.markdown": ["markdown"], + "reflex_components_core.el.elements.media": ["image"], + "reflex_components_lucide": ["icon"], + **_COMPONENTS_BASE_MAPPING, + "reflex_components_core": ["el"], + "reflex_components_markdown.markdown": ["markdown"], **RADIX_MAPPING, - "components.plotly": ["plotly"], - "components.react_player": ["audio", "video"], - **COMPONENTS_CORE_MAPPING, - "components.datadisplay.code": [ + "reflex_components_plotly": ["plotly"], + "reflex_components_react_player": ["audio", "video"], + **_COMPONENTS_CORE_MAPPING, + "reflex_components_code.code": [ "code_block", ], - "components.datadisplay.dataeditor": [ + "reflex_components_dataeditor.dataeditor": [ "data_editor", "data_editor_theme", ], - "components.sonner.toast": ["toast"], - "components.props": ["PropsBase"], - "components.datadisplay.logo": ["logo"], - "components.gridjs": ["data_table"], - "components.moment": ["MomentDelta", "moment"], + "reflex_components_sonner.toast": ["toast"], + "reflex_core.components.props": ["PropsBase"], + "reflex_components_core.datadisplay.logo": ["logo"], + "reflex_components_gridjs": ["data_table"], + "reflex_components_moment": ["MomentDelta", "moment"], +} + +_COMPONENT_NAME_TO_PATH: dict[str, str] = { + comp: path + "." + comp + for path, comps in _ALL_COMPONENTS_MAPPING.items() + for comp in comps +} | { + "radix": "reflex_components_radix", + "lucide": "reflex_components_lucide", + "recharts": "reflex_components_recharts", +} + +_MAPPING: dict[str, list[str]] = { + "experimental": ["_x"], + "admin": ["AdminDash"], + "app": ["App", "UploadFile"], + "assets": ["asset"], "config": ["Config", "DBConfig"], "constants": ["Env"], "constants.colors": ["Color"], @@ -356,6 +243,7 @@ "style", "admin", "base", + "constants", "model", "testing", "utils", @@ -364,11 +252,13 @@ "compiler", "plugins", } -_SUBMOD_ATTRS: dict = _MAPPING +_SUBMOD_ATTRS: dict[str, list[str]] = _MAPPING +_EXTRA_MAPPINGS: dict[str, str] = _COMPONENT_NAME_TO_PATH getattr, __dir__, __all__ = lazy_loader.attach( __name__, submodules=_SUBMODULES, submod_attrs=_SUBMOD_ATTRS, + **_EXTRA_MAPPINGS, ) diff --git a/reflex/_upload.py b/reflex/_upload.py index 1e7ca21537c..6820782b940 100644 --- a/reflex/_upload.py +++ b/reflex/_upload.py @@ -1,719 +1,3 @@ -"""Backend upload helpers and routes for Reflex apps.""" +"""Re-export from reflex_components_core.core._upload.""" -from __future__ import annotations - -import asyncio -import contextlib -import dataclasses -from collections import deque -from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable -from pathlib import Path -from typing import TYPE_CHECKING, Any, BinaryIO, cast - -from python_multipart.multipart import MultipartParser, parse_options_header -from starlette.datastructures import Headers -from starlette.datastructures import UploadFile as StarletteUploadFile -from starlette.exceptions import HTTPException -from starlette.formparsers import MultiPartException, _user_safe_decode -from starlette.requests import ClientDisconnect, Request -from starlette.responses import JSONResponse, Response, StreamingResponse -from typing_extensions import Self - -from reflex import constants -from reflex.utils import exceptions - -if TYPE_CHECKING: - from reflex.app import App - from reflex.event import EventHandler - from reflex.state import BaseState - from reflex.utils.types import Receive, Scope, Send - - -@dataclasses.dataclass(frozen=True) -class UploadFile(StarletteUploadFile): - """A file uploaded to the server. - - Args: - file: The standard Python file object (non-async). - filename: The original file name. - size: The size of the file in bytes. - headers: The headers of the request. - """ - - file: BinaryIO - - path: Path | None = dataclasses.field(default=None) - - size: int | None = dataclasses.field(default=None) - - headers: Headers = dataclasses.field(default_factory=Headers) - - @property - def filename(self) -> str | None: - """Get the name of the uploaded file. - - Returns: - The name of the uploaded file. - """ - return self.name - - @property - def name(self) -> str | None: - """Get the name of the uploaded file. - - Returns: - The name of the uploaded file. - """ - if self.path: - return self.path.name - return None - - -@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) -class UploadChunk: - """A chunk of uploaded file data.""" - - filename: str - offset: int - content_type: str - data: bytes - - -class UploadChunkIterator(AsyncIterator[UploadChunk]): - """An async iterator over uploaded file chunks.""" - - __slots__ = ( - "_chunks", - "_closed", - "_condition", - "_consumer_task", - "_error", - "_maxsize", - ) - - def __init__(self, *, maxsize: int = 8): - """Initialize the iterator. - - Args: - maxsize: Maximum number of chunks to buffer before blocking producers. - """ - self._maxsize = maxsize - self._chunks: deque[UploadChunk] = deque() - self._condition = asyncio.Condition() - self._closed = False - self._error: Exception | None = None - self._consumer_task: asyncio.Task[Any] | None = None - - def __aiter__(self) -> Self: - """Return the iterator itself. - - Returns: - The upload chunk iterator. - """ - return self - - async def __anext__(self) -> UploadChunk: - """Yield the next available upload chunk. - - Returns: - The next upload chunk. - - Raises: - _error: Any error forwarded from the upload producer. - StopAsyncIteration: When all chunks have been consumed. - """ - async with self._condition: - while not self._chunks and not self._closed: - await self._condition.wait() - - if self._chunks: - chunk = self._chunks.popleft() - self._condition.notify_all() - return chunk - - if self._error is not None: - raise self._error - raise StopAsyncIteration - - def set_consumer_task(self, task: asyncio.Task[Any]) -> None: - """Track the task consuming this iterator. - - Args: - task: The background task consuming upload chunks. - """ - self._consumer_task = task - task.add_done_callback(self._wake_waiters) - - async def push(self, chunk: UploadChunk) -> None: - """Push a new chunk into the iterator. - - Args: - chunk: The chunk to push. - - Raises: - RuntimeError: If the iterator is already closed or the consumer exited early. - """ - async with self._condition: - while len(self._chunks) >= self._maxsize and not self._closed: - self._raise_if_consumer_finished() - await self._condition.wait() - - if self._closed: - msg = "Upload chunk iterator is closed." - raise RuntimeError(msg) - - self._raise_if_consumer_finished() - self._chunks.append(chunk) - self._condition.notify_all() - - async def finish(self) -> None: - """Mark the iterator as complete.""" - async with self._condition: - if self._closed: - return - self._closed = True - self._condition.notify_all() - - async def fail(self, error: Exception) -> None: - """Mark the iterator as failed. - - Args: - error: The error to raise from the iterator. - """ - async with self._condition: - if self._closed: - return - self._closed = True - self._error = error - self._condition.notify_all() - - def _raise_if_consumer_finished(self) -> None: - """Raise if the consumer task exited before draining the iterator. - - Raises: - RuntimeError: If the consumer task completed before draining the iterator. - """ - if self._consumer_task is None or not self._consumer_task.done(): - return - - try: - task_exc = self._consumer_task.exception() - except asyncio.CancelledError as err: - task_exc = err - - msg = "Upload handler returned before consuming all upload chunks." - if task_exc is not None: - raise RuntimeError(msg) from task_exc - raise RuntimeError(msg) - - def _wake_waiters(self, task: asyncio.Task[Any]) -> None: - """Wake any producers or consumers blocked on the iterator condition. - - Args: - task: The completed consumer task. - """ - task.get_loop().create_task(self._notify_waiters()) - - async def _notify_waiters(self) -> None: - """Notify tasks waiting on the iterator condition.""" - async with self._condition: - self._condition.notify_all() - - -@dataclasses.dataclass(kw_only=True, slots=True) -class _UploadChunkPart: - """Track the current multipart file part for upload streaming.""" - - content_disposition: bytes | None = None - field_name: str = "" - filename: str | None = None - content_type: str = "" - item_headers: list[tuple[bytes, bytes]] = dataclasses.field(default_factory=list) - offset: int = 0 - bytes_emitted: int = 0 - is_upload_chunk: bool = False - - -@dataclasses.dataclass(kw_only=True, slots=True) -class _UploadChunkMultipartParser: - """Streaming multipart parser for streamed upload files.""" - - headers: Headers - stream: AsyncGenerator[bytes, None] - chunk_iter: UploadChunkIterator - _charset: str = "" - _current_partial_header_name: bytes = b"" - _current_partial_header_value: bytes = b"" - _current_part: _UploadChunkPart = dataclasses.field( - default_factory=_UploadChunkPart - ) - _chunks_to_emit: deque[UploadChunk] = dataclasses.field(default_factory=deque) - _seen_upload_chunk: bool = False - _part_count: int = 0 - _emitted_chunk_count: int = 0 - _emitted_bytes: int = 0 - _stream_chunk_count: int = 0 - - def on_part_begin(self) -> None: - """Reset parser state for a new multipart part.""" - self._current_part = _UploadChunkPart() - - def on_part_data(self, data: bytes, start: int, end: int) -> None: - """Record streamed chunk data for the current part.""" - if ( - not self._current_part.is_upload_chunk - or self._current_part.filename is None - ): - return - - message_bytes = data[start:end] - self._chunks_to_emit.append( - UploadChunk( - filename=self._current_part.filename, - offset=self._current_part.offset + self._current_part.bytes_emitted, - content_type=self._current_part.content_type, - data=message_bytes, - ) - ) - self._current_part.bytes_emitted += len(message_bytes) - self._emitted_chunk_count += 1 - self._emitted_bytes += len(message_bytes) - - def on_part_end(self) -> None: - """Emit a zero-byte chunk for empty file parts.""" - if ( - self._current_part.is_upload_chunk - and self._current_part.filename is not None - and self._current_part.bytes_emitted == 0 - ): - self._chunks_to_emit.append( - UploadChunk( - filename=self._current_part.filename, - offset=self._current_part.offset, - content_type=self._current_part.content_type, - data=b"", - ) - ) - self._emitted_chunk_count += 1 - - def on_header_field(self, data: bytes, start: int, end: int) -> None: - """Accumulate multipart header field bytes.""" - self._current_partial_header_name += data[start:end] - - def on_header_value(self, data: bytes, start: int, end: int) -> None: - """Accumulate multipart header value bytes.""" - self._current_partial_header_value += data[start:end] - - def on_header_end(self) -> None: - """Store the completed multipart header.""" - field = self._current_partial_header_name.lower() - if field == b"content-disposition": - self._current_part.content_disposition = self._current_partial_header_value - self._current_part.item_headers.append(( - field, - self._current_partial_header_value, - )) - self._current_partial_header_name = b"" - self._current_partial_header_value = b"" - - def on_headers_finished(self) -> None: - """Parse upload metadata from multipart headers.""" - disposition, options = parse_options_header( - self._current_part.content_disposition - ) - if disposition != b"form-data": - msg = "Invalid upload chunk disposition." - raise MultiPartException(msg) - - try: - field_name = _user_safe_decode(options[b"name"], self._charset) - except KeyError as err: - msg = 'The Content-Disposition header field "name" must be provided.' - raise MultiPartException(msg) from err - - try: - filename = _user_safe_decode(options[b"filename"], self._charset) - except KeyError: - # Ignore non-file form fields entirely. - return - filename = Path(filename.lstrip("/")).name - - content_type = "" - for header_name, header_value in self._current_part.item_headers: - if header_name == b"content-type": - content_type = _user_safe_decode(header_value, self._charset) - break - - self._current_part.field_name = field_name - self._current_part.filename = filename - self._current_part.content_type = content_type - self._current_part.offset = 0 - self._current_part.bytes_emitted = 0 - self._current_part.is_upload_chunk = True - self._seen_upload_chunk = True - self._part_count += 1 - - def on_end(self) -> None: - """Finalize parser callbacks.""" - - async def _flush_emitted_chunks(self) -> None: - """Push parsed upload chunks into the handler iterator.""" - while self._chunks_to_emit: - await self.chunk_iter.push(self._chunks_to_emit.popleft()) - - async def parse(self) -> None: - """Parse the incoming request stream and push chunks to the iterator. - - Raises: - MultiPartException: If the request is not valid multipart upload data. - RuntimeError: If the upload handler exits before consuming all chunks. - """ - _, params = parse_options_header(self.headers["Content-Type"]) - charset = params.get(b"charset", "utf-8") - if isinstance(charset, bytes): - charset = charset.decode("latin-1") - self._charset = charset - - try: - boundary = params[b"boundary"] - except KeyError as err: - msg = "Missing boundary in multipart." - raise MultiPartException(msg) from err - - callbacks = { - "on_part_begin": self.on_part_begin, - "on_part_data": self.on_part_data, - "on_part_end": self.on_part_end, - "on_header_field": self.on_header_field, - "on_header_value": self.on_header_value, - "on_header_end": self.on_header_end, - "on_headers_finished": self.on_headers_finished, - "on_end": self.on_end, - } - parser = MultipartParser(boundary, cast(Any, callbacks)) - - async for chunk in self.stream: - self._stream_chunk_count += 1 - parser.write(chunk) - await self._flush_emitted_chunks() - - parser.finalize() - await self._flush_emitted_chunks() - - -class _UploadStreamingResponse(StreamingResponse): - """Streaming response that always releases upload form resources.""" - - _on_finish: Callable[[], Awaitable[None]] - - def __init__( - self, - *args: Any, - on_finish: Callable[[], Awaitable[None]], - **kwargs: Any, - ) -> None: - super().__init__(*args, **kwargs) - self._on_finish = on_finish - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - try: - await super().__call__(scope, receive, send) - finally: - await self._on_finish() - - -def _require_upload_headers(request: Request) -> tuple[str, str]: - """Extract the required upload headers from a request. - - Args: - request: The incoming request. - - Returns: - The client token and event handler name. - - Raises: - HTTPException: If the upload headers are missing. - """ - token = request.headers.get("reflex-client-token") - handler = request.headers.get("reflex-event-handler") - - if not token or not handler: - raise HTTPException( - status_code=400, - detail="Missing reflex-client-token or reflex-event-handler header.", - ) - - return token, handler - - -async def _get_upload_runtime_handler( - app: App, - token: str, - handler_name: str, -) -> tuple[BaseState, EventHandler]: - """Resolve the runtime state and event handler for an upload request. - - Args: - app: The Reflex app. - token: The client token. - handler_name: The fully qualified event handler name. - - Returns: - The root state instance and resolved event handler. - """ - from reflex.state import _substate_key - - substate_token = _substate_key(token, handler_name.rpartition(".")[0]) - state = await app.state_manager.get_state(substate_token) - _current_state, event_handler = state._get_event_handler(handler_name) - return state, event_handler - - -def _seed_upload_router_data(state: BaseState, token: str) -> None: - """Ensure upload-launched handlers have the client token in router state. - - Background upload handlers use ``StateProxy`` which derives its mutable-state - token from ``self.router.session.client_token``. Upload requests do not flow - through the normal websocket event pipeline, so we seed the token here. - - Args: - state: The root state instance. - token: The client token from the upload request. - """ - from reflex.state import RouterData - - router_data = dict(state.router_data) - if router_data.get(constants.RouteVar.CLIENT_TOKEN) == token: - return - - router_data[constants.RouteVar.CLIENT_TOKEN] = token - state.router_data = router_data - state.router = RouterData.from_router_data(router_data) - - -async def _upload_buffered_file( - request: Request, - app: App, - *, - token: str, - handler_name: str, - handler_upload_param: tuple[str, Any], -) -> Response: - """Handle buffered uploads on the standard upload endpoint. - - Returns: - A streaming response for the buffered upload. - """ - from reflex.event import Event - from reflex.utils.exceptions import UploadValueError - - try: - form_data = await request.form() - except ClientDisconnect: - return Response() - - form_data_closed = False - - async def _close_form_data() -> None: - """Close the parsed form data exactly once.""" - nonlocal form_data_closed - if form_data_closed: - return - form_data_closed = True - await form_data.close() - - def _create_upload_event() -> Event: - """Create an upload event using the live Starlette temp files. - - Returns: - The upload event backed by the parsed files. - """ - files = form_data.getlist("files") - file_uploads = [] - for file in files: - if not isinstance(file, StarletteUploadFile): - raise UploadValueError( - "Uploaded file is not an UploadFile." + str(file) - ) - file_uploads.append( - UploadFile( - file=file.file, - path=Path(file.filename.lstrip("/")) if file.filename else None, - size=file.size, - headers=file.headers, - ) - ) - - return Event( - token=token, - name=handler_name, - payload={handler_upload_param[0]: file_uploads}, - ) - - event: Event | None = None - try: - event = _create_upload_event() - finally: - if event is None: - await _close_form_data() - - if event is None: - msg = "Upload event was not created." - raise RuntimeError(msg) - - async def _ndjson_updates(): - """Process the upload event, generating ndjson updates. - - Yields: - Each state update as newline-delimited JSON. - """ - async with app.state_manager.modify_state_with_links( - event.substate_token, event=event - ) as state: - async for update in state._process(event): - update = await app._postprocess(state, event, update) - yield update.json() + "\n" - - return _UploadStreamingResponse( - _ndjson_updates(), - media_type="application/x-ndjson", - on_finish=_close_form_data, - ) - - -def _background_upload_accepted_response() -> StreamingResponse: - """Return a minimal ndjson response for background upload dispatch.""" - from reflex.state import StateUpdate - - def _accepted_updates(): - yield StateUpdate(final=True).json() + "\n" - - return StreamingResponse( - _accepted_updates(), - media_type="application/x-ndjson", - status_code=202, - ) - - -async def _upload_chunk_file( - request: Request, - app: App, - *, - token: str, - handler_name: str, - handler_upload_param: tuple[str, Any], - acknowledge_on_upload_endpoint: bool, -) -> Response: - """Handle a streaming upload request. - - Returns: - The streaming upload response. - """ - from reflex.event import Event - - chunk_iter = UploadChunkIterator(maxsize=8) - event = Event( - token=token, - name=handler_name, - payload={handler_upload_param[0]: chunk_iter}, - ) - - async with app.state_manager.modify_state_with_links( - event.substate_token, - event=event, - ) as state: - _seed_upload_router_data(state, token) - task = app._process_background(state, event) - - if task is None: - msg = f"@rx.event(background=True) is required for upload_files_chunk handler `{handler_name}`." - return JSONResponse({"detail": msg}, status_code=400) - - chunk_iter.set_consumer_task(task) - - parser = _UploadChunkMultipartParser( - headers=request.headers, - stream=request.stream(), - chunk_iter=chunk_iter, - ) - - try: - await parser.parse() - except ClientDisconnect: - task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await task - return Response() - except (MultiPartException, RuntimeError, ValueError) as err: - await chunk_iter.fail(err) - return JSONResponse({"detail": str(err)}, status_code=400) - - try: - await chunk_iter.finish() - except RuntimeError as err: - return JSONResponse({"detail": str(err)}, status_code=400) - - if acknowledge_on_upload_endpoint: - return _background_upload_accepted_response() - return Response(status_code=202) - - -def upload(app: App): - """Upload files, dispatching to buffered or streaming handling. - - Args: - app: The app to upload the file for. - - Returns: - The upload function. - """ - - async def upload_file(request: Request): - """Upload a file. - - Args: - request: The Starlette request object. - - Returns: - The upload response. - - Raises: - UploadValueError: If the handler does not have a supported annotation. - UploadTypeError: If a non-streaming upload is wired to a background task. - HTTPException: when the request does not include token / handler headers. - """ - from reflex.event import ( - resolve_upload_chunk_handler_param, - resolve_upload_handler_param, - ) - - token, handler_name = _require_upload_headers(request) - _state, event_handler = await _get_upload_runtime_handler( - app, token, handler_name - ) - - if event_handler.is_background: - try: - handler_upload_param = resolve_upload_chunk_handler_param(event_handler) - except exceptions.UploadValueError: - pass - else: - return await _upload_chunk_file( - request, - app, - token=token, - handler_name=handler_name, - handler_upload_param=handler_upload_param, - acknowledge_on_upload_endpoint=True, - ) - - handler_upload_param = resolve_upload_handler_param(event_handler) - return await _upload_buffered_file( - request, - app, - token=token, - handler_name=handler_name, - handler_upload_param=handler_upload_param, - ) - - return upload_file +from reflex_components_core.core._upload import * diff --git a/reflex/app.py b/reflex/app.py index d9b579b6f18..6c4a127a649 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -30,6 +30,40 @@ from types import SimpleNamespace from typing import TYPE_CHECKING, Any, ParamSpec +from reflex_components_core.base.app_wrap import AppWrap +from reflex_components_core.base.error_boundary import ErrorBoundary +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.base.strict_mode import StrictMode +from reflex_components_core.core.banner import ( + backend_disabled, + connection_pulser, + connection_toaster, +) +from reflex_components_core.core.breakpoints import set_breakpoints +from reflex_components_core.core.sticky import sticky +from reflex_components_radix import themes +from reflex_components_sonner.toast import toast +from reflex_core import constants +from reflex_core.components.component import ( + CUSTOM_COMPONENTS, + Component, + ComponentStyle, + evaluate_style_namespaces, +) +from reflex_core.config import get_config +from reflex_core.environment import ExecutorType, environment +from reflex_core.event import ( + _EVENT_FIELDS, + Event, + EventSpec, + EventType, + IndividualEventType, + get_hydrate_event, + noop, +) +from reflex_core.utils import console +from reflex_core.utils.imports import ImportVar +from reflex_core.utils.types import ASGIApp, Message, Receive, Scope, Send from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from socketio import ASGIApp as EngineIOApp from socketio import AsyncNamespace, AsyncServer @@ -40,7 +74,6 @@ from starlette.staticfiles import StaticFiles from typing_extensions import Unpack -from reflex import constants from reflex._upload import UploadFile as UploadFile from reflex._upload import upload from reflex.admin import AdminDash @@ -52,36 +85,6 @@ compile_theme, readable_name_from_component, ) -from reflex.components.base.app_wrap import AppWrap -from reflex.components.base.error_boundary import ErrorBoundary -from reflex.components.base.fragment import Fragment -from reflex.components.base.strict_mode import StrictMode -from reflex.components.component import ( - CUSTOM_COMPONENTS, - Component, - ComponentStyle, - evaluate_style_namespaces, -) -from reflex.components.core.banner import ( - backend_disabled, - connection_pulser, - connection_toaster, -) -from reflex.components.core.breakpoints import set_breakpoints -from reflex.components.core.sticky import sticky -from reflex.components.radix import themes -from reflex.components.sonner.toast import toast -from reflex.config import get_config -from reflex.environment import ExecutorType, environment -from reflex.event import ( - _EVENT_FIELDS, - Event, - EventSpec, - EventType, - IndividualEventType, - get_hydrate_event, - noop, -) from reflex.experimental.memo import EXPERIMENTAL_MEMOS from reflex.istate.manager import StateModificationContext from reflex.istate.proxy import StateProxy @@ -104,7 +107,6 @@ ) from reflex.utils import ( codespaces, - console, exceptions, format, frontend_skeleton, @@ -118,13 +120,11 @@ is_testing_env, should_prerender_routes, ) -from reflex.utils.imports import ImportVar from reflex.utils.misc import run_in_thread from reflex.utils.token_manager import RedisTokenManager, TokenManager -from reflex.utils.types import ASGIApp, Message, Receive, Scope, Send if TYPE_CHECKING: - from reflex.vars import Var + from reflex_core.vars import Var # Define custom types. ComponentCallable = Callable[[], Component | tuple[Component, ...] | str | Var] @@ -154,7 +154,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec: EventSpec: The window alert event. """ - from reflex.components.sonner.toast import toast + from reflex_components_sonner.toast import toast error = traceback.format_exc() @@ -211,7 +211,7 @@ def default_overlay_component() -> Component: Returns: The default overlay_component, which is a connection_modal. """ - from reflex.components.component import memo + from reflex_core.components.component import memo def default_overlay_components(): return Fragment.create( @@ -577,8 +577,9 @@ def __call__(self) -> ASGIApp: Raises: ValueError: If the app has not been initialized. """ + from reflex_core.vars.base import GLOBAL_CACHE + from reflex.assets import remove_stale_external_asset_symlinks - from reflex.vars.base import GLOBAL_CACHE # Clean up stale symlinks in assets/external/ before compiling, so that # rx.asset(shared=True) symlink re-creation doesn't trigger further reloads. @@ -654,7 +655,7 @@ def _add_default_endpoints(self): def _add_optional_endpoints(self): """Add optional api endpoints (_upload).""" - from reflex.components.core.upload import Upload, get_upload_dir + from reflex_components_core.core.upload import Upload, get_upload_dir if not self._api: return @@ -773,7 +774,7 @@ def add_page( if route == constants.Page404.SLUG: if component is None: - from reflex.components.el.elements import span + from reflex_components_core.el.elements import span component = span("404: Page not found") component = self._generate_component(component) @@ -896,7 +897,7 @@ def _check_routes_conflict(self, new_route: str): Raises: RouteValueError: exception showing which conflict exist with the route to be added """ - from reflex.utils.exceptions import RouteValueError + from reflex_core.utils.exceptions import RouteValueError if "[" not in new_route: return @@ -1057,7 +1058,7 @@ def _setup_overlay_component(self): def _setup_sticky_badge(self): """Add the sticky badge to the app.""" - from reflex.components.component import memo + from reflex_core.components.component import memo @memo def memoized_badge(): @@ -1123,7 +1124,7 @@ def _compile( ReflexRuntimeError: When any page uses state, but no rx.State subclass is defined. FileNotFoundError: When a plugin requires a file that does not exist. """ - from reflex.utils.exceptions import ReflexRuntimeError + from reflex_core.utils.exceptions import ReflexRuntimeError self._apply_decorated_pages() @@ -1257,7 +1258,7 @@ def get_compilation_time() -> str: all_imports = {} if (toaster := self.toaster) is not None: - from reflex.components.component import memo + from reflex_core.components.component import memo @memo def memoized_toast_provider(): diff --git a/reflex/app_mixins/lifespan.py b/reflex/app_mixins/lifespan.py index 075129024c5..e6a61d34245 100644 --- a/reflex/app_mixins/lifespan.py +++ b/reflex/app_mixins/lifespan.py @@ -10,11 +10,10 @@ import time from collections.abc import Callable, Coroutine +from reflex_core.utils import console +from reflex_core.utils.exceptions import InvalidLifespanTaskTypeError from starlette.applications import Starlette -from reflex.utils import console -from reflex.utils.exceptions import InvalidLifespanTaskTypeError - from .mixin import AppMixin diff --git a/reflex/app_mixins/middleware.py b/reflex/app_mixins/middleware.py index b78b96ec2dd..e999fcc4c31 100644 --- a/reflex/app_mixins/middleware.py +++ b/reflex/app_mixins/middleware.py @@ -5,7 +5,8 @@ import dataclasses import inspect -from reflex.event import Event +from reflex_core.event import Event + from reflex.middleware import HydrateMiddleware, Middleware from reflex.state import BaseState, StateUpdate diff --git a/reflex/assets.py b/reflex/assets.py index d86e93ad9c6..c7dcd46a93f 100644 --- a/reflex/assets.py +++ b/reflex/assets.py @@ -3,8 +3,8 @@ import inspect from pathlib import Path -from reflex import constants -from reflex.environment import EnvironmentVariables +from reflex_core import constants +from reflex_core.environment import EnvironmentVariables def remove_stale_external_asset_symlinks(): diff --git a/reflex/base.py b/reflex/base.py deleted file mode 100644 index a7bd70b3c99..00000000000 --- a/reflex/base.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Define the base Reflex class.""" - -from importlib.util import find_spec - -if find_spec("pydantic") and find_spec("pydantic.v1"): - from pydantic.v1 import BaseModel - - from reflex.utils.compat import ModelMetaclassLazyAnnotations - - class Base(BaseModel, metaclass=ModelMetaclassLazyAnnotations): - """The base class subclassed by all Reflex classes. - - This class wraps Pydantic and provides common methods such as - serialization and setting fields. - - Any data structure that needs to be transferred between the - frontend and backend should subclass this class. - """ - - class Config: - """Pydantic config.""" - - arbitrary_types_allowed = True - use_enum_values = True - extra = "allow" - - def __init_subclass__(cls): - """Warn that rx.Base is deprecated.""" - from reflex.utils import console - - console.deprecate( - feature_name="rx.Base", - reason=f"{cls!r} is subclassing rx.Base. You can subclass from `pydantic.BaseModel` directly instead or use dataclasses if possible.", - deprecation_version="0.8.15", - removal_version="0.9.0", - ) - super().__init_subclass__() - - def json(self) -> str: - """Convert the object to a json string. - - Returns: - The object as a json string. - """ - from reflex.utils.serializers import serialize - - return self.__config__.json_dumps( - self.dict(), - default=serialize, - ) - -else: - - class PydanticNotFoundFallback: - """Fallback base class for environments without Pydantic.""" - - def __init__(self, *args, **kwargs): - """Initialize the base class. - - Args: - *args: Positional arguments. - **kwargs: Keyword arguments. - - Raises: - ImportError: As Pydantic is not installed. - """ - msg = ( - "Pydantic is not installed. Please install it to use rx.Base." - "You can install it with `pip install pydantic`." - ) - raise ImportError(msg) - - Base = PydanticNotFoundFallback # pyright: ignore[reportAssignmentType] diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index c5f18cad07c..d17a8baf012 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -8,34 +8,35 @@ from pathlib import Path from typing import TYPE_CHECKING, Any -from reflex import constants -from reflex.compiler import templates, utils -from reflex.components.base.fragment import Fragment -from reflex.components.component import ( +from reflex_components_core.base.fragment import Fragment +from reflex_core import constants +from reflex_core.components.component import ( BaseComponent, Component, ComponentStyle, CustomComponent, StatefulComponent, ) -from reflex.config import get_config -from reflex.constants.compiler import PageNames, ResetStylesheet -from reflex.constants.state import FIELD_MARKER -from reflex.environment import environment +from reflex_core.config import get_config +from reflex_core.constants.compiler import PageNames, ResetStylesheet +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.environment import environment +from reflex_core.style import SYSTEM_COLOR_MODE +from reflex_core.utils.exceptions import ReflexError +from reflex_core.utils.format import to_title_case +from reflex_core.utils.imports import ImportVar, ParsedImportDict +from reflex_core.vars.base import LiteralVar, Var + +from reflex.compiler import templates, utils from reflex.experimental.memo import ( ExperimentalMemoComponentDefinition, ExperimentalMemoDefinition, ExperimentalMemoFunctionDefinition, ) from reflex.state import BaseState -from reflex.style import SYSTEM_COLOR_MODE from reflex.utils import console, path_ops -from reflex.utils.exceptions import ReflexError from reflex.utils.exec import is_prod_mode -from reflex.utils.format import to_title_case -from reflex.utils.imports import ImportVar, ParsedImportDict from reflex.utils.prerequisites import get_web_dir -from reflex.vars.base import LiteralVar, Var def _apply_common_imports( @@ -87,7 +88,7 @@ def _compile_app(app_root: Component) -> str: Returns: The compiled app. """ - from reflex.components.dynamic import bundled_libraries + from reflex_core.components.dynamic import bundled_libraries window_libraries = [ (_normalize_library_name(name), name) for name in bundled_libraries @@ -839,7 +840,7 @@ def compile_unevaluated_page( component._add_style_recursive(style or {}, theme) - from reflex.utils.format import make_default_page_title + from reflex_core.utils.format import make_default_page_title component = Fragment.create(component) diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 1bf6c8ce061..42cea30e3b0 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -1,741 +1,3 @@ -"""Templates to use in the reflex compiler.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import json -from collections.abc import Iterable, Mapping -from typing import TYPE_CHECKING, Any, Literal - -from reflex import constants -from reflex.constants import Hooks -from reflex.utils.format import format_state_name, json_dumps -from reflex.vars.base import VarData - -if TYPE_CHECKING: - from reflex.compiler.utils import _ImportDict - from reflex.components.component import Component, StatefulComponent - - -def _sort_hooks( - hooks: dict[str, VarData | None], -) -> tuple[list[str], list[str], list[str]]: - """Sort the hooks by their position. - - Args: - hooks: The hooks to sort. - - Returns: - The sorted hooks. - """ - internal_hooks = [] - pre_trigger_hooks = [] - post_trigger_hooks = [] - - for hook, data in hooks.items(): - if data and data.position and data.position == Hooks.HookPosition.INTERNAL: - internal_hooks.append(hook) - elif not data or ( - not data.position - or data.position == constants.Hooks.HookPosition.PRE_TRIGGER - ): - pre_trigger_hooks.append(hook) - elif ( - data - and data.position - and data.position == constants.Hooks.HookPosition.POST_TRIGGER - ): - post_trigger_hooks.append(hook) - - return internal_hooks, pre_trigger_hooks, post_trigger_hooks - - -class _RenderUtils: - @staticmethod - def render(component: Mapping[str, Any] | str) -> str: - if isinstance(component, str): - return component or "null" - if "iterable" in component: - return _RenderUtils.render_iterable_tag(component) - if "match_cases" in component: - return _RenderUtils.render_match_tag(component) - if "cond_state" in component: - return _RenderUtils.render_condition_tag(component) - if (contents := component.get("contents")) is not None: - return contents or "null" - return _RenderUtils.render_tag(component) - - @staticmethod - def render_tag(component: Mapping[str, Any]) -> str: - name = component.get("name") or "Fragment" - props = f"{{{','.join(component['props'])}}}" - rendered_children = [ - _RenderUtils.render(child) - for child in component.get("children", []) - if child - ] - - return f"jsx({name},{props},{','.join(rendered_children)})" - - @staticmethod - def render_condition_tag(component: Any) -> str: - return f"({component['cond_state']}?({_RenderUtils.render(component['true_value'])}):({_RenderUtils.render(component['false_value'])}))" - - @staticmethod - def render_iterable_tag(component: Any) -> str: - children_rendered = "".join([ - _RenderUtils.render(child) for child in component.get("children", []) - ]) - return f"Array.prototype.map.call({component['iterable_state']} ?? [],(({component['arg_name']},{component['arg_index']})=>({children_rendered})))" - - @staticmethod - def render_match_tag(component: Any) -> str: - cases_code = "" - for conditions, return_value in component["match_cases"]: - for condition in conditions: - cases_code += f" case JSON.stringify({condition}):\n" - cases_code += f""" return {_RenderUtils.render(return_value)}; - break; -""" - - return f"""(() => {{ - switch (JSON.stringify({component["cond"]})) {{ -{cases_code} default: - return {_RenderUtils.render(component["default"])}; - break; - }} -}})()""" - - @staticmethod - def get_import(module: _ImportDict) -> str: - default_import = module["default"] - rest_imports = module["rest"] - - if default_import and rest_imports: - rest_imports_str = ",".join(sorted(rest_imports)) - return f'import {default_import}, {{{rest_imports_str}}} from "{module["lib"]}"' - if default_import: - return f'import {default_import} from "{module["lib"]}"' - if rest_imports: - rest_imports_str = ",".join(sorted(rest_imports)) - return f'import {{{rest_imports_str}}} from "{module["lib"]}"' - return f'import "{module["lib"]}"' - - -def rxconfig_template(app_name: str): - """Template for the Reflex config file. - - Args: - app_name: The name of the application. - - Returns: - Rendered Reflex config file content as string. - """ - return f"""import reflex as rx - -config = rx.Config( - app_name="{app_name}", - plugins=[ - rx.plugins.SitemapPlugin(), - rx.plugins.TailwindV4Plugin(), - ] -)""" - - -def document_root_template(*, imports: list[_ImportDict], document: dict[str, Any]): - """Template for the document root. - - Args: - imports: List of import statements. - document: Document root component. - - Returns: - Rendered document root code as string. - """ - imports_rendered = "\n".join([_RenderUtils.get_import(mod) for mod in imports]) - return f"""{imports_rendered} - -export function Layout({{children}}) {{ - return ( - {_RenderUtils.render(document)} - ) -}}""" - - -def app_root_template( - *, - imports: list[_ImportDict], - custom_codes: Iterable[str], - hooks: dict[str, VarData | None], - window_libraries: list[tuple[str, str]], - render: dict[str, Any], - dynamic_imports: set[str], -): - """Template for the App root. - - Args: - imports: The list of import statements. - custom_codes: The set of custom code snippets. - hooks: The dictionary of hooks. - window_libraries: The list of window libraries. - render: The dictionary of render functions. - dynamic_imports: The set of dynamic imports. - - Returns: - Rendered App root component as string. - """ - imports_str = "\n".join([_RenderUtils.get_import(mod) for mod in imports]) - dynamic_imports_str = "\n".join(dynamic_imports) - - custom_code_str = "\n".join(custom_codes) - - import_window_libraries = "\n".join([ - f'import * as {lib_alias} from "{lib_path}";' - for lib_alias, lib_path in window_libraries - ]) - - window_imports_str = "\n".join([ - f' "{lib_path}": {lib_alias},' for lib_alias, lib_path in window_libraries - ]) - - return f""" -{imports_str} -{dynamic_imports_str} -import {{ EventLoopProvider, StateProvider, defaultColorMode }} from "$/utils/context"; -import {{ ThemeProvider }} from '$/utils/react-theme'; -import {{ Layout as AppLayout }} from './_document'; -import {{ Outlet }} from 'react-router'; -{import_window_libraries} - -{custom_code_str} - -function AppWrap({{children}}) {{ -{_render_hooks(hooks)} -return ({_RenderUtils.render(render)}) -}} - - -export function Layout({{children}}) {{ - useEffect(() => {{ - // Make contexts and state objects available globally for dynamic eval'd components - let windowImports = {{ - {window_imports_str} - }}; - window["__reflex"] = windowImports; - }}, []); - - return jsx(AppLayout, {{}}, - jsx(ThemeProvider, {{defaultTheme: defaultColorMode, attribute: "class"}}, - jsx(StateProvider, {{}}, - jsx(EventLoopProvider, {{}}, - jsx(AppWrap, {{}}, children) - ) - ) - ) - ); -}} - -export default function App() {{ - return jsx(Outlet, {{}}); -}} - -""" - - -def theme_template(theme: str): - """Template for the theme file. - - Args: - theme: The theme to render. - - Returns: - Rendered theme file content as string. - """ - return f"""export default {theme}""" - - -def context_template( - *, - is_dev_mode: bool, - default_color_mode: str, - initial_state: dict[str, Any] | None = None, - state_name: str | None = None, - client_storage: dict[str, dict[str, dict[str, Any]]] | None = None, -): - """Template for the context file. - - Args: - initial_state: The initial state for the context. - state_name: The name of the state. - client_storage: The client storage for the context. - is_dev_mode: Whether the app is in development mode. - default_color_mode: The default color mode for the context. - - Returns: - Rendered context file content as string. - """ - initial_state = initial_state or {} - state_contexts_str = "".join([ - f"{format_state_name(state_name)}: createContext(null)," - for state_name in initial_state - ]) - - state_str = ( - rf""" -export const state_name = "{state_name}" - -export const exception_state_name = "{constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL}" - -// These events are triggered on initial load and each page navigation. -export const onLoadInternalEvent = () => {{ - const internal_events = []; - - // Get tracked cookie and local storage vars to send to the backend. - const client_storage_vars = hydrateClientStorage(clientStorage); - // But only send the vars if any are actually set in the browser. - if (client_storage_vars && Object.keys(client_storage_vars).length !== 0) {{ - internal_events.push( - ReflexEvent( - '{state_name}.{constants.CompileVars.UPDATE_VARS_INTERNAL}', - {{vars: client_storage_vars}}, - ), - ); - }} - - // `on_load_internal` triggers the correct on_load event(s) for the current page. - // If the page does not define any on_load event, this will just set `is_hydrated = true`. - internal_events.push(ReflexEvent('{state_name}.{constants.CompileVars.ON_LOAD_INTERNAL}')); - - return internal_events; -}} - -// The following events are sent when the websocket connects or reconnects. -export const initialEvents = () => [ - ReflexEvent('{state_name}.{constants.CompileVars.HYDRATE}'), - ...onLoadInternalEvent() -] - """ - if state_name - else """ -export const state_name = undefined - -export const exception_state_name = undefined - -export const onLoadInternalEvent = () => [] - -export const initialEvents = () => [] -""" - ) - - state_reducer_str = "\n".join( - rf'const [{format_state_name(state_name)}, dispatch_{format_state_name(state_name)}] = useReducer(applyDelta, initialState["{state_name}"])' - for state_name in initial_state - ) - - create_state_contexts_str = "\n".join( - rf"createElement(StateContexts.{format_state_name(state_name)},{{value: {format_state_name(state_name)}}}," - for state_name in initial_state - ) - - dispatchers_str = "\n".join( - f'"{state_name}": dispatch_{format_state_name(state_name)},' - for state_name in initial_state - ) - - return rf"""import {{ createContext, useContext, useMemo, useReducer, useState, createElement, useEffect }} from "react" -import {{ applyDelta, ReflexEvent, hydrateClientStorage, useEventLoop, refs }} from "$/utils/state" -import {{ jsx }} from "@emotion/react"; - -export const initialState = {"{}" if not initial_state else json_dumps(initial_state)} - -export const defaultColorMode = {default_color_mode} -export const ColorModeContext = createContext(null); -export const UploadFilesContext = createContext(null); -export const DispatchContext = createContext(null); -export const StateContexts = {{{state_contexts_str}}}; -export const EventLoopContext = createContext(null); -export const clientStorage = {"{}" if client_storage is None else json.dumps(client_storage)} - -{state_str} - -export const isDevMode = {json.dumps(is_dev_mode)}; - -export function UploadFilesProvider({{ children }}) {{ - const [filesById, setFilesById] = useState({{}}) - refs["__clear_selected_files"] = (id) => setFilesById(filesById => {{ - const newFilesById = {{...filesById}} - delete newFilesById[id] - return newFilesById - }}) - return createElement( - UploadFilesContext.Provider, - {{ value: [filesById, setFilesById] }}, - children - ); -}} - -export function ClientSide(component) {{ - return ({{ children, ...props }}) => {{ - const [Component, setComponent] = useState(null); - useEffect(() => {{ - async function load() {{ - const comp = await component(); - setComponent(() => comp); - }} - load(); - }}, []); - return Component ? jsx(Component, props, children) : null; - }}; -}} - -export function EventLoopProvider({{ children }}) {{ - const dispatch = useContext(DispatchContext) - const [addEvents, connectErrors] = useEventLoop( - dispatch, - initialEvents, - clientStorage, - ) - return createElement( - EventLoopContext.Provider, - {{ value: [addEvents, connectErrors] }}, - children - ); -}} - -export function StateProvider({{ children }}) {{ - {state_reducer_str} - const dispatchers = useMemo(() => {{ - return {{ - {dispatchers_str} - }} - }}, []) - - return ( - {create_state_contexts_str} - createElement(DispatchContext, {{value: dispatchers}}, children) - {")" * len(initial_state)} - ) -}}""" - - -def component_template(component: Component | StatefulComponent): - """Template to render a component tag. - - Args: - component: The component to render. - - Returns: - Rendered component as string. - """ - return _RenderUtils.render(component.render()) - - -def page_template( - imports: Iterable[_ImportDict], - dynamic_imports: Iterable[str], - custom_codes: Iterable[str], - hooks: dict[str, VarData | None], - render: dict[str, Any], -): - """Template for a single react page. - - Args: - imports: List of import statements. - dynamic_imports: List of dynamic import statements. - custom_codes: List of custom code snippets. - hooks: Dictionary of hooks. - render: Render function for the component. - - Returns: - Rendered React page component as string. - """ - imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports]) - custom_code_str = "\n".join(custom_codes) - dynamic_imports_str = "\n".join(dynamic_imports) - - hooks_str = _render_hooks(hooks) - return f"""{imports_str} - -{dynamic_imports_str} - -{custom_code_str} - -export default function Component() {{ -{hooks_str} - - return ( - {_RenderUtils.render(render)} - ) -}}""" - - -def package_json_template( - scripts: dict[str, str], - dependencies: dict[str, str], - dev_dependencies: dict[str, str], - overrides: dict[str, str], -): - """Template for package.json. - - Args: - scripts: The scripts to include in the package.json file. - dependencies: The dependencies to include in the package.json file. - dev_dependencies: The devDependencies to include in the package.json file. - overrides: The overrides to include in the package.json file. - - Returns: - Rendered package.json content as string. - """ - return json.dumps({ - "name": "reflex", - "type": "module", - "scripts": scripts, - "dependencies": dependencies, - "devDependencies": dev_dependencies, - "overrides": overrides, - }) - - -def vite_config_template( - base: str, - hmr: bool, - force_full_reload: bool, - experimental_hmr: bool, - sourcemap: bool | Literal["inline", "hidden"], - allowed_hosts: bool | list[str] = False, -): - """Template for vite.config.js. - - Args: - base: The base path for the Vite config. - hmr: Whether to enable hot module replacement. - force_full_reload: Whether to force a full reload on changes. - experimental_hmr: Whether to enable experimental HMR features. - sourcemap: The sourcemap configuration. - allowed_hosts: Allow all hosts (True), specific hosts (list of strings), or only localhost (False). - - Returns: - Rendered vite.config.js content as string. - """ - if allowed_hosts is True: - allowed_hosts_line = "\n allowedHosts: true," - elif isinstance(allowed_hosts, list) and allowed_hosts: - allowed_hosts_line = f"\n allowedHosts: {json.dumps(allowed_hosts)}," - else: - allowed_hosts_line = "" - return rf"""import {{ fileURLToPath, URL }} from "url"; -import {{ reactRouter }} from "@react-router/dev/vite"; -import {{ defineConfig }} from "vite"; -import safariCacheBustPlugin from "./vite-plugin-safari-cachebust"; - -// Ensure that bun always uses the react-dom/server.node functions. -function alwaysUseReactDomServerNode() {{ - return {{ - name: "vite-plugin-always-use-react-dom-server-node", - enforce: "pre", - - resolveId(source, importer) {{ - if ( - typeof importer === "string" && - importer.endsWith("/entry.server.node.tsx") && - source.includes("react-dom/server") - ) {{ - return this.resolve("react-dom/server.node", importer, {{ - skipSelf: true, - }}); - }} - return null; - }}, - }}; -}} - -function fullReload() {{ - return {{ - name: "full-reload", - enforce: "pre", - handleHotUpdate({{ server }}) {{ - server.ws.send({{ - type: "full-reload", - }}); - return []; - }} - }}; -}} - -export default defineConfig((config) => ({{ - plugins: [ - alwaysUseReactDomServerNode(), - reactRouter(), - safariCacheBustPlugin(), - ].concat({"[fullReload()]" if force_full_reload else "[]"}), - build: {{ - assetsDir: "{base}assets".slice(1), - sourcemap: {"true" if sourcemap is True else "false" if sourcemap is False else repr(sourcemap)}, - rollupOptions: {{ - onwarn(warning, warn) {{ - if (warning.code === "EVAL" && warning.id && warning.id.endsWith("state.js")) return; - warn(warning); - }}, - jsx: {{}}, - output: {{ - advancedChunks: {{ - groups: [ - {{ - test: /env.json/, - name: "reflex-env", - }}, - ], - }}, - }}, - }}, - }}, - experimental: {{ - enableNativePlugin: false, - hmr: {"true" if experimental_hmr else "false"}, - }}, - server: {{ - port: process.env.PORT,{allowed_hosts_line} - hmr: {"true" if hmr else "false"}, - watch: {{ - ignored: [ - "**/.web/backend/**", - "**/.web/reflex.install_frontend_packages.cached", - ], - }}, - }}, - resolve: {{ - mainFields: ["browser", "module", "jsnext"], - alias: [ - {{ - find: "$", - replacement: fileURLToPath(new URL("./", import.meta.url)), - }}, - {{ - find: "@", - replacement: fileURLToPath(new URL("./public", import.meta.url)), - }}, - ], - }}, -}}));""" - - -def stateful_component_template( - tag_name: str, memo_trigger_hooks: list[str], component: Component, export: bool -): - """Template for stateful component. - - Args: - tag_name: The tag name for the component. - memo_trigger_hooks: The memo trigger hooks for the component. - component: The component to render. - export: Whether to export the component. - - Returns: - Rendered stateful component code as string. - """ - all_hooks = component._get_all_hooks() - return f""" -{"export " if export else ""}function {tag_name} () {{ - {_render_hooks(all_hooks, memo_trigger_hooks)} - return ( - {_RenderUtils.render(component.render())} - ) -}} -""" - - -def stateful_components_template(imports: list[_ImportDict], memoized_code: str) -> str: - """Template for stateful components. - - Args: - imports: List of import statements. - memoized_code: Memoized code for stateful components. - - Returns: - Rendered stateful components code as string. - """ - imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports]) - return f"{imports_str}\n{memoized_code}" - - -def memo_components_template( - imports: list[_ImportDict], - components: list[dict[str, Any]], - functions: list[dict[str, Any]], - dynamic_imports: Iterable[str], - custom_codes: Iterable[str], -) -> str: - """Template for custom component. - - Args: - imports: List of import statements. - components: List of component definitions. - functions: List of function definitions. - dynamic_imports: List of dynamic import statements. - custom_codes: List of custom code snippets. - - Returns: - Rendered custom component code as string. - """ - imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports]) - dynamic_imports_str = "\n".join(dynamic_imports) - custom_code_str = "\n".join(custom_codes) - - components_code = "" - for component in components: - components_code += f""" -export const {component["name"]} = memo(({component["signature"]}) => {{ - {_render_hooks(component.get("hooks", {}))} - return( - {_RenderUtils.render(component["render"])} - ) -}}); -""" - - functions_code = "" - for function in functions: - functions_code += ( - f"\nexport const {function['name']} = {function['function']};\n" - ) - - return f""" -{imports_str} - -{dynamic_imports_str} - -{custom_code_str} - -{functions_code} - -{components_code}""" - - -def styles_template(stylesheets: list[str]) -> str: - """Template for styles.css. - - Args: - stylesheets: List of stylesheets to include. - - Returns: - Rendered styles.css content as string. - """ - return "@layer __reflex_base;\n" + "\n".join([ - f"@import url('{sheet_name}');" for sheet_name in stylesheets - ]) - - -def _render_hooks(hooks: dict[str, VarData | None], memo: list | None = None) -> str: - """Render hooks for macros. - - Args: - hooks: Dictionary of hooks to render. - memo: Optional list of memo hooks. - - Returns: - Rendered hooks code as string. - """ - internal, pre_trigger, post_trigger = _sort_hooks(hooks) - internal_str = "\n".join(internal) - pre_trigger_str = "\n".join(pre_trigger) - post_trigger_str = "\n".join(post_trigger) - memo_str = "\n".join(memo) if memo is not None else "" - return f"{internal_str}\n{pre_trigger_str}\n{memo_str}\n{post_trigger_str}" +from reflex_core.compiler.templates import * diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index cd3d2f6ce2b..fd56b2e079a 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -13,27 +13,29 @@ from typing import Any, TypedDict from urllib.parse import urlparse -from reflex import constants -from reflex.components.base import Description, Image, Scripts -from reflex.components.base.document import Links, ScrollRestoration -from reflex.components.base.document import Meta as ReactMeta -from reflex.components.component import Component, ComponentStyle, CustomComponent -from reflex.components.el.elements.metadata import Head, Link, Meta, Title -from reflex.components.el.elements.other import Html -from reflex.components.el.elements.sectioning import Body -from reflex.constants.state import CAMEL_CASE_MEMO_MARKER, FIELD_MARKER +from reflex_components_core.base import Description, Image, Scripts +from reflex_components_core.base.document import Links, ScrollRestoration +from reflex_components_core.base.document import Meta as ReactMeta +from reflex_components_core.el.elements.metadata import Head, Link, Meta, Title +from reflex_components_core.el.elements.other import Html +from reflex_components_core.el.elements.sectioning import Body +from reflex_core import constants +from reflex_core.components.component import Component, ComponentStyle, CustomComponent +from reflex_core.constants.state import CAMEL_CASE_MEMO_MARKER, FIELD_MARKER +from reflex_core.style import Style +from reflex_core.utils import format, imports +from reflex_core.utils.imports import ImportVar, ParsedImportDict +from reflex_core.vars.base import Field, Var, VarData +from reflex_core.vars.function import DestructuredArg + from reflex.experimental.memo import ( ExperimentalMemoComponentDefinition, ExperimentalMemoFunctionDefinition, ) from reflex.istate.storage import Cookie, LocalStorage, SessionStorage from reflex.state import BaseState, _resolve_delta -from reflex.style import Style -from reflex.utils import format, imports, path_ops -from reflex.utils.imports import ImportVar, ParsedImportDict +from reflex.utils import path_ops from reflex.utils.prerequisites import get_web_dir -from reflex.vars.base import Field, Var, VarData -from reflex.vars.function import DestructuredArg # To re-export this function. merge_imports = imports.merge_imports diff --git a/reflex/components/__init__.py b/reflex/components/__init__.py index 36364a3b851..741709b0463 100644 --- a/reflex/components/__init__.py +++ b/reflex/components/__init__.py @@ -1,34 +1,145 @@ -"""Import all the components.""" +"""Import all the components. + +Components have been split across multiple packages. +This module installs an import redirect hook so that +``from reflex_core.components. import X`` continues to work +by delegating to the appropriate package. +""" from __future__ import annotations -from reflex.utils import lazy_loader - -_SUBMODULES: set[str] = { - "lucide", - "core", - "datadisplay", - "gridjs", - "markdown", - "moment", - "plotly", - "radix", - "react_player", - "react_router", - "sonner", - "el", - "base", - "recharts", +import importlib +import importlib.abc +import importlib.machinery +import sys +from types import ModuleType + +from reflex_core.utils import lazy_loader + +# Mapping from subpackage name to the target top-level package. +_SUBPACKAGE_TARGETS: dict[str, str] = { + # reflex-components (base package) + "base": "reflex_components_core.base", + "core": "reflex_components_core.core", + "datadisplay": "reflex_components_core.datadisplay", + "el": "reflex_components_core.el", + # Standalone packages + "gridjs": "reflex_components_gridjs", + "lucide": "reflex_components_lucide", + "markdown": "reflex_components_markdown", + "moment": "reflex_components_moment", + "plotly": "reflex_components_plotly", + "radix": "reflex_components_radix", + "react_player": "reflex_components_react_player", + "react_router": "reflex_components_core.react_router", + "recharts": "reflex_components_recharts", + "sonner": "reflex_components_sonner", } +# Deeper overrides for subpackages that were split from datadisplay. +# Checked before the general _SUBPACKAGE_TARGETS mapping. +_DEEP_OVERRIDES: dict[str, str] = { + "datadisplay.code": "reflex_components_code.code", + "datadisplay.shiki_code_block": "reflex_components_code.shiki_code_block", + "datadisplay.dataeditor": "reflex_components_dataeditor.dataeditor", +} + + +class _AliasLoader(importlib.abc.Loader): + """Loader that aliases one module name to another.""" + + def __init__(self, target_name: str): + self.target_name = target_name + + def create_module(self, spec: importlib.machinery.ModuleSpec) -> ModuleType | None: + return None + + def exec_module(self, module: ModuleType) -> None: + target = importlib.import_module(self.target_name) + # Make the alias point to the real module. + module.__dict__.update(target.__dict__) + module.__path__ = getattr(target, "__path__", []) + module.__file__ = getattr(target, "__file__", None) + module.__loader__ = self + # Register the target module under the alias name so subsequent + # imports resolve immediately. + sys.modules[module.__name__] = target + + +class _ComponentsRedirect(importlib.abc.MetaPathFinder): + """Import hook: redirects ``reflex.components.`` to the real package.""" + + def find_spec( + self, + fullname: str, + path: object = None, + target: object = None, + ) -> importlib.machinery.ModuleSpec | None: + parts = fullname.split(".") + if len(parts) >= 3 and parts[0] == "reflex" and parts[1] == "components": + subpkg = parts[2] + rest_parts = parts[3:] + + # Check deep overrides first (e.g. datadisplay.code -> reflex_components_code.code). + if rest_parts: + deep_key = f"{subpkg}.{rest_parts[0]}" + override = _DEEP_OVERRIDES.get(deep_key) + if override is not None: + extra = ".".join(rest_parts[1:]) + target_name = f"{override}.{extra}" if extra else override + return importlib.machinery.ModuleSpec( + fullname, + _AliasLoader(target_name), + is_package=True, + ) + + # General subpackage mapping. + if subpkg in _SUBPACKAGE_TARGETS: + base = _SUBPACKAGE_TARGETS[subpkg] + rest = ".".join(rest_parts) + target_name = f"{base}.{rest}" if rest else base + return importlib.machinery.ModuleSpec( + fullname, + _AliasLoader(target_name), + is_package=True, + ) + return None + + +# Install the import redirect hook. +if not any(isinstance(f, _ComponentsRedirect) for f in sys.meta_path): + sys.meta_path.insert(0, _ComponentsRedirect()) + + +# Submodules that still live in reflex.components (infrastructure). _SUBMOD_ATTRS: dict[str, list[str]] = { "component": [ "Component", "NoSSRComponent", ], } -__getattr__, __dir__, __all__ = lazy_loader.attach( + +_lazy_getattr, __dir__, __all__ = lazy_loader.attach( __name__, - submodules=_SUBMODULES, + submodules=set(), submod_attrs=_SUBMOD_ATTRS, ) + + +def __getattr__(name: str) -> object: + """Resolve attributes: first try local lazy loader, then delegate to component packages. + + Returns: + The requested attribute from this module or a component package. + + Raises: + AttributeError: If the attribute is not found. + """ + try: + return _lazy_getattr(name) + except AttributeError: + pass + if name in _SUBPACKAGE_TARGETS: + return importlib.import_module(_SUBPACKAGE_TARGETS[name]) + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) diff --git a/reflex/components/component.py b/reflex/components/component.py index fbdc1decfda..5a82e9ec3eb 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -1,3016 +1,3 @@ -"""Base component definitions.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import contextlib -import copy -import dataclasses -import enum -import functools -import inspect -import typing -from abc import ABC, ABCMeta, abstractmethod -from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence -from dataclasses import _MISSING_TYPE, MISSING -from functools import wraps -from hashlib import md5 -from types import SimpleNamespace -from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast, get_args, get_origin - -from rich.markup import escape -from typing_extensions import dataclass_transform - -import reflex.state -from reflex import constants -from reflex.compiler.templates import stateful_component_template -from reflex.components.core.breakpoints import Breakpoints -from reflex.components.dynamic import load_dynamic_serializer -from reflex.components.field import BaseField, FieldBasedMeta -from reflex.components.tags import Tag -from reflex.constants import ( - Dirs, - EventTriggers, - Hooks, - Imports, - MemoizationDisposition, - MemoizationMode, - PageNames, -) -from reflex.constants.compiler import SpecialAttributes -from reflex.constants.state import CAMEL_CASE_MEMO_MARKER, FRONTEND_EVENT_STATE -from reflex.event import ( - EventCallback, - EventChain, - EventHandler, - EventSpec, - args_specs_from_fields, - no_args_event_spec, - parse_args_spec, - pointer_event_spec, - run_script, - unwrap_var_annotation, -) -from reflex.style import Style, format_as_emotion -from reflex.utils import console, format, imports, types -from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict -from reflex.vars import VarData -from reflex.vars.base import ( - CachedVarOperation, - LiteralNoneVar, - LiteralVar, - Var, - cached_property_no_lock, -) -from reflex.vars.function import ArgsFunctionOperation, FunctionStringVar, FunctionVar -from reflex.vars.number import ternary_operation -from reflex.vars.object import ObjectVar -from reflex.vars.sequence import LiteralArrayVar, LiteralStringVar, StringVar - -FIELD_TYPE = TypeVar("FIELD_TYPE") - - -class ComponentField(BaseField[FIELD_TYPE]): - """A field for a component.""" - - def __init__( - self, - default: FIELD_TYPE | _MISSING_TYPE = MISSING, - default_factory: Callable[[], FIELD_TYPE] | None = None, - is_javascript: bool | None = None, - annotated_type: type[Any] | _MISSING_TYPE = MISSING, - doc: str | None = None, - ) -> None: - """Initialize the field. - - Args: - default: The default value for the field. - default_factory: The default factory for the field. - is_javascript: Whether the field is a javascript property. - annotated_type: The annotated type for the field. - doc: Documentation string for the field. - """ - super().__init__(default, default_factory, annotated_type) - self.doc = doc - self.is_javascript = is_javascript - - def __repr__(self) -> str: - """Represent the field in a readable format. - - Returns: - The string representation of the field. - """ - annotated_type_str = ( - f", annotated_type={self.annotated_type!r}" - if self.annotated_type is not MISSING - else "" - ) - if self.default is not MISSING: - return f"ComponentField(default={self.default!r}, is_javascript={self.is_javascript!r}{annotated_type_str})" - return f"ComponentField(default_factory={self.default_factory!r}, is_javascript={self.is_javascript!r}{annotated_type_str})" - - -def field( - default: FIELD_TYPE | _MISSING_TYPE = MISSING, - default_factory: Callable[[], FIELD_TYPE] | None = None, - is_javascript_property: bool | None = None, - doc: str | None = None, -) -> FIELD_TYPE: - """Create a field for a component. - - Args: - default: The default value for the field. - default_factory: The default factory for the field. - is_javascript_property: Whether the field is a javascript property. - doc: Documentation string for the field. - - Returns: - The field for the component. - - Raises: - ValueError: If both default and default_factory are specified. - """ - if default is not MISSING and default_factory is not None: - msg = "cannot specify both default and default_factory" - raise ValueError(msg) - return ComponentField( # pyright: ignore [reportReturnType] - default=default, - default_factory=default_factory, - is_javascript=is_javascript_property, - doc=doc, - ) - - -@dataclass_transform(kw_only_default=True, field_specifiers=(field,)) -class BaseComponentMeta(FieldBasedMeta, ABCMeta): - """Meta class for BaseComponent.""" - - if TYPE_CHECKING: - _inherited_fields: Mapping[str, ComponentField] - _own_fields: Mapping[str, ComponentField] - _fields: Mapping[str, ComponentField] - _js_fields: Mapping[str, ComponentField] - - @classmethod - def _process_annotated_fields( - cls, - namespace: dict[str, Any], - annotations: dict[str, Any], - inherited_fields: dict[str, ComponentField], - ) -> dict[str, ComponentField]: - own_fields: dict[str, ComponentField] = {} - - for key, annotation in annotations.items(): - value = namespace.get(key, MISSING) - - if types.is_classvar(annotation): - # If the annotation is a classvar, skip it. - continue - - if value is MISSING: - value = ComponentField( - default=None, - is_javascript=(key[0] != "_"), - annotated_type=annotation, - ) - elif not isinstance(value, ComponentField): - value = ComponentField( - default=value, - is_javascript=( - (key[0] != "_") - if (existing_field := inherited_fields.get(key)) is None - else existing_field.is_javascript - ), - annotated_type=annotation, - ) - else: - is_js = value.is_javascript - if is_js is None: - if (existing_field := inherited_fields.get(key)) is not None: - is_js = existing_field.is_javascript - else: - is_js = key[0] != "_" - default = value.default - # If no default or factory provided, default to None - # (same behavior as bare annotations without field()) - if default is MISSING and value.default_factory is None: - default = None - value = ComponentField( - default=default, - default_factory=value.default_factory, - is_javascript=is_js, - annotated_type=annotation, - doc=value.doc, - ) - - own_fields[key] = value - - return own_fields - - @classmethod - def _create_field( - cls, - annotated_type: Any, - default: Any = MISSING, - default_factory: Callable[[], Any] | None = None, - ) -> ComponentField: - return ComponentField( - annotated_type=annotated_type, - default=default, - default_factory=default_factory, - is_javascript=True, # Default for components - ) - - @classmethod - def _process_field_overrides( - cls, - namespace: dict[str, Any], - annotations: dict[str, Any], - inherited_fields: dict[str, Any], - ) -> dict[str, ComponentField]: - own_fields: dict[str, ComponentField] = {} - - for key, value, inherited_field in [ - (key, value, inherited_field) - for key, value in namespace.items() - if key not in annotations - and ((inherited_field := inherited_fields.get(key)) is not None) - ]: - new_field = ComponentField( - default=value, - is_javascript=inherited_field.is_javascript, - annotated_type=inherited_field.annotated_type, - ) - own_fields[key] = new_field - - return own_fields - - @classmethod - def _finalize_fields( - cls, - namespace: dict[str, Any], - inherited_fields: dict[str, ComponentField], - own_fields: dict[str, ComponentField], - ) -> None: - # Call parent implementation - super()._finalize_fields(namespace, inherited_fields, own_fields) - - # Add JavaScript fields mapping - all_fields = namespace["_fields"] - namespace["_js_fields"] = { - key: value - for key, value in all_fields.items() - if value.is_javascript is True - } - - -class BaseComponent(metaclass=BaseComponentMeta): - """The base class for all Reflex components. - - This is something that can be rendered as a Component via the Reflex compiler. - """ - - children: list[BaseComponent] = field( - doc="The children nested within the component.", - default_factory=list, - is_javascript_property=False, - ) - - # The library that the component is based on. - library: str | None = field(default=None, is_javascript_property=False) - - lib_dependencies: list[str] = field( - doc="List here the non-react dependency needed by `library`", - default_factory=list, - is_javascript_property=False, - ) - - # The tag to use when rendering the component. - tag: str | None = field(default=None, is_javascript_property=False) - - def __init__( - self, - **kwargs, - ): - """Initialize the component. - - Args: - **kwargs: The kwargs to pass to the component. - """ - for key, value in kwargs.items(): - setattr(self, key, value) - for name, value in self.get_fields().items(): - if name not in kwargs: - setattr(self, name, value.default_value()) - - def set(self, **kwargs): - """Set the component props. - - Args: - **kwargs: The kwargs to set. - - Returns: - The component with the updated props. - """ - for key, value in kwargs.items(): - setattr(self, key, value) - return self - - def __eq__(self, value: Any) -> bool: - """Check if the component is equal to another value. - - Args: - value: The value to compare to. - - Returns: - Whether the component is equal to the value. - """ - return type(self) is type(value) and bool( - getattr(self, key) == getattr(value, key) for key in self.get_fields() - ) - - @classmethod - def get_fields(cls) -> Mapping[str, ComponentField]: - """Get the fields of the component. - - Returns: - The fields of the component. - """ - return cls._fields - - @classmethod - def get_js_fields(cls) -> Mapping[str, ComponentField]: - """Get the javascript fields of the component. - - Returns: - The javascript fields of the component. - """ - return cls._js_fields - - @abstractmethod - def render(self) -> dict: - """Render the component. - - Returns: - The dictionary for template of the component. - """ - - @abstractmethod - def _get_all_hooks_internal(self) -> dict[str, VarData | None]: - """Get the reflex internal hooks for the component and its children. - - Returns: - The code that should appear just before user-defined hooks. - """ - - @abstractmethod - def _get_all_hooks(self) -> dict[str, VarData | None]: - """Get the React hooks for this component. - - Returns: - The code that should appear just before returning the rendered component. - """ - - @abstractmethod - def _get_all_imports(self) -> ParsedImportDict: - """Get all the libraries and fields that are used by the component. - - Returns: - The import dict with the required imports. - """ - - @abstractmethod - def _get_all_dynamic_imports(self) -> set[str]: - """Get dynamic imports for the component. - - Returns: - The dynamic imports. - """ - - @abstractmethod - def _get_all_custom_code(self) -> dict[str, None]: - """Get custom code for the component. - - Returns: - The custom code. - """ - - @abstractmethod - def _get_all_refs(self) -> dict[str, None]: - """Get the refs for the children of the component. - - Returns: - The refs for the children. - """ - - -class ComponentNamespace(SimpleNamespace): - """A namespace to manage components with subcomponents.""" - - def __hash__(self) -> int: # pyright: ignore [reportIncompatibleVariableOverride] - """Get the hash of the namespace. - - Returns: - The hash of the namespace. - """ - return hash(type(self).__name__) - - -def evaluate_style_namespaces(style: ComponentStyle) -> dict: - """Evaluate namespaces in the style. - - Args: - style: The style to evaluate. - - Returns: - The evaluated style. - """ - return { - k.__call__ if isinstance(k, ComponentNamespace) else k: v - for k, v in style.items() - } - - -# Map from component to styling. -ComponentStyle = dict[str | type[BaseComponent] | Callable | ComponentNamespace, Any] -ComponentChildTypes = (*types.PrimitiveTypes, Var, BaseComponent, type(None)) - - -def _satisfies_type_hint(obj: Any, type_hint: Any) -> bool: - return types._isinstance( - obj, - type_hint, - nested=1, - treat_var_as_type=True, - treat_mutable_obj_as_immutable=( - isinstance(obj, Var) and not isinstance(obj, LiteralVar) - ), - ) - - -def satisfies_type_hint(obj: Any, type_hint: Any) -> bool: - """Check if an object satisfies a type hint. - - Args: - obj: The object to check. - type_hint: The type hint to check against. - - Returns: - Whether the object satisfies the type hint. - """ - if _satisfies_type_hint(obj, type_hint): - return True - if _satisfies_type_hint(obj, type_hint | None): - obj = ( - obj - if not isinstance(obj, Var) - else (obj._var_value if isinstance(obj, LiteralVar) else obj) - ) - console.warn( - "Passing None to a Var that is not explicitly marked as Optional (| None) is deprecated. " - f"Passed {obj!s} of type {escape(str(type(obj) if not isinstance(obj, Var) else obj._var_type))} to {escape(str(type_hint))}." - ) - return True - return False - - -def _components_from( - component_or_var: BaseComponent | Var, -) -> tuple[BaseComponent, ...]: - """Get the components from a component or Var. - - Args: - component_or_var: The component or Var to get the components from. - - Returns: - The components. - """ - if isinstance(component_or_var, Var): - var_data = component_or_var._get_all_var_data() - return var_data.components if var_data else () - if isinstance(component_or_var, BaseComponent): - return (component_or_var,) - return () - - -def _hash_str(value: str) -> str: - return md5(f'"{value}"'.encode(), usedforsecurity=False).hexdigest() - - -def _hash_sequence(value: Sequence) -> str: - return _hash_str(str([_deterministic_hash(v) for v in value])) - - -def _hash_dict(value: dict) -> str: - return _hash_sequence( - sorted([(k, _deterministic_hash(v)) for k, v in value.items()]) - ) - - -def _deterministic_hash(value: object) -> str: - """Hash a rendered dictionary. - - Args: - value: The dictionary to hash. - - Returns: - The hash of the dictionary. - - Raises: - TypeError: If the value is not hashable. - """ - if value is None: - # Hash None as a special case. - return "None" - if isinstance(value, (int, float, enum.Enum)): - # Hash numbers and booleans directly. - return str(value) - if isinstance(value, str): - return _hash_str(value) - if isinstance(value, dict): - return _hash_dict(value) - if isinstance(value, (tuple, list)): - # Hash tuples by hashing each element. - return _hash_sequence(value) - if isinstance(value, Var): - return _hash_str( - str((value._js_expr, _deterministic_hash(value._get_all_var_data()))) - ) - if dataclasses.is_dataclass(value): - return _hash_dict({ - k.name: getattr(value, k.name) for k in dataclasses.fields(value) - }) - if isinstance(value, BaseComponent): - # If the value is a component, hash its rendered code. - return _hash_dict(value.render()) - - msg = ( - f"Cannot hash value `{value}` of type `{type(value).__name__}`. " - "Only BaseComponent, Var, VarData, dict, str, tuple, and enum.Enum are supported." - ) - raise TypeError(msg) - - -@dataclasses.dataclass(kw_only=True, frozen=True, slots=True) -class TriggerDefinition: - """A default event trigger with its args spec and description.""" - - spec: types.ArgsSpec | Sequence[types.ArgsSpec] - description: str - - -DEFAULT_TRIGGERS_AND_DESC: Mapping[str, TriggerDefinition] = { - EventTriggers.ON_FOCUS: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the element (or some element inside of it) receives focus. For example, it is called when the user clicks on a text input.", - ), - EventTriggers.ON_BLUR: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when focus has left the element (or left some element inside of it). For example, it is called when the user clicks outside of a focused text input.", - ), - EventTriggers.ON_CLICK: TriggerDefinition( - spec=pointer_event_spec, # pyright: ignore [reportArgumentType] - description="Fired when the user clicks on an element. For example, it's called when the user clicks on a button.", - ), - EventTriggers.ON_CONTEXT_MENU: TriggerDefinition( - spec=pointer_event_spec, # pyright: ignore [reportArgumentType] - description="Fired when the user right-clicks on an element.", - ), - EventTriggers.ON_DOUBLE_CLICK: TriggerDefinition( - spec=pointer_event_spec, # pyright: ignore [reportArgumentType] - description="Fired when the user double-clicks on an element.", - ), - EventTriggers.ON_MOUSE_DOWN: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the user presses a mouse button on an element.", - ), - EventTriggers.ON_MOUSE_ENTER: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the mouse pointer enters the element.", - ), - EventTriggers.ON_MOUSE_LEAVE: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the mouse pointer leaves the element.", - ), - EventTriggers.ON_MOUSE_MOVE: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the mouse pointer moves over the element.", - ), - EventTriggers.ON_MOUSE_OUT: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the mouse pointer moves out of the element.", - ), - EventTriggers.ON_MOUSE_OVER: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the mouse pointer moves onto the element.", - ), - EventTriggers.ON_MOUSE_UP: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the user releases a mouse button on an element.", - ), - EventTriggers.ON_SCROLL: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the user scrolls the element.", - ), - EventTriggers.ON_SCROLL_END: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when scrolling ends on the element.", - ), - EventTriggers.ON_MOUNT: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the component is mounted to the page.", - ), - EventTriggers.ON_UNMOUNT: TriggerDefinition( - spec=no_args_event_spec, - description="Fired when the component is removed from the page. Only called during navigation, not on page refresh.", - ), -} -DEFAULT_TRIGGERS = { - name: trigger.spec for name, trigger in DEFAULT_TRIGGERS_AND_DESC.items() -} - -T = TypeVar("T", bound="Component") - - -class Component(BaseComponent, ABC): - """A component with style, event trigger and other props.""" - - style: Style = field( - doc="The style of the component.", - default_factory=Style, - is_javascript_property=False, - ) - - event_triggers: dict[str, EventChain | Var] = field( - doc="A mapping from event triggers to event chains.", - default_factory=dict, - is_javascript_property=False, - ) - - # The alias for the tag. - alias: str | None = field(default=None, is_javascript_property=False) - - # Whether the component is a global scope tag. True for tags like `html`, `head`, `body`. - _is_tag_in_global_scope: ClassVar[bool] = False - - # Whether the import is default or named. - is_default: bool | None = field(default=False, is_javascript_property=False) - - key: Any = field( - doc="A unique key for the component.", - default=None, - is_javascript_property=False, - ) - - id: Any = field( - doc="The id for the component.", default=None, is_javascript_property=False - ) - - ref: Var | None = field( - doc="The Var to pass as the ref to the component.", - default=None, - is_javascript_property=False, - ) - - class_name: Any = field( - doc="The class name for the component.", - default=None, - is_javascript_property=False, - ) - - special_props: list[Var] = field( - doc="Special component props.", - default_factory=list, - is_javascript_property=False, - ) - - # components that cannot be children - _invalid_children: ClassVar[list[str]] = [] - - # only components that are allowed as children - _valid_children: ClassVar[list[str]] = [] - - # only components that are allowed as parent - _valid_parents: ClassVar[list[str]] = [] - - # props to change the name of - _rename_props: ClassVar[dict[str, str]] = {} - - custom_attrs: dict[str, Var | Any] = field( - doc="custom attribute", default_factory=dict, is_javascript_property=False - ) - - _memoization_mode: MemoizationMode = field( - doc="When to memoize this component and its children.", - default_factory=MemoizationMode, - is_javascript_property=False, - ) - - # State class associated with this component instance - State: type[reflex.state.State] | None = field( - default=None, is_javascript_property=False - ) - - def add_imports(self) -> ImportDict | list[ImportDict]: - """Add imports for the component. - - This method should be implemented by subclasses to add new imports for the component. - - Implementations do NOT need to call super(). The result of calling - add_imports in each parent class will be merged internally. - - Returns: - The additional imports for this component subclass. - - The format of the return value is a dictionary where the keys are the - library names (with optional npm-style version specifications) mapping - to a single name to be imported, or a list names to be imported. - - For advanced use cases, the values can be ImportVar instances (for - example, to provide an alias or mark that an import is the default - export from the given library). - - ```python - return { - "react": "useEffect", - "react-draggable": ["DraggableCore", rx.ImportVar(tag="Draggable", is_default=True)], - } - ``` - """ - return {} - - 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 - React component function. - - Each logical hook should be a separate string in the list. - - Common strings will be deduplicated and inserted into the component - function only once, so define const variables and other identical code - in their own strings to avoid defining the same const or hook multiple - times. - - If a hook depends on specific data from the component instance, be sure - to use unique values inside the string to _avoid_ deduplication. - - Implementations do NOT need to call super(). The result of calling - add_hooks in each parent class will be merged and deduplicated internally. - - Returns: - The additional hooks for this component subclass. - - ```python - return [ - "const [count, setCount] = useState(0);", - "useEffect(() => { setCount((prev) => prev + 1); console.log(`mounted ${count} times`); }, []);", - ] - ``` - """ - return [] - - def add_custom_code(self) -> list[str]: - """Add custom Javascript code into the page that contains this component. - - Custom code is inserted at module level, after any imports. - - Each string of custom code is deduplicated per-page, so take care to - avoid defining the same const or function differently from different - component instances. - - Custom code is useful for defining global functions or constants which - can then be referenced inside hooks or used by component vars. - - Implementations do NOT need to call super(). The result of calling - add_custom_code in each parent class will be merged and deduplicated internally. - - Returns: - The additional custom code for this component subclass. - - ```python - return [ - "const translatePoints = (event) => { return { x: event.clientX, y: event.clientY }; };", - ] - ``` - """ - return [] - - @classmethod - def __init_subclass__(cls, **kwargs): - """Set default properties. - - Args: - **kwargs: The kwargs to pass to the superclass. - """ - super().__init_subclass__(**kwargs) - - # Ensure renamed props from parent classes are applied to the subclass. - if cls._rename_props: - inherited_rename_props = {} - for parent in reversed(cls.mro()): - if issubclass(parent, Component) and parent._rename_props: - inherited_rename_props.update(parent._rename_props) - cls._rename_props = inherited_rename_props - - def __init__(self, **kwargs): - """Initialize the custom component. - - Args: - **kwargs: The kwargs to pass to the component. - """ - console.error( - "Instantiating components directly is not supported." - f" Use `{self.__class__.__name__}.create` method instead." - ) - - def _post_init(self, *args, **kwargs): - """Initialize the component. - - Args: - *args: Args to initialize the component. - **kwargs: Kwargs to initialize the component. - - Raises: - TypeError: If an invalid prop is passed. - ValueError: If an event trigger passed is not valid. - """ - # Set the id and children initially. - children = kwargs.get("children", []) - - self._validate_component_children(children) - - # Get the component fields, triggers, and props. - fields = self.get_fields() - component_specific_triggers = self.get_event_triggers() - props = self.get_props() - - # Add any events triggers. - if "event_triggers" not in kwargs: - kwargs["event_triggers"] = {} - kwargs["event_triggers"] = kwargs["event_triggers"].copy() - - # Iterate through the kwargs and set the props. - for key, value in kwargs.items(): - if ( - key.startswith("on_") - and key not in component_specific_triggers - and key not in props - ): - valid_triggers = sorted(component_specific_triggers.keys()) - msg = ( - f"The {(comp_name := type(self).__name__)} does not take in an `{key}` event trigger. " - f"Valid triggers for {comp_name}: {valid_triggers}. " - f"If {comp_name} is a third party component make sure to add `{key}` to the component's event triggers. " - f"visit https://reflex.dev/docs/wrapping-react/guide/#event-triggers for more info." - ) - raise ValueError(msg) - if key in component_specific_triggers: - # Event triggers are bound to event chains. - is_var = False - elif key in props: - # Set the field type. - is_var = ( - field.type_origin is Var if (field := fields.get(key)) else False - ) - else: - continue - - # Check whether the key is a component prop. - if is_var: - try: - kwargs[key] = LiteralVar.create(value) - - # Get the passed type and the var type. - passed_type = kwargs[key]._var_type - expected_type = types.get_args( - types.get_field_type(type(self), key) - )[0] - except TypeError: - # If it is not a valid var, check the base types. - passed_type = type(value) - expected_type = types.get_field_type(type(self), key) - - if not satisfies_type_hint(value, expected_type): - value_name = value._js_expr if isinstance(value, Var) else value - - additional_info = ( - " You can call `.bool()` on the value to convert it to a boolean." - if expected_type is bool and isinstance(value, Var) - else "" - ) - - raise TypeError( - f"Invalid var passed for prop {type(self).__name__}.{key}, expected type {expected_type}, got value {value_name} of type {passed_type}." - + additional_info - ) - # Check if the key is an event trigger. - if key in component_specific_triggers: - kwargs["event_triggers"][key] = EventChain.create( - value=value, - args_spec=component_specific_triggers[key], - key=key, - ) - - # Remove any keys that were added as events. - for key in kwargs["event_triggers"]: - kwargs.pop(key, None) - - # Place data_ and aria_ attributes into custom_attrs - special_attributes = [ - key - for key in kwargs - if key not in fields and SpecialAttributes.is_special(key) - ] - if special_attributes: - custom_attrs = kwargs.setdefault("custom_attrs", {}) - custom_attrs.update({ - format.to_kebab_case(key): kwargs.pop(key) for key in special_attributes - }) - - # Add style props to the component. - style = kwargs.get("style", {}) - if isinstance(style, Sequence): - if any(not isinstance(s, Mapping) for s in style): - msg = "Style must be a dictionary or a list of dictionaries." - raise TypeError(msg) - # Merge styles, the later ones overriding keys in the earlier ones. - style = { - k: v - for style_dict in style - for k, v in cast(Mapping, style_dict).items() - } - - if isinstance(style, (Breakpoints, Var)): - style = { - # Assign the Breakpoints to the self-referential selector to avoid squashing down to a regular dict. - "&": style, - } - - fields_style = self.get_fields()["style"] - - kwargs["style"] = Style({ - **fields_style.default_value(), - **style, - **{attr: value for attr, value in kwargs.items() if attr not in fields}, - }) - - # Convert class_name to str if it's list - class_name = kwargs.get("class_name", "") - if isinstance(class_name, (list, tuple)): - has_var = False - for c in class_name: - if isinstance(c, str): - continue - if isinstance(c, Var): - if not isinstance(c, StringVar) and not issubclass( - c._var_type, str - ): - msg = f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {c._js_expr} of type {c._var_type}." - raise TypeError(msg) - has_var = True - else: - msg = f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {c} of type {type(c)}." - raise TypeError(msg) - if has_var: - kwargs["class_name"] = LiteralArrayVar.create( - class_name, _var_type=list[str] - ).join(" ") - else: - kwargs["class_name"] = " ".join(class_name) - elif ( - isinstance(class_name, Var) - and not isinstance(class_name, StringVar) - and not issubclass(class_name._var_type, str) - ): - msg = f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {class_name._js_expr} of type {class_name._var_type}." - raise TypeError(msg) - # Construct the component. - for key, value in kwargs.items(): - setattr(self, key, value) - - @classmethod - def get_event_triggers(cls) -> dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]]: - """Get the event triggers for the component. - - Returns: - The event triggers. - """ - # Look for component specific triggers, - # e.g. variable declared as EventHandler types. - return DEFAULT_TRIGGERS | args_specs_from_fields(cls.get_fields()) # pyright: ignore [reportOperatorIssue] - - def __repr__(self) -> str: - """Represent the component in React. - - Returns: - The code to render the component. - """ - return format.json_dumps(self.render()) - - def __str__(self) -> str: - """Represent the component in React. - - Returns: - The code to render the component. - """ - from reflex.compiler.compiler import _compile_component - - return _compile_component(self) - - def _exclude_props(self) -> list[str]: - """Props to exclude when adding the component props to the Tag. - - Returns: - A list of component props to exclude. - """ - return [] - - def _render(self, props: dict[str, Any] | None = None) -> Tag: - """Define how to render the component in React. - - Args: - props: The props to render (if None, then use get_props). - - Returns: - The tag to render. - """ - # Create the base tag. - name = (self.tag if not self.alias else self.alias) or "" - if self._is_tag_in_global_scope and self.library is None: - name = '"' + name + '"' - - # Create the base tag. - tag = Tag( - name=name, - special_props=self.special_props.copy(), - ) - - if props is None: - # Add component props to the tag. - props = { - attr.removesuffix("_"): getattr(self, attr) for attr in self.get_props() - } - - # Add ref to element if `ref` is None and `id` is not None. - if self.ref is not None: - props["ref"] = self.ref - elif (ref := self.get_ref()) is not None: - props["ref"] = Var(_js_expr=ref) - else: - props = props.copy() - - props.update( - **{ - trigger: handler - for trigger, handler in self.event_triggers.items() - if trigger not in {EventTriggers.ON_MOUNT, EventTriggers.ON_UNMOUNT} - }, - key=self.key, - id=self.id, - class_name=self.class_name, - ) - props.update(self._get_style()) - props.update(self.custom_attrs) - - # remove excluded props from prop dict before adding to tag. - for prop_to_exclude in self._exclude_props(): - props.pop(prop_to_exclude, None) - - return tag.add_props(**props) - - @classmethod - @functools.cache - def get_props(cls) -> Iterable[str]: - """Get the unique fields for the component. - - Returns: - The unique fields. - """ - return cls.get_js_fields() - - @classmethod - @functools.cache - def get_initial_props(cls) -> set[str]: - """Get the initial props to set for the component. - - Returns: - The initial props to set. - """ - return set() - - @functools.cached_property - def _get_component_prop_property(self) -> Sequence[BaseComponent]: - return [ - component - for prop in self.get_props() - if (value := getattr(self, prop)) is not None - and isinstance(value, (BaseComponent, Var)) - for component in _components_from(value) - ] - - def _get_components_in_props(self) -> Sequence[BaseComponent]: - """Get the components in the props. - - Returns: - The components in the props - """ - return self._get_component_prop_property - - @classmethod - def _validate_children(cls, children: tuple | list): - from reflex.utils.exceptions import ChildrenTypeError - - for child in children: - if isinstance(child, (tuple, list)): - cls._validate_children(child) - - # Make sure the child is a valid type. - if isinstance(child, dict) or not isinstance(child, ComponentChildTypes): - raise ChildrenTypeError(component=cls.__name__, child=child) - - @classmethod - def create(cls: type[T], *children, **props) -> T: - """Create the component. - - Args: - *children: The children of the component. - **props: The props of the component. - - Returns: - The component. - """ - # Import here to avoid circular imports. - from reflex.components.base.bare import Bare - from reflex.components.base.fragment import Fragment - - # Filter out None props - props = {key: value for key, value in props.items() if value is not None} - - # Validate all the children. - cls._validate_children(children) - - children_normalized = [ - ( - child - if isinstance(child, Component) - else ( - Fragment.create(*child) - if isinstance(child, tuple) - else Bare.create(contents=LiteralVar.create(child)) - ) - ) - for child in children - ] - - return cls._create(children_normalized, **props) - - @classmethod - def _create(cls: type[T], children: Sequence[BaseComponent], **props: Any) -> T: - """Create the component. - - Args: - children: The children of the component. - **props: The props of the component. - - Returns: - The component. - """ - comp = cls.__new__(cls) - super(Component, comp).__init__(id=props.get("id"), children=list(children)) - comp._post_init(children=list(children), **props) - return comp - - @classmethod - def _unsafe_create( - cls: type[T], children: Sequence[BaseComponent], **props: Any - ) -> T: - """Create the component without running post_init. - - Args: - children: The children of the component. - **props: The props of the component. - - Returns: - The component. - """ - comp = cls.__new__(cls) - super(Component, comp).__init__(id=props.get("id"), children=list(children)) - for prop, value in props.items(): - setattr(comp, prop, value) - return comp - - def add_style(self) -> dict[str, Any] | None: - """Add style to the component. - - Downstream components can override this method to return a style dict - that will be applied to the component. - - Returns: - The style to add. - """ - return None - - def _add_style(self) -> Style: - """Call add_style for all bases in the MRO. - - Downstream components should NOT override. Use add_style instead. - - Returns: - The style to add. - """ - styles = [] - - # Walk the MRO to call all `add_style` methods. - for base in self._iter_parent_classes_with_method("add_style"): - s = base.add_style(self) - if s is not None: - styles.append(s) - - style_ = Style() - for s in reversed(styles): - style_.update(s) - return style_ - - def _get_component_style(self, styles: ComponentStyle | Style) -> Style | None: - """Get the style to the component from `App.style`. - - Args: - styles: The style to apply. - - Returns: - The style of the component. - """ - component_style = None - if (style := styles.get(type(self))) is not None: # pyright: ignore [reportArgumentType] - component_style = Style(style) - if (style := styles.get(self.create)) is not None: # pyright: ignore [reportArgumentType] - component_style = Style(style) - return component_style - - def _add_style_recursive( - self, style: ComponentStyle | Style, theme: Component | None = None - ) -> Component: - """Add additional style to the component and its children. - - Apply order is as follows (with the latest overriding the earliest): - 1. Default style from `_add_style`/`add_style`. - 2. User-defined style from `App.style`. - 3. User-defined style from `Component.style`. - 4. style dict and css props passed to the component instance. - - Args: - style: A dict from component to styling. - theme: The theme to apply. (for retro-compatibility with deprecated _apply_theme API) - - Returns: - The component with the additional style. - - Raises: - UserWarning: If `_add_style` has been overridden. - """ - # 1. Default style from `_add_style`/`add_style`. - if type(self)._add_style != Component._add_style: - msg = "Do not override _add_style directly. Use add_style instead." - raise UserWarning(msg) - new_style = self._add_style() - style_vars = [new_style._var_data] - - # 2. User-defined style from `App.style`. - component_style = self._get_component_style(style) - if component_style: - new_style.update(component_style) - style_vars.append(component_style._var_data) - - # 4. style dict and css props passed to the component instance. - new_style.update(self.style) - style_vars.append(self.style._var_data) - - new_style._var_data = VarData.merge(*style_vars) - - # Assign the new style - self.style = new_style - - # Recursively add style to the children. - for child in self.children: - # Skip BaseComponent and StatefulComponent children. - if not isinstance(child, Component): - continue - child._add_style_recursive(style, theme) - return self - - def _get_style(self) -> dict: - """Get the style for the component. - - Returns: - The dictionary of the component style as value and the style notation as key. - """ - if isinstance(self.style, Var): - return {"css": self.style} - emotion_style = format_as_emotion(self.style) - return ( - {"css": LiteralVar.create(emotion_style)} - if emotion_style is not None - else {} - ) - - def render(self) -> dict: - """Render the component. - - Returns: - The dictionary for template of component. - """ - tag = self._render() - rendered_dict = dict( - tag.set( - children=[child.render() for child in self.children], - ) - ) - self._replace_prop_names(rendered_dict) - return rendered_dict - - def _replace_prop_names(self, rendered_dict: dict) -> None: - """Replace the prop names in the render dictionary. - - Args: - rendered_dict: The render dictionary with all the component props and event handlers. - """ - # fast path - if not self._rename_props: - return - - for ix, prop in enumerate(rendered_dict["props"]): - for old_prop, new_prop in self._rename_props.items(): - if prop.startswith(old_prop): - rendered_dict["props"][ix] = prop.replace(old_prop, new_prop, 1) - - def _validate_component_children(self, children: list[Component]): - """Validate the children components. - - Args: - children: The children of the component. - - """ - from reflex.components.base.fragment import Fragment - from reflex.components.core.cond import Cond - from reflex.components.core.foreach import Foreach - from reflex.components.core.match import Match - - no_valid_parents_defined = all(child._valid_parents == [] for child in children) - if ( - not self._invalid_children - and not self._valid_children - and no_valid_parents_defined - ): - return - - comp_name = type(self).__name__ - allowed_components = [ - comp.__name__ for comp in (Fragment, Foreach, Cond, Match) - ] - - def validate_child(child: Any): - child_name = type(child).__name__ - - # Iterate through the immediate children of fragment - if isinstance(child, Fragment): - for c in child.children: - validate_child(c) - - if isinstance(child, Cond): - validate_child(child.children[0]) - validate_child(child.children[1]) - - if isinstance(child, Match): - for cases in child.match_cases: - validate_child(cases[-1]) - validate_child(child.default) - - if self._invalid_children and child_name in self._invalid_children: - msg = f"The component `{comp_name}` cannot have `{child_name}` as a child component" - raise ValueError(msg) - - if self._valid_children and child_name not in [ - *self._valid_children, - *allowed_components, - ]: - valid_child_list = ", ".join([ - f"`{v_child}`" for v_child in self._valid_children - ]) - msg = f"The component `{comp_name}` only allows the components: {valid_child_list} as children. Got `{child_name}` instead." - raise ValueError(msg) - - if child._valid_parents and all( - clz_name not in [*child._valid_parents, *allowed_components] - for clz_name in self._iter_parent_classes_names() - ): - valid_parent_list = ", ".join([ - f"`{v_parent}`" for v_parent in child._valid_parents - ]) - msg = f"The component `{child_name}` can only be a child of the components: {valid_parent_list}. Got `{comp_name}` instead." - raise ValueError(msg) - - for child in children: - validate_child(child) - - @staticmethod - def _get_vars_from_event_triggers( - event_triggers: dict[str, EventChain | Var], - ) -> Iterator[tuple[str, list[Var]]]: - """Get the Vars associated with each event trigger. - - Args: - event_triggers: The event triggers from the component instance. - - Yields: - tuple of (event_name, event_vars) - """ - for event_trigger, event in event_triggers.items(): - if isinstance(event, Var): - yield event_trigger, [event] - elif isinstance(event, EventChain): - event_args = [] - for spec in event.events: - if isinstance(spec, EventSpec): - for args in spec.args: - event_args.extend(args) - else: - event_args.append(spec) - yield event_trigger, event_args - - def _get_vars( - self, include_children: bool = False, ignore_ids: set[int] | None = None - ) -> Iterator[Var]: - """Walk all Vars used in this component. - - Args: - include_children: Whether to include Vars from children. - ignore_ids: The ids to ignore. - - Yields: - Each var referenced by the component (props, styles, event handlers). - """ - ignore_ids = ignore_ids or set() - vars: list[Var] | None = getattr(self, "__vars", None) - if vars is not None: - yield from vars - vars = self.__vars = [] - # Get Vars associated with event trigger arguments. - for _, event_vars in self._get_vars_from_event_triggers(self.event_triggers): - vars.extend(event_vars) - - # Get Vars associated with component props. - for prop in self.get_props(): - prop_var = getattr(self, prop) - if isinstance(prop_var, Var): - vars.append(prop_var) - - # Style keeps track of its own VarData instance, so embed in a temp Var that is yielded. - if (isinstance(self.style, dict) and self.style) or isinstance(self.style, Var): - vars.append( - Var( - _js_expr="style", - _var_type=str, - _var_data=VarData.merge(self.style._var_data), - ) - ) - - # Special props are always Var instances. - vars.extend(self.special_props) - - # Get Vars associated with common Component props. - for comp_prop in ( - self.class_name, - self.id, - self.key, - *self.custom_attrs.values(), - ): - if isinstance(comp_prop, Var): - vars.append(comp_prop) - elif isinstance(comp_prop, str): - # Collapse VarData encoded in f-strings. - var = LiteralStringVar.create(comp_prop) - if var._get_all_var_data() is not None: - vars.append(var) - - # Get Vars associated with children. - if include_children: - for child in self.children: - if not isinstance(child, Component) or id(child) in ignore_ids: - continue - ignore_ids.add(id(child)) - child_vars = child._get_vars( - include_children=include_children, ignore_ids=ignore_ids - ) - vars.extend(child_vars) - - yield from vars - - def _event_trigger_values_use_state(self) -> bool: - """Check if the values of a component's event trigger use state. - - Returns: - True if any of the component's event trigger values uses State. - """ - for trigger in self.event_triggers.values(): - if isinstance(trigger, EventChain): - for event in trigger.events: - if isinstance(event, EventCallback): - continue - if isinstance(event, EventSpec): - if ( - event.handler.state_full_name - and event.handler.state_full_name != FRONTEND_EVENT_STATE - ): - return True - else: - if event._var_state: - return True - elif isinstance(trigger, Var) and trigger._var_state: - return True - return False - - def _has_stateful_event_triggers(self): - """Check if component or children have any event triggers that use state. - - Returns: - True if the component or children have any event triggers that uses state. - """ - if self.event_triggers and self._event_trigger_values_use_state(): - return True - for child in self.children: - if isinstance(child, Component) and child._has_stateful_event_triggers(): - return True - return False - - @classmethod - def _iter_parent_classes_names(cls) -> Iterator[str]: - for clz in cls.mro(): - if clz is Component: - break - yield clz.__name__ - - @classmethod - def _iter_parent_classes_with_method(cls, method: str) -> Sequence[type[Component]]: - """Iterate through parent classes that define a given method. - - Used for handling the `add_*` API functions that internally simulate a super() call chain. - - Args: - method: The method to look for. - - Returns: - A sequence of parent classes that define the method (differently than the base). - """ - current_class_method = getattr(Component, method, None) - seen_methods = ( - {current_class_method} if current_class_method is not None else set() - ) - clzs: list[type[Component]] = [] - for clz in cls.mro(): - if clz is Component: - break - if not issubclass(clz, Component): - continue - method_func = getattr(clz, method, None) - if not callable(method_func) or method_func in seen_methods: - continue - seen_methods.add(method_func) - clzs.append(clz) - return clzs - - def _get_custom_code(self) -> str | None: - """Get custom code for the component. - - Returns: - The custom code. - """ - return None - - def _get_all_custom_code(self) -> dict[str, None]: - """Get custom code for the component and its children. - - Returns: - The custom code. - """ - # Store the code in a set to avoid duplicates. - code: dict[str, None] = {} - - # Add the custom code for this component. - custom_code = self._get_custom_code() - if custom_code is not None: - code[custom_code] = None - - for component in self._get_components_in_props(): - code |= component._get_all_custom_code() - - # Add the custom code from add_custom_code method. - for clz in self._iter_parent_classes_with_method("add_custom_code"): - for item in clz.add_custom_code(self): - code[item] = None - - # Add the custom code for the children. - for child in self.children: - code |= child._get_all_custom_code() - - # Return the code. - return code - - def _get_dynamic_imports(self) -> str | None: - """Get dynamic import for the component. - - Returns: - The dynamic import. - """ - return None - - def _get_all_dynamic_imports(self) -> set[str]: - """Get dynamic imports for the component and its children. - - Returns: - The dynamic imports. - """ - # Store the import in a set to avoid duplicates. - dynamic_imports: set[str] = set() - - # Get dynamic import for this component. - dynamic_import = self._get_dynamic_imports() - if dynamic_import: - dynamic_imports.add(dynamic_import) - - # Get the dynamic imports from children - for child in self.children: - dynamic_imports |= child._get_all_dynamic_imports() - - for component in self._get_components_in_props(): - dynamic_imports |= component._get_all_dynamic_imports() - - # Return the dynamic imports - return dynamic_imports - - def _get_dependencies_imports(self) -> ParsedImportDict: - """Get the imports from lib_dependencies for installing. - - Returns: - The dependencies imports of the component. - """ - return { - dep: [ImportVar(tag=None, render=False)] for dep in self.lib_dependencies - } - - def _get_hooks_imports(self) -> ParsedImportDict: - """Get the imports required by certain hooks. - - Returns: - The imports required for all selected hooks. - """ - imports_ = {} - - if self._get_ref_hook() is not None: - # Handle hooks needed for attaching react refs to DOM nodes. - imports_.setdefault("react", set()).add(ImportVar(tag="useRef")) - imports_.setdefault(f"$/{Dirs.STATE_PATH}", set()).add( - ImportVar(tag="refs") - ) - - if self._get_mount_lifecycle_hook(): - # Handle hooks for `on_mount` / `on_unmount`. - imports_.setdefault("react", set()).add(ImportVar(tag="useEffect")) - - other_imports = [] - user_hooks = self._get_hooks() - user_hooks_data = ( - VarData.merge(user_hooks._get_all_var_data()) - if user_hooks is not None and isinstance(user_hooks, Var) - else None - ) - if user_hooks_data is not None: - other_imports.append(user_hooks_data.imports) - other_imports.extend( - hook_vardata.imports - for hook_vardata in self._get_added_hooks().values() - if hook_vardata is not None - ) - - return imports.merge_imports(imports_, *other_imports) - - def _get_imports(self) -> ParsedImportDict: - """Get all the libraries and fields that are used by the component. - - Returns: - The imports needed by the component. - """ - imports_ = ( - {self.library: [self.import_var]} - if self.library is not None and self.tag is not None - else {} - ) - - # Get static imports required for event processing. - event_imports = Imports.EVENTS if self.event_triggers else {} - - # Collect imports from Vars used directly by this component. - var_imports = [ - dict(var_data.imports) - for var in self._get_vars() - if (var_data := var._get_all_var_data()) is not None - ] - - added_import_dicts: list[ParsedImportDict] = [] - for clz in self._iter_parent_classes_with_method("add_imports"): - list_of_import_dict = clz.add_imports(self) - - if not isinstance(list_of_import_dict, list): - added_import_dicts.append(imports.parse_imports(list_of_import_dict)) - else: - added_import_dicts.extend([ - imports.parse_imports(item) for item in list_of_import_dict - ]) - - return imports.merge_parsed_imports( - self._get_dependencies_imports(), - self._get_hooks_imports(), - imports_, - event_imports, - *var_imports, - *added_import_dicts, - ) - - def _get_all_imports(self, collapse: bool = False) -> ParsedImportDict: - """Get all the libraries and fields that are used by the component and its children. - - Args: - collapse: Whether to collapse the imports by removing duplicates. - - Returns: - The import dict with the required imports. - """ - imports_ = imports.merge_parsed_imports( - self._get_imports(), *[child._get_all_imports() for child in self.children] - ) - return imports.collapse_imports(imports_) if collapse else imports_ - - def _get_mount_lifecycle_hook(self) -> str | None: - """Generate the component lifecycle hook. - - Returns: - The useEffect hook for managing `on_mount` and `on_unmount` events. - """ - # pop on_mount and on_unmount from event_triggers since these are handled by - # hooks, not as actually props in the component - on_mount = self.event_triggers.get(EventTriggers.ON_MOUNT, None) - on_unmount = self.event_triggers.get(EventTriggers.ON_UNMOUNT, None) - if on_mount is not None: - on_mount = str(LiteralVar.create(on_mount)) + "()" - if on_unmount is not None: - on_unmount = str(LiteralVar.create(on_unmount)) + "()" - if on_mount is not None or on_unmount is not None: - return f""" - useEffect(() => {{ - {on_mount or ""} - return () => {{ - {on_unmount or ""} - }} - }}, []);""" - return None - - def _get_ref_hook(self) -> Var | None: - """Generate the ref hook for the component. - - Returns: - The useRef hook for managing refs. - """ - ref = self.get_ref() - if ref is not None: - return Var( - f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};", - _var_data=VarData(position=Hooks.HookPosition.INTERNAL), - ) - return None - - def _get_vars_hooks(self) -> dict[str, VarData | None]: - """Get the hooks required by vars referenced in this component. - - Returns: - The hooks for the vars. - """ - vars_hooks = {} - for var in self._get_vars(): - var_data = var._get_all_var_data() - if var_data is not None: - vars_hooks.update( - var_data.hooks - if isinstance(var_data.hooks, dict) - else { - k: VarData(position=Hooks.HookPosition.INTERNAL) - for k in var_data.hooks - } - ) - for component in var_data.components: - vars_hooks.update(component._get_all_hooks()) - return vars_hooks - - def _get_events_hooks(self) -> dict[str, VarData | None]: - """Get the hooks required by events referenced in this component. - - Returns: - The hooks for the events. - """ - return ( - {Hooks.EVENTS: VarData(position=Hooks.HookPosition.INTERNAL)} - if self.event_triggers - else {} - ) - - def _get_hooks_internal(self) -> dict[str, VarData | None]: - """Get the React hooks for this component managed by the framework. - - Downstream components should NOT override this method to avoid breaking - framework functionality. - - Returns: - The internally managed hooks. - """ - return { - **{ - str(hook): VarData(position=Hooks.HookPosition.INTERNAL) - for hook in [self._get_ref_hook(), self._get_mount_lifecycle_hook()] - if hook is not None - }, - **self._get_vars_hooks(), - **self._get_events_hooks(), - } - - def _get_added_hooks(self) -> dict[str, VarData | None]: - """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): - var_data = VarData.merge(hook._get_all_var_data()) - if var_data is not None: - for sub_hook in var_data.hooks: - code[sub_hook] = None - - if str(hook) in code: - code[str(hook)] = VarData.merge(var_data, code[str(hook)]) - else: - code[str(hook)] = var_data - - # 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(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] = None - - return code - - def _get_hooks(self) -> str | None: - """Get the React hooks for this component. - - Downstream components should override this method to add their own hooks. - - Returns: - The hooks for just this component. - """ - return - - def _get_all_hooks_internal(self) -> dict[str, VarData | None]: - """Get the reflex internal hooks for the component and its children. - - Returns: - The code that should appear just before user-defined hooks. - """ - # Store the code in a set to avoid duplicates. - code = self._get_hooks_internal() - - # Add the hook code for the children. - for child in self.children: - code.update(child._get_all_hooks_internal()) - - return code - - def _get_all_hooks(self) -> dict[str, VarData | None]: - """Get the React hooks for this component and its children. - - Returns: - The code that should appear just before returning the rendered component. - """ - code = {} - - # Add the internal hooks for this component. - code.update(self._get_hooks_internal()) - - # Add the hook code for this component. - hooks = self._get_hooks() - if hooks is not None: - code[hooks] = None - - code.update(self._get_added_hooks()) - - # Add the hook code for the children. - for child in self.children: - code.update(child._get_all_hooks()) - - return code - - def get_ref(self) -> str | None: - """Get the name of the ref for the component. - - Returns: - The ref name. - """ - # do not create a ref if the id is dynamic or unspecified - if self.id is None or isinstance(self.id, Var): - return None - return format.format_ref(self.id) - - def _get_all_refs(self) -> dict[str, None]: - """Get the refs for the children of the component. - - Returns: - The refs for the children. - """ - refs = {} - ref = self.get_ref() - if ref is not None: - refs[ref] = None - for child in self.children: - refs |= child._get_all_refs() - for component in self._get_components_in_props(): - refs |= component._get_all_refs() - - return refs - - @property - def import_var(self): - """The tag to import. - - Returns: - An import var. - """ - # If the tag is dot-qualified, only import the left-most name. - tag = self.tag.partition(".")[0] if self.tag else None - alias = self.alias.partition(".")[0] if self.alias else None - return ImportVar(tag=tag, is_default=self.is_default, alias=alias) - - @staticmethod - def _get_app_wrap_components() -> dict[tuple[int, str], Component]: - """Get the app wrap components for the component. - - Returns: - The app wrap components. - """ - return {} - - def _get_all_app_wrap_components( - self, *, ignore_ids: set[int] | None = None - ) -> dict[tuple[int, str], Component]: - """Get the app wrap components for the component and its children. - - Args: - ignore_ids: A set of component IDs to ignore. Used to avoid duplicates. - - Returns: - The app wrap components. - """ - ignore_ids = ignore_ids or set() - # Store the components in a set to avoid duplicates. - components = self._get_app_wrap_components() - - for component in tuple(components.values()): - component_id = id(component) - if component_id in ignore_ids: - continue - ignore_ids.add(component_id) - components.update( - component._get_all_app_wrap_components(ignore_ids=ignore_ids) - ) - - # Add the app wrap components for the children. - for child in self.children: - child_id = id(child) - # Skip BaseComponent and StatefulComponent children. - if not isinstance(child, Component) or child_id in ignore_ids: - continue - ignore_ids.add(child_id) - components.update(child._get_all_app_wrap_components(ignore_ids=ignore_ids)) - - # Return the components. - return components - - -class CustomComponent(Component): - """A custom user-defined component.""" - - # Use the components library. - library = f"$/{Dirs.COMPONENTS_PATH}" - - component_fn: Callable[..., Component] = field( - doc="The function that creates the component.", default=Component.create - ) - - props: dict[str, Any] = field( - doc="The props of the component.", default_factory=dict - ) - - def _post_init(self, **kwargs): - """Initialize the custom component. - - Args: - **kwargs: The kwargs to pass to the component. - """ - component_fn = kwargs.get("component_fn") - - # Set the props. - props_types = typing.get_type_hints(component_fn) if component_fn else {} - props = {key: value for key, value in kwargs.items() if key in props_types} - kwargs = {key: value for key, value in kwargs.items() if key not in props_types} - - event_types = { - key - for key in props - if ( - (get_origin((annotation := props_types.get(key))) or annotation) - == EventHandler - ) - } - - def get_args_spec(key: str) -> types.ArgsSpec | Sequence[types.ArgsSpec]: - type_ = props_types[key] - - return ( - args[0] - if (args := get_args(type_)) - else ( - annotation_args[1] - if get_origin( - annotation := inspect.getfullargspec(component_fn).annotations[ - key - ] - ) - is typing.Annotated - and (annotation_args := get_args(annotation)) - else no_args_event_spec - ) - ) - - super()._post_init( - event_triggers={ - key: EventChain.create( - value=props[key], - args_spec=get_args_spec(key), - key=key, - ) - for key in event_types - }, - **kwargs, - ) - - to_camel_cased_props = { - format.to_camel_case(key): None for key in props if key not in event_types - } - self.get_props = lambda: to_camel_cased_props # pyright: ignore [reportIncompatibleVariableOverride] - - # Unset the style. - self.style = Style() - - # Set the tag to the name of the function. - self.tag = format.to_title_case(self.component_fn.__name__) - - for key, value in props.items(): - # Skip kwargs that are not props. - if key not in props_types: - continue - - camel_cased_key = format.to_camel_case(key) - - # Get the type based on the annotation. - type_ = props_types[key] - - # Handle event chains. - if type_ is EventHandler: - inspect.getfullargspec(component_fn).annotations[key] - self.props[camel_cased_key] = EventChain.create( - value=value, args_spec=get_args_spec(key), key=key - ) - continue - - value = LiteralVar.create(value) - self.props[camel_cased_key] = value - setattr(self, camel_cased_key, value) - - def __eq__(self, other: Any) -> bool: - """Check if the component is equal to another. - - Args: - other: The other component. - - Returns: - Whether the component is equal to the other. - """ - return isinstance(other, CustomComponent) and self.tag == other.tag - - def __hash__(self) -> int: - """Get the hash of the component. - - Returns: - The hash of the component. - """ - return hash(self.tag) - - @classmethod - def get_props(cls) -> Iterable[str]: - """Get the props for the component. - - Returns: - The set of component props. - """ - return () - - @staticmethod - def _get_event_spec_from_args_spec(name: str, event: EventChain) -> Callable: - """Get the event spec from the args spec. - - Args: - name: The name of the event - event: The args spec. - - Returns: - The event spec. - """ - - def fn(*args): - return run_script(Var(name).to(FunctionVar).call(*args)) - - if event.args_spec: - arg_spec = ( - event.args_spec - if not isinstance(event.args_spec, Sequence) - else event.args_spec[0] - ) - names = inspect.getfullargspec(arg_spec).args - fn.__signature__ = inspect.Signature( # pyright: ignore[reportFunctionMemberAccess] - parameters=[ - inspect.Parameter( - name=name, - kind=inspect.Parameter.POSITIONAL_ONLY, - annotation=arg._var_type, - ) - for name, arg in zip( - names, parse_args_spec(event.args_spec)[0], strict=True - ) - ] - ) - - return fn - - def get_prop_vars(self) -> list[Var | Callable]: - """Get the prop vars. - - Returns: - The prop vars. - """ - return [ - Var( - _js_expr=name + CAMEL_CASE_MEMO_MARKER, - _var_type=(prop._var_type if isinstance(prop, Var) else type(prop)), - ).guess_type() - if isinstance(prop, Var) or not isinstance(prop, EventChain) - else CustomComponent._get_event_spec_from_args_spec( - name + CAMEL_CASE_MEMO_MARKER, prop - ) - for name, prop in self.props.items() - ] - - @functools.cache # noqa: B019 - def get_component(self) -> Component: - """Render the component. - - Returns: - The code to render the component. - """ - component = self.component_fn(*self.get_prop_vars()) - - try: - from reflex.utils.prerequisites import get_and_validate_app - - style = get_and_validate_app().app.style - except Exception: - style = {} - - component._add_style_recursive(style) - return component - - def _get_all_app_wrap_components( - self, *, ignore_ids: set[int] | None = None - ) -> dict[tuple[int, str], Component]: - """Get the app wrap components for the custom component. - - Args: - ignore_ids: A set of IDs to ignore to avoid infinite recursion. - - Returns: - The app wrap components. - """ - ignore_ids = ignore_ids or set() - component = self.get_component() - if id(component) in ignore_ids: - return {} - ignore_ids.add(id(component)) - return self.get_component()._get_all_app_wrap_components(ignore_ids=ignore_ids) - - -CUSTOM_COMPONENTS: dict[str, CustomComponent] = {} - - -def _register_custom_component( - component_fn: Callable[..., Component], -): - """Register a custom component to be compiled. - - Args: - component_fn: The function that creates the component. - - Returns: - The custom component. - - Raises: - TypeError: If the tag name cannot be determined. - """ - dummy_props = { - prop: ( - Var( - "", - _var_type=unwrap_var_annotation(annotation), - ).guess_type() - if not types.safe_issubclass(annotation, EventHandler) - else EventSpec(handler=EventHandler(fn=no_args_event_spec)) - ) - for prop, annotation in typing.get_type_hints(component_fn).items() - if prop != "return" - } - dummy_component = CustomComponent._create( - children=[], - component_fn=component_fn, - **dummy_props, - ) - if dummy_component.tag is None: - msg = f"Could not determine the tag name for {component_fn!r}" - raise TypeError(msg) - CUSTOM_COMPONENTS[dummy_component.tag] = dummy_component - return dummy_component - - -def custom_component( - component_fn: Callable[..., Component], -) -> Callable[..., CustomComponent]: - """Create a custom component from a function. - - Args: - component_fn: The function that creates the component. - - Returns: - The decorated function. - """ - - @wraps(component_fn) - def wrapper(*children, **props) -> CustomComponent: - # Remove the children from the props. - props.pop("children", None) - return CustomComponent._create( - children=list(children), component_fn=component_fn, **props - ) - - # Register this component so it can be compiled. - dummy_component = _register_custom_component(component_fn) - if tag := dummy_component.tag: - object.__setattr__( - wrapper, - "_as_var", - lambda: Var( - tag, - _var_type=type[Component], - _var_data=VarData( - imports={ - f"$/{constants.Dirs.UTILS}/components": [ImportVar(tag=tag)], - "@emotion/react": [ - ImportVar(tag="jsx"), - ], - } - ), - ), - ) - - return wrapper - - -# Alias memo to custom_component. -memo = custom_component - - -class NoSSRComponent(Component): - """A dynamic component that is not rendered on the server.""" - - def _get_import_name(self) -> str | None: - if not self.library: - return None - return f"${self.library}" if self.library.startswith("/") else self.library - - def _get_imports(self) -> ParsedImportDict: - """Get the imports for the component. - - Returns: - The imports for dynamically importing the component at module load time. - """ - # React lazy import mechanism. - dynamic_import = { - f"$/{constants.Dirs.UTILS}/context": [ImportVar(tag="ClientSide")], - } - - # The normal imports for this component. - imports_ = super()._get_imports() - - # Do NOT import the main library/tag statically. - import_name = self._get_import_name() - if import_name is not None: - with contextlib.suppress(ValueError): - imports_[import_name].remove(self.import_var) - imports_[import_name].append(ImportVar(tag=None, render=False)) - - return imports.merge_imports( - dynamic_import, - imports_, - self._get_dependencies_imports(), - ) - - def _get_dynamic_imports(self) -> str: - # extract the correct import name from library name - base_import_name = self._get_import_name() - if base_import_name is None: - msg = "Undefined library for NoSSRComponent" - raise ValueError(msg) - import_name = format.format_library_name(base_import_name) - - library_import = f"import('{import_name}')" - mod_import = ( - # https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#with-named-exports - f".then((mod) => mod.{self.tag})" - if not self.is_default - else ".then((mod) => mod.default.default ?? mod.default)" - ) - return ( - f"const {self.alias or self.tag} = ClientSide(() => " - + library_import - + mod_import - + ")" - ) - - -class StatefulComponent(BaseComponent): - """A component that depends on state and is rendered outside of the page component. - - If a StatefulComponent is used in multiple pages, it will be rendered to a common file and - imported into each page that uses it. - - A stateful component has a tag name that includes a hash of the code that it renders - to. This tag name refers to the specific component with the specific props that it - was created with. - """ - - # A lookup table to caching memoized component instances. - tag_to_stateful_component: ClassVar[dict[str, StatefulComponent]] = {} - - # Reference to the original component that was memoized into this component. - component: Component = field( - default_factory=Component, is_javascript_property=False - ) - - references: int = field( - doc="How many times this component is referenced in the app.", - default=0, - is_javascript_property=False, - ) - - rendered_as_shared: bool = field( - doc="Whether the component has already been rendered to a shared file.", - default=False, - is_javascript_property=False, - ) - - memo_trigger_hooks: list[str] = field( - default_factory=list, is_javascript_property=False - ) - - @classmethod - def create(cls, component: Component) -> StatefulComponent | None: - """Create a stateful component from a component. - - Args: - component: The component to memoize. - - Returns: - The stateful component or None if the component should not be memoized. - """ - from reflex.components.core.foreach import Foreach - - if component._memoization_mode.disposition == MemoizationDisposition.NEVER: - # Never memoize this component. - return None - - if component.tag is None: - # Only memoize components with a tag. - return None - - # If _var_data is found in this component, it is a candidate for auto-memoization. - should_memoize = False - - # If the component requests to be memoized, then ignore other checks. - if component._memoization_mode.disposition == MemoizationDisposition.ALWAYS: - should_memoize = True - - if not should_memoize: - # Determine if any Vars have associated data. - for prop_var in component._get_vars(include_children=True): - if prop_var._get_all_var_data(): - should_memoize = True - break - - if not should_memoize: - # Check for special-cases in child components. - for child in component.children: - # Skip BaseComponent and StatefulComponent children. - if not isinstance(child, Component): - continue - # Always consider Foreach something that must be memoized by the parent. - if isinstance(child, Foreach): - should_memoize = True - break - child = cls._child_var(child) - if isinstance(child, Var) and child._get_all_var_data(): - should_memoize = True - break - - if should_memoize or component.event_triggers: - # Render the component to determine tag+hash based on component code. - tag_name = cls._get_tag_name(component) - if tag_name is None: - return None - - # Look up the tag in the cache - stateful_component = cls.tag_to_stateful_component.get(tag_name) - if stateful_component is None: - memo_trigger_hooks = cls._fix_event_triggers(component) - # Set the stateful component in the cache for the given tag. - stateful_component = cls.tag_to_stateful_component.setdefault( - tag_name, - cls( - children=component.children, - component=component, - tag=tag_name, - memo_trigger_hooks=memo_trigger_hooks, - ), - ) - # Bump the reference count -- multiple pages referencing the same component - # will result in writing it to a common file. - stateful_component.references += 1 - return stateful_component - - # Return None to indicate this component should not be memoized. - return None - - @staticmethod - def _child_var(child: Component) -> Var | Component: - """Get the Var from a child component. - - This method is used for special cases when the StatefulComponent should actually - wrap the parent component of the child instead of recursing into the children - and memoizing them independently. - - Args: - child: The child component. - - Returns: - The Var from the child component or the child itself (for regular cases). - """ - from reflex.components.base.bare import Bare - from reflex.components.core.cond import Cond - from reflex.components.core.foreach import Foreach - from reflex.components.core.match import Match - - if isinstance(child, Bare): - return child.contents - if isinstance(child, Cond): - return child.cond - if isinstance(child, Foreach): - return child.iterable - if isinstance(child, Match): - return child.cond - return child - - @classmethod - def _get_tag_name(cls, component: Component) -> str | None: - """Get the tag based on rendering the given component. - - Args: - component: The component to render. - - Returns: - The tag for the stateful component. - """ - # Get the render dict for the component. - rendered_code = component.render() - if not rendered_code: - # Never memoize non-visual components. - return None - - # Compute the hash based on the rendered code. - code_hash = _hash_str(_deterministic_hash(rendered_code)) - - # Format the tag name including the hash. - return format.format_state_name( - f"{component.tag or 'Comp'}_{code_hash}" - ).capitalize() - - def _render_stateful_code( - self, - export: bool = False, - ) -> str: - if not self.tag: - return "" - # Render the code for this component and hooks. - return stateful_component_template( - tag_name=self.tag, - memo_trigger_hooks=self.memo_trigger_hooks, - component=self.component, - export=export, - ) - - @classmethod - def _fix_event_triggers( - cls, - component: Component, - ) -> list[str]: - """Render the code for a stateful component. - - Args: - component: The component to render. - - Returns: - The memoized event trigger hooks for the component. - """ - # Memoize event triggers useCallback to avoid unnecessary re-renders. - memo_event_triggers = tuple(cls._get_memoized_event_triggers(component).items()) - - # Trigger hooks stored separately to write after the normal hooks (see stateful_component.js.jinja2) - memo_trigger_hooks: list[str] = [] - - if memo_event_triggers: - # Copy the component to avoid mutating the original. - component = copy.copy(component) - - for event_trigger, ( - memo_trigger, - memo_trigger_hook, - ) in memo_event_triggers: - # Replace the event trigger with the memoized version. - memo_trigger_hooks.append(memo_trigger_hook) - component.event_triggers[event_trigger] = memo_trigger - - return memo_trigger_hooks - - @staticmethod - def _get_hook_deps(hook: str) -> list[str]: - """Extract var deps from a hook. - - Args: - hook: The hook line to extract deps from. - - Returns: - A list of var names created by the hook declaration. - """ - # Ensure that the hook is a var declaration. - var_decl = hook.partition("=")[0].strip() - if not any(var_decl.startswith(kw) for kw in ["const ", "let ", "var "]): - return [] - - # Extract the var name from the declaration. - _, _, var_name = var_decl.partition(" ") - var_name = var_name.strip() - - # Break up array and object destructuring if used. - if var_name.startswith(("[", "{")): - return [ - v.strip().replace("...", "") for v in var_name.strip("[]{}").split(",") - ] - return [var_name] - - @staticmethod - def _get_deps_from_event_trigger( - event: EventChain | EventSpec | Var, - ) -> dict[str, None]: - """Get the dependencies accessed by event triggers. - - Args: - event: The event trigger to extract deps from. - - Returns: - The dependencies accessed by the event triggers. - """ - events: list = [event] - deps = {} - - if isinstance(event, EventChain): - events.extend(event.events) - - for ev in events: - if isinstance(ev, EventSpec): - for arg in ev.args: - for a in arg: - var_datas = VarData.merge(a._get_all_var_data()) - if var_datas and var_datas.deps is not None: - deps |= {str(dep): None for dep in var_datas.deps} - return deps - - @classmethod - def _get_memoized_event_triggers( - cls, - component: Component, - ) -> dict[str, tuple[Var, str]]: - """Memoize event handler functions with useCallback to avoid unnecessary re-renders. - - Args: - component: The component with events to memoize. - - Returns: - A dict of event trigger name to a tuple of the memoized event trigger Var and - the hook code that memoizes the event handler. - """ - trigger_memo = {} - for event_trigger, event_args in component._get_vars_from_event_triggers( - component.event_triggers - ): - if event_trigger in { - EventTriggers.ON_MOUNT, - EventTriggers.ON_UNMOUNT, - EventTriggers.ON_SUBMIT, - }: - # Do not memoize lifecycle or submit events. - continue - - # Get the actual EventSpec and render it. - event = component.event_triggers[event_trigger] - rendered_chain = str(LiteralVar.create(event)) - - # Hash the rendered EventChain to get a deterministic function name. - chain_hash = md5(str(rendered_chain).encode("utf-8")).hexdigest() - memo_name = f"{event_trigger}_{chain_hash}" - - # Calculate Var dependencies accessed by the handler for useCallback dep array. - var_deps = ["addEvents", "ReflexEvent"] - - # Get deps from event trigger var data. - var_deps.extend(cls._get_deps_from_event_trigger(event)) - - # Get deps from hooks. - for arg in event_args: - var_data = arg._get_all_var_data() - if var_data is None: - continue - for hook in var_data.hooks: - var_deps.extend(cls._get_hook_deps(hook)) - memo_var_data = VarData.merge( - *[var._get_all_var_data() for var in event_args], - VarData( - imports={"react": [ImportVar(tag="useCallback")]}, - ), - ) - - # Store the memoized function name and hook code for this event trigger. - trigger_memo[event_trigger] = ( - Var(_js_expr=memo_name)._replace( - _var_type=EventChain, merge_var_data=memo_var_data - ), - f"const {memo_name} = useCallback({rendered_chain}, [{', '.join(var_deps)}])", - ) - return trigger_memo - - def _get_all_hooks_internal(self) -> dict[str, VarData | None]: - """Get the reflex internal hooks for the component and its children. - - Returns: - The code that should appear just before user-defined hooks. - """ - return {} - - def _get_all_hooks(self) -> dict[str, VarData | None]: - """Get the React hooks for this component. - - Returns: - The code that should appear just before returning the rendered component. - """ - return {} - - def _get_all_imports(self) -> ParsedImportDict: - """Get all the libraries and fields that are used by the component. - - Returns: - The import dict with the required imports. - """ - if self.rendered_as_shared: - return { - f"$/{Dirs.UTILS}/{PageNames.STATEFUL_COMPONENTS}": [ - ImportVar(tag=self.tag) - ] - } - return self.component._get_all_imports() - - def _get_all_dynamic_imports(self) -> set[str]: - """Get dynamic imports for the component. - - Returns: - The dynamic imports. - """ - if self.rendered_as_shared: - return set() - return self.component._get_all_dynamic_imports() - - def _get_all_custom_code(self, export: bool = False) -> dict[str, None]: - """Get custom code for the component. - - Args: - export: Whether to export the component. - - Returns: - The custom code. - """ - if self.rendered_as_shared: - return {} - return self.component._get_all_custom_code() | ({ - self._render_stateful_code(export=export): None - }) - - def _get_all_refs(self) -> dict[str, None]: - """Get the refs for the children of the component. - - Returns: - The refs for the children. - """ - if self.rendered_as_shared: - return {} - return self.component._get_all_refs() - - def render(self) -> dict: - """Define how to render the component in React. - - Returns: - The tag to render. - """ - return dict(Tag(name=self.tag or "")) - - def __str__(self) -> str: - """Represent the component in React. - - Returns: - The code to render the component. - """ - from reflex.compiler.compiler import _compile_component - - return _compile_component(self) - - @classmethod - def compile_from(cls, component: BaseComponent) -> BaseComponent: - """Walk through the component tree and memoize all stateful components. - - Args: - component: The component to memoize. - - Returns: - The memoized component tree. - """ - if isinstance(component, Component): - if component._memoization_mode.recursive: - # Recursively memoize stateful children (default). - component.children = [ - cls.compile_from(child) for child in component.children - ] - # Memoize this component if it depends on state. - stateful_component = cls.create(component) - if stateful_component is not None: - return stateful_component - return component - - -class MemoizationLeaf(Component): - """A component that does not separately memoize its children. - - Any component which depends on finding the exact names of children - components within it, should be a memoization leaf so the compiler - does not replace the provided child tags with memoized tags. - - During creation, a memoization leaf will mark itself as wanting to be - memoized if any of its children return any hooks. - """ - - _memoization_mode = MemoizationMode(recursive=False) - - @classmethod - def create(cls, *children, **props) -> Component: - """Create a new memoization leaf component. - - Args: - *children: The children of the component. - **props: The props of the component. - - Returns: - The memoization leaf - """ - comp = super().create(*children, **props) - if comp._get_all_hooks(): - comp._memoization_mode = dataclasses.replace( - comp._memoization_mode, disposition=MemoizationDisposition.ALWAYS - ) - return comp - - -load_dynamic_serializer() - - -class ComponentVar(Var[Component], python_types=BaseComponent): - """A Var that represents a Component.""" - - -def empty_component() -> Component: - """Create an empty component. - - Returns: - An empty component. - """ - from reflex.components.base.bare import Bare - - return Bare.create("") - - -def render_dict_to_var(tag: dict | Component | str) -> Var: - """Convert a render dict to a Var. - - Args: - tag: The render dict. - - Returns: - The Var. - """ - if not isinstance(tag, dict): - if isinstance(tag, Component): - return render_dict_to_var(tag.render()) - return Var.create(tag) - - if "contents" in tag: - return Var(tag["contents"]) - - if "iterable" in tag: - function_return = LiteralArrayVar.create([ - render_dict_to_var(child.render()) for child in tag["children"] - ]) - - func = ArgsFunctionOperation.create( - (tag["arg_var_name"], tag["index_var_name"]), - function_return, - ) - - return FunctionStringVar.create("Array.prototype.map.call").call( - tag["iterable"] - if not isinstance(tag["iterable"], ObjectVar) - else tag["iterable"].items(), - func, - ) - - if "match_cases" in tag: - element = Var(tag["cond"]) - - conditionals = render_dict_to_var(tag["default"]) - - for case in tag["match_cases"][::-1]: - conditions, return_value = case - condition = Var.create(False) - for pattern in conditions: - condition = condition | ( - Var(pattern).to_string() == element.to_string() - ) - - conditionals = ternary_operation( - condition, - render_dict_to_var(return_value), - conditionals, - ) - - return conditionals - - if "cond_state" in tag: - return ternary_operation( - Var(tag["cond_state"]), - render_dict_to_var(tag["true_value"]), - render_dict_to_var(tag["false_value"]) - if tag["false_value"] is not None - else LiteralNoneVar.create(), - ) - - props = Var("({" + ",".join(tag["props"]) + "})") - - raw_tag_name = tag.get("name") - tag_name = Var(raw_tag_name or "Fragment") - - return FunctionStringVar.create( - "jsx", - ).call( - tag_name, - props, - *[render_dict_to_var(child) for child in tag["children"]], - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar): - """A Var that represents a Component.""" - - _var_value: BaseComponent = dataclasses.field(default_factory=empty_component) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """Get the name of the var. - - Returns: - The name of the var. - """ - return str(render_dict_to_var(self._var_value.render())) - - @cached_property_no_lock - def _cached_get_all_var_data(self) -> VarData | None: - """Get the VarData for the var. - - Returns: - The VarData for the var. - """ - return VarData.merge( - self._var_data, - VarData( - imports={ - "@emotion/react": ["jsx"], - "react": ["Fragment"], - }, - ), - VarData( - imports=self._var_value._get_all_imports(), - ), - ) - - def __hash__(self) -> int: - """Get the hash of the var. - - Returns: - The hash of the var. - """ - return hash((type(self).__name__, self._js_expr)) - - @classmethod - def create( - cls, - value: Component, - _var_data: VarData | None = None, - ): - """Create a var from a value. - - Args: - value: The value of the var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - var_datas = [ - var_data - for var in value._get_vars(include_children=True) - if (var_data := var._get_all_var_data()) - ] - - return LiteralComponentVar( - _js_expr="", - _var_type=type(value), - _var_data=VarData.merge( - _var_data, - *var_datas, - VarData( - components=(value,), - ), - ), - _var_value=value, - ) +from reflex_core.components.component import * diff --git a/reflex/components/dynamic.py b/reflex/components/dynamic.py index 0b98a8844b0..951d21d14c2 100644 --- a/reflex/components/dynamic.py +++ b/reflex/components/dynamic.py @@ -1,215 +1,3 @@ -"""Components that are dynamically generated on the backend.""" +"""Re-export from reflex_core.""" -from typing import TYPE_CHECKING, Union - -from reflex import constants -from reflex.utils import imports -from reflex.utils.exceptions import DynamicComponentMissingLibraryError -from reflex.utils.format import format_library_name -from reflex.utils.serializers import serializer -from reflex.vars import Var, get_unique_variable_name -from reflex.vars.base import VarData, transform - -if TYPE_CHECKING: - from reflex.components.component import Component - - -def get_cdn_url(lib: str) -> str: - """Get the CDN URL for a library. - - Args: - lib: The library to get the CDN URL for. - - Returns: - The CDN URL for the library. - """ - return f"https://cdn.jsdelivr.net/npm/{lib}" + "/+esm" - - -bundled_libraries = [ - "react", - "@radix-ui/themes", - "@emotion/react", - f"$/{constants.Dirs.UTILS}/context", - f"$/{constants.Dirs.UTILS}/state", - f"$/{constants.Dirs.UTILS}/components", -] - - -def bundle_library(component: Union["Component", str]): - """Bundle a library with the component. - - Args: - component: The component to bundle the library with. - - Raises: - DynamicComponentMissingLibraryError: Raised when a dynamic component is missing a library. - """ - if isinstance(component, str): - bundled_libraries.append(component) - return - if component.library is None: - msg = "Component must have a library to bundle." - raise DynamicComponentMissingLibraryError(msg) - bundled_libraries.append(format_library_name(component.library)) - - -def load_dynamic_serializer(): - """Load the serializer for dynamic components.""" - # Causes a circular import, so we import here. - from reflex.components.component import Component - - @serializer - def make_component(component: Component) -> str: - """Generate the code for a dynamic component. - - Args: - component: The component to generate code for. - - Returns: - The generated code - """ - # Causes a circular import, so we import here. - from reflex.compiler import compiler, templates, utils - from reflex.components.base.bare import Bare - - component = Bare.create(Var.create(component)) - - rendered_components = {} - # Include dynamic imports in the shared component. - if dynamic_imports := component._get_all_dynamic_imports(): - rendered_components.update(dict.fromkeys(dynamic_imports)) - - # Include custom code in the shared component. - rendered_components.update(component._get_all_custom_code()) - - rendered_components[ - templates.stateful_component_template( - tag_name="MySSRComponent", - memo_trigger_hooks=[], - component=component, - export=True, - ) - ] = None - - libs_in_window = bundled_libraries - - component_imports = component._get_all_imports() - compiler._apply_common_imports(component_imports) - - imports = {} - for lib, names in component_imports.items(): - formatted_lib_name = format_library_name(lib) - if ( - not lib.startswith((".", "/", "$/")) - and not lib.startswith("http") - and formatted_lib_name not in libs_in_window - ): - imports[get_cdn_url(lib)] = names - else: - imports[lib] = names - - module_code_lines = templates.stateful_components_template( - imports=utils.compile_imports(imports), - memoized_code="\n".join(rendered_components), - ).splitlines() - - # Rewrite imports from `/` to destructure from window - for ix, line in enumerate(module_code_lines[:]): - if line.startswith("import "): - if 'from "$/' in line or 'from "/' in line: - module_code_lines[ix] = ( - line - .replace("import ", "const ", 1) - .replace(" as ", ": ") - .replace(" from ", " = window['__reflex'][", 1) - + "]" - ) - else: - for lib in libs_in_window: - if f'from "{lib}"' in line: - module_code_lines[ix] = ( - line - .replace("import ", "const ", 1) - .replace( - f' from "{lib}"', f" = window.__reflex['{lib}']", 1 - ) - .replace(" as ", ": ") - ) - if line.startswith("export function"): - module_code_lines[ix] = line.replace( - "export function", "export default function", 1 - ) - line_stripped = line.strip() - if line_stripped.startswith("{") and line_stripped.endswith("}"): - module_code_lines[ix] = line_stripped[1:-1] - - module_code_lines.insert(0, "const React = window.__reflex.react;") - - function_line = next( - index - for index, line in enumerate(module_code_lines) - if line.startswith("export default function") - ) - - module_code_lines = [ - line - for _, line in sorted( - enumerate(module_code_lines), - key=lambda x: ( - not (x[1].startswith("import ") and x[0] < function_line), - x[0], - ), - ) - ] - - return "\n".join([ - "//__reflex_evaluate", - *module_code_lines, - ]) - - @transform - def evaluate_component(js_string: Var[str]) -> Var[Component]: - """Evaluate a component. - - Args: - js_string: The JavaScript string to evaluate. - - Returns: - The evaluated JavaScript string. - """ - unique_var_name = get_unique_variable_name() - - return js_string._replace( - _js_expr=unique_var_name, - _var_type=Component, - merge_var_data=VarData.merge( - VarData( - imports={ - f"$/{constants.Dirs.STATE_PATH}": [ - imports.ImportVar(tag="evalReactComponent"), - ], - "react": [ - imports.ImportVar(tag="useState"), - imports.ImportVar(tag="useEffect"), - ], - }, - hooks={ - f"const [{unique_var_name}, set_{unique_var_name}] = useState(null);": None, - "useEffect(() => {" - "let isMounted = true;" - f"evalReactComponent({js_string!s})" - ".then((component) => {" - "if (isMounted) {" - f"set_{unique_var_name}(component);" - "}" - "});" - "return () => {" - "isMounted = false;" - "};" - "}" - f", [{js_string!s}]);": None, - }, - ), - ), - ) +from reflex_core.components.dynamic import * diff --git a/reflex/components/field.py b/reflex/components/field.py index 43944485e2b..a58c45e230f 100644 --- a/reflex/components/field.py +++ b/reflex/components/field.py @@ -1,183 +1,3 @@ -"""Shared field infrastructure for components and props.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import _MISSING_TYPE, MISSING -from typing import Annotated, Any, Generic, TypeVar, get_origin - -from reflex.utils import types -from reflex.utils.compat import annotations_from_namespace - -FIELD_TYPE = TypeVar("FIELD_TYPE") - - -class BaseField(Generic[FIELD_TYPE]): - """Base field class used by internal metadata classes.""" - - def __init__( - self, - default: FIELD_TYPE | _MISSING_TYPE = MISSING, - default_factory: Callable[[], FIELD_TYPE] | None = None, - annotated_type: type[Any] | _MISSING_TYPE = MISSING, - ) -> None: - """Initialize the field. - - Args: - default: The default value for the field. - default_factory: The default factory for the field. - annotated_type: The annotated type for the field. - """ - self.default = default - self.default_factory = default_factory - self.outer_type_ = self.annotated_type = annotated_type - - # Process type annotation - type_origin = get_origin(annotated_type) or annotated_type - if type_origin is Annotated: - type_origin = annotated_type.__origin__ # pyright: ignore [reportAttributeAccessIssue] - # For Annotated types, use the actual type inside the annotation - self.type_ = annotated_type - else: - # For other types (including Union), preserve the original type - self.type_ = annotated_type - self.type_origin = type_origin - - def default_value(self) -> FIELD_TYPE: - """Get the default value for the field. - - Returns: - The default value for the field. - - Raises: - ValueError: If no default value or factory is provided. - """ - if self.default is not MISSING: - return self.default - if self.default_factory is not None: - return self.default_factory() - msg = "No default value or factory provided." - raise ValueError(msg) - - -class FieldBasedMeta(type): - """Shared metaclass for field-based classes like components and props. - - Provides common field inheritance and processing logic for both - PropsBaseMeta and BaseComponentMeta. - """ - - def __new__( - cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any] - ) -> type: - """Create a new field-based class. - - Args: - name: The name of the class. - bases: The base classes. - namespace: The class namespace. - - Returns: - The new class. - """ - # Collect inherited fields from base classes - inherited_fields = cls._collect_inherited_fields(bases) - - # Get annotations from the namespace - annotations = cls._resolve_annotations(namespace, name) - - # Process field overrides (fields with values but no annotations) - own_fields = cls._process_field_overrides( - namespace, annotations, inherited_fields - ) - - # Process annotated fields - own_fields.update( - cls._process_annotated_fields(namespace, annotations, inherited_fields) - ) - - # Finalize fields and store on class - cls._finalize_fields(namespace, inherited_fields, own_fields) - - return super().__new__(cls, name, bases, namespace) - - @classmethod - def _collect_inherited_fields(cls, bases: tuple[type, ...]) -> dict[str, Any]: - inherited_fields: dict[str, Any] = {} - - # Collect inherited fields from base classes - for base in bases[::-1]: - if hasattr(base, "_inherited_fields"): - inherited_fields.update(base._inherited_fields) - for base in bases[::-1]: - if hasattr(base, "_own_fields"): - inherited_fields.update(base._own_fields) - - return inherited_fields - - @classmethod - def _resolve_annotations( - cls, namespace: dict[str, Any], name: str - ) -> dict[str, Any]: - return types.resolve_annotations( - annotations_from_namespace(namespace), - namespace["__module__"], - ) - - @classmethod - def _process_field_overrides( - cls, - namespace: dict[str, Any], - annotations: dict[str, Any], - inherited_fields: dict[str, Any], - ) -> dict[str, Any]: - own_fields: dict[str, Any] = {} - - for key, value in namespace.items(): - if key not in annotations and key in inherited_fields: - inherited_field = inherited_fields[key] - new_field = cls._create_field( - annotated_type=inherited_field.annotated_type, - default=value, - default_factory=None, - ) - own_fields[key] = new_field - - return own_fields - - @classmethod - def _process_annotated_fields( - cls, - namespace: dict[str, Any], - annotations: dict[str, Any], - inherited_fields: dict[str, Any], - ) -> dict[str, Any]: - raise NotImplementedError - - @classmethod - def _create_field( - cls, - annotated_type: Any, - default: Any = MISSING, - default_factory: Callable[[], Any] | None = None, - ) -> Any: - raise NotImplementedError - - @classmethod - def _finalize_fields( - cls, - namespace: dict[str, Any], - inherited_fields: dict[str, Any], - own_fields: dict[str, Any], - ) -> None: - # Combine all fields - all_fields = inherited_fields | own_fields - - # Set field names for compatibility - for field_name, field in all_fields.items(): - field._name = field_name - - # Store field mappings on the class - namespace["_own_fields"] = own_fields - namespace["_inherited_fields"] = inherited_fields - namespace["_fields"] = all_fields +from reflex_core.components.field import * diff --git a/reflex/components/literals.py b/reflex/components/literals.py index 0616677acdf..974b23f9304 100644 --- a/reflex/components/literals.py +++ b/reflex/components/literals.py @@ -1,34 +1,3 @@ -"""Literal custom type used by Reflex.""" +"""Re-export from reflex_core.components.literals.""" -from typing import Literal - -# Base Literals -LiteralInputType = Literal[ - "button", - "checkbox", - "color", - "date", - "datetime-local", - "email", - "file", - "hidden", - "image", - "month", - "number", - "password", - "radio", - "range", - "reset", - "search", - "submit", - "tel", - "text", - "time", - "url", - "week", -] - - -LiteralRowMarker = Literal[ - "none", "number", "checkbox", "both", "checkbox-visible", "clickable-number" -] +from reflex_core.components.literals import * diff --git a/reflex/components/props.py b/reflex/components/props.py index bfa03e50851..a139804455f 100644 --- a/reflex/components/props.py +++ b/reflex/components/props.py @@ -1,441 +1,3 @@ -"""A class that holds props to be passed or applied to a component.""" +"""Re-export from reflex_core.components.props.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import _MISSING_TYPE, MISSING -from typing import Any, TypeVar, get_args, get_origin - -from typing_extensions import dataclass_transform - -from reflex.components.field import BaseField, FieldBasedMeta -from reflex.event import EventChain, args_specs_from_fields -from reflex.utils import format -from reflex.utils.exceptions import InvalidPropValueError -from reflex.utils.serializers import serializer -from reflex.utils.types import is_union -from reflex.vars.object import LiteralObjectVar - -PROPS_FIELD_TYPE = TypeVar("PROPS_FIELD_TYPE") - - -def _get_props_subclass(field_type: Any) -> type | None: - """Extract the Props subclass from a field type annotation. - - Args: - field_type: The type annotation to check. - - Returns: - The Props subclass if found, None otherwise. - """ - from reflex.utils.types import typehint_issubclass - - # For direct class types, we can return them directly if they're Props subclasses - if isinstance(field_type, type): - return field_type if typehint_issubclass(field_type, PropsBase) else None - - # For Union types, check each union member - if is_union(field_type): - for arg in get_args(field_type): - result = _get_props_subclass(arg) - if result is not None: - return result - - return None - - -def _find_props_in_list_annotation(field_type: Any) -> type | None: - """Find Props subclass within a list type annotation. - - Args: - field_type: The type annotation to check (e.g., list[SomeProps] or list[SomeProps] | None). - - Returns: - The Props subclass if found in a list annotation, None otherwise. - """ - origin = get_origin(field_type) - if origin is list: - args = get_args(field_type) - if args: - return _get_props_subclass(args[0]) - - # Handle Union types - check if any union member is a list - if is_union(field_type): - for arg in get_args(field_type): - if arg is not type(None): # Skip None from Optional - list_element = _find_props_in_list_annotation(arg) - if list_element is not None: - return list_element - - return None - - -class PropsField(BaseField[PROPS_FIELD_TYPE]): - """A field for a props class.""" - - def __init__( - self, - default: PROPS_FIELD_TYPE | _MISSING_TYPE = MISSING, - default_factory: Callable[[], PROPS_FIELD_TYPE] | None = None, - annotated_type: type[Any] | _MISSING_TYPE = MISSING, - ) -> None: - """Initialize the field. - - Args: - default: The default value for the field. - default_factory: The default factory for the field. - annotated_type: The annotated type for the field. - """ - super().__init__(default, default_factory, annotated_type) - self._name: str = "" # Will be set by metaclass - - @property - def required(self) -> bool: - """Check if the field is required (for Pydantic compatibility). - - Returns: - True if the field has no default value or factory. - """ - return self.default is MISSING and self.default_factory is None - - @property - def name(self) -> str | None: - """Field name (for Pydantic compatibility). - - Note: This is set by the metaclass when processing fields. - - Returns: - The field name if set, None otherwise. - """ - return getattr(self, "_name", None) - - def get_default(self) -> Any: - """Get the default value (for Pydantic compatibility). - - Returns: - The default value for the field, or None if required. - """ - try: - return self.default_value() - except ValueError: - # Field is required (no default) - return None - - def __repr__(self) -> str: - """Represent the field in a readable format. - - Returns: - The string representation of the field. - """ - annotated_type_str = ( - f", annotated_type={self.annotated_type!r}" - if self.annotated_type is not MISSING - else "" - ) - if self.default is not MISSING: - return f"PropsField(default={self.default!r}{annotated_type_str})" - return ( - f"PropsField(default_factory={self.default_factory!r}{annotated_type_str})" - ) - - -def props_field( - default: PROPS_FIELD_TYPE | _MISSING_TYPE = MISSING, - default_factory: Callable[[], PROPS_FIELD_TYPE] | None = None, -) -> PROPS_FIELD_TYPE: - """Create a field for a props class. - - Args: - default: The default value for the field. - default_factory: The default factory for the field. - - Returns: - The field for the props class. - - Raises: - ValueError: If both default and default_factory are specified. - """ - if default is not MISSING and default_factory is not None: - msg = "cannot specify both default and default_factory" - raise ValueError(msg) - return PropsField( # pyright: ignore [reportReturnType] - default=default, - default_factory=default_factory, - annotated_type=MISSING, - ) - - -@dataclass_transform(field_specifiers=(props_field,)) -class PropsBaseMeta(FieldBasedMeta): - """Meta class for PropsBase.""" - - @classmethod - def _process_annotated_fields( - cls, - namespace: dict[str, Any], - annotations: dict[str, Any], - inherited_fields: dict[str, PropsField], - ) -> dict[str, PropsField]: - own_fields: dict[str, PropsField] = {} - - for key, annotation in annotations.items(): - value = namespace.get(key, MISSING) - - if value is MISSING: - # Field with only annotation, no default value - field = PropsField(annotated_type=annotation, default=None) - elif not isinstance(value, PropsField): - # Field with default value - field = PropsField(annotated_type=annotation, default=value) - else: - # Field is already a PropsField, update annotation - field = PropsField( - annotated_type=annotation, - default=value.default, - default_factory=value.default_factory, - ) - - own_fields[key] = field - - return own_fields - - @classmethod - def _create_field( - cls, - annotated_type: Any, - default: Any = MISSING, - default_factory: Callable[[], Any] | None = None, - ) -> PropsField: - return PropsField( - annotated_type=annotated_type, - default=default, - default_factory=default_factory, - ) - - @classmethod - def _finalize_fields( - cls, - namespace: dict[str, Any], - inherited_fields: dict[str, PropsField], - own_fields: dict[str, PropsField], - ) -> None: - # Call parent implementation - super()._finalize_fields(namespace, inherited_fields, own_fields) - - # Add Pydantic compatibility - namespace["__fields__"] = namespace["_fields"] - - -class PropsBase(metaclass=PropsBaseMeta): - """Base for a class containing props that can be serialized as a JS object.""" - - def __init__(self, **kwargs): - """Initialize the props with field values. - - Args: - **kwargs: The field values to set. - """ - # Set field values from kwargs with nested object instantiation - for key, value in kwargs.items(): - field_info = self.get_fields().get(key) - if field_info: - field_type = field_info.annotated_type - - # Check if this field expects a specific Props type and we got a dict - if isinstance(value, dict): - props_class = _get_props_subclass(field_type) - if props_class is not None: - value = props_class(**value) - - # Check if this field expects a list of Props and we got a list of dicts - elif isinstance(value, list): - element_type = _find_props_in_list_annotation(field_type) - if element_type is not None: - # Convert each dict in the list to the appropriate Props class - value = [ - element_type(**item) if isinstance(item, dict) else item - for item in value - ] - - setattr(self, key, value) - - # Set default values for fields not provided - for field_name, field in self.get_fields().items(): - if field_name not in kwargs: - if field.default is not MISSING: - setattr(self, field_name, field.default) - elif field.default_factory is not None: - setattr(self, field_name, field.default_factory()) - # Note: Fields with no default and no factory remain unset (required fields) - - # Convert EventHandler to EventChain - args_specs = args_specs_from_fields(self.get_fields()) - for handler_name, args_spec in args_specs.items(): - if (handler := getattr(self, handler_name, None)) is not None: - setattr( - self, - handler_name, - EventChain.create( - value=handler, - args_spec=args_spec, - key=handler_name, - ), - ) - - @classmethod - def get_fields(cls) -> dict[str, Any]: - """Get the fields of the object. - - Returns: - The fields of the object. - """ - return getattr(cls, "_fields", {}) - - 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 LiteralObjectVar.create({ - format.to_camel_case(key): value for key, value in self.dict().items() - }).json() - - def dict( - self, - exclude_none: bool = True, - include: set[str] | None = None, - exclude: set[str] | None = None, - **kwargs, - ): - """Convert the object to a dictionary. - - Keys will be converted to camelCase. - By default, None values are excluded (exclude_none=True). - - Args: - exclude_none: Whether to exclude None values. - include: Fields to include in the output. - exclude: Fields to exclude from the output. - **kwargs: Additional keyword arguments (for compatibility). - - Returns: - The object as a dictionary. - """ - result = {} - - for field_name in self.get_fields(): - if hasattr(self, field_name): - value = getattr(self, field_name) - - # Apply include/exclude filters - if include is not None and field_name not in include: - continue - if exclude is not None and field_name in exclude: - continue - - # Apply exclude_none logic - if exclude_none and value is None: - continue - - # Recursively convert nested structures - value = self._convert_to_camel_case( - value, exclude_none, include, exclude - ) - - # Convert key to camelCase - camel_key = format.to_camel_case(field_name) - result[camel_key] = value - - return result - - def _convert_to_camel_case( - self, - value: Any, - exclude_none: bool = True, - include: set[str] | None = None, - exclude: set[str] | None = None, - ) -> Any: - """Recursively convert nested dictionaries and lists to camelCase. - - Args: - value: The value to convert. - exclude_none: Whether to exclude None values. - include: Fields to include in the output. - exclude: Fields to exclude from the output. - - Returns: - The converted value with camelCase keys. - """ - if isinstance(value, PropsBase): - # Convert nested PropsBase objects - return value.dict( - exclude_none=exclude_none, include=include, exclude=exclude - ) - if isinstance(value, dict): - # Convert dictionary keys to camelCase - return { - format.to_camel_case(k): self._convert_to_camel_case( - v, exclude_none, include, exclude - ) - for k, v in value.items() - if not (exclude_none and v is None) - } - if isinstance(value, (list, tuple)): - # Convert list/tuple items recursively - return [ - self._convert_to_camel_case(item, exclude_none, include, exclude) - for item in value - ] - # Return primitive values as-is - return value - - -@serializer(to=dict) -def serialize_props_base(value: PropsBase) -> dict: - """Serialize a PropsBase instance. - - Unlike serialize_base, this preserves callables (lambdas) since they're - needed for AG Grid and other components that process them on the frontend. - - Args: - value: The PropsBase instance to serialize. - - Returns: - Dictionary representation of the PropsBase instance. - """ - return value.dict() - - -class NoExtrasAllowedProps(PropsBase): - """A class that holds props to be passed or applied to a component with no extra props allowed.""" - - def __init__(self, component_name: str | None = None, **kwargs): - """Initialize the props with validation. - - Args: - component_name: The custom name of the component. - kwargs: Kwargs to initialize the props. - - Raises: - InvalidPropValueError: If invalid props are passed on instantiation. - """ - component_name = component_name or type(self).__name__ - - # Validate fields BEFORE setting them - known_fields = set(self.__class__.get_fields().keys()) - provided_fields = set(kwargs.keys()) - invalid_fields = provided_fields - known_fields - - if invalid_fields: - invalid_fields_str = ", ".join(invalid_fields) - supported_props_str = ", ".join(f'"{field}"' for field in known_fields) - msg = f"Invalid prop(s) {invalid_fields_str} for {component_name!r}. Supported props are {supported_props_str}" - raise InvalidPropValueError(msg) - - # Use parent class initialization after validation - super().__init__(**kwargs) +from reflex_core.components.props import * diff --git a/reflex/components/tags/__init__.py b/reflex/components/tags/__init__.py index 993da11fe69..7ac42b86898 100644 --- a/reflex/components/tags/__init__.py +++ b/reflex/components/tags/__init__.py @@ -1,6 +1,3 @@ -"""Representations for React tags.""" +"""Re-export from reflex_core.""" -from .cond_tag import CondTag -from .iter_tag import IterTag -from .match_tag import MatchTag -from .tag import Tag +from reflex_core.components.tags import * diff --git a/reflex/components/tags/cond_tag.py b/reflex/components/tags/cond_tag.py index 297f1c0c461..af4b393cf7d 100644 --- a/reflex/components/tags/cond_tag.py +++ b/reflex/components/tags/cond_tag.py @@ -1,31 +1,3 @@ -"""Tag to conditionally render components.""" +"""Re-export from reflex_core.""" -import dataclasses -from collections.abc import Iterator, Mapping -from typing import Any - -from reflex.components.tags.tag import Tag - - -@dataclasses.dataclass(frozen=True, kw_only=True) -class CondTag(Tag): - """A conditional tag.""" - - # The condition to determine which component to render. - cond_state: str - - # The code to render if the condition is true. - true_value: Mapping - - # The code to render if the condition is false. - false_value: Mapping | None = None - - def __iter__(self) -> Iterator[tuple[str, Any]]: - """Iterate over the tag's attributes. - - Yields: - An iterator over the tag's attributes. - """ - yield ("cond_state", self.cond_state) - yield ("true_value", self.true_value) - yield ("false_value", self.false_value) +from reflex_core.components.tags.cond_tag import * diff --git a/reflex/components/tags/iter_tag.py b/reflex/components/tags/iter_tag.py index 64b7e1e9ad2..c26ed69b3e3 100644 --- a/reflex/components/tags/iter_tag.py +++ b/reflex/components/tags/iter_tag.py @@ -1,116 +1,3 @@ -"""Tag to loop through a list of components.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import dataclasses -import inspect -from collections.abc import Callable, Iterable -from typing import TYPE_CHECKING - -from reflex.components.tags.tag import Tag -from reflex.utils.types import GenericType -from reflex.vars import LiteralArrayVar, Var, get_unique_variable_name -from reflex.vars.sequence import _determine_value_of_array_index - -if TYPE_CHECKING: - from reflex.components.component import Component - - -@dataclasses.dataclass(frozen=True) -class IterTag(Tag): - """An iterator tag.""" - - # The var to iterate over. - iterable: Var[Iterable] = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]) - ) - - # The component render function for each item in the iterable. - render_fn: Callable = dataclasses.field(default_factory=lambda: lambda x: x) - - # The name of the arg var. - arg_var_name: str = dataclasses.field(default_factory=get_unique_variable_name) - - # The name of the index var. - index_var_name: str = dataclasses.field(default_factory=get_unique_variable_name) - - def get_iterable_var_type(self) -> GenericType: - """Get the type of the iterable var. - - Returns: - The type of the iterable var. - """ - return _determine_value_of_array_index(self.iterable._var_type) - - def get_index_var(self) -> Var: - """Get the index var for the tag (with curly braces). - - This is used to reference the index var within the tag. - - Returns: - The index var. - """ - return Var( - _js_expr=self.index_var_name, - _var_type=int, - ).guess_type() - - def get_arg_var(self) -> Var: - """Get the arg var for the tag (with curly braces). - - This is used to reference the arg var within the tag. - - Returns: - The arg var. - """ - return Var( - _js_expr=self.arg_var_name, - _var_type=self.get_iterable_var_type(), - ).guess_type() - - def render_component(self) -> Component: - """Render the component. - - Returns: - The rendered component. - - Raises: - ValueError: If the render function takes more than 2 arguments. - ValueError: If the render function doesn't return a component. - """ - # Import here to avoid circular imports. - from reflex.compiler.compiler import _into_component_once - from reflex.components.base.fragment import Fragment - from reflex.components.core.cond import Cond - from reflex.components.core.foreach import Foreach - - # Get the render function arguments. - args = inspect.getfullargspec(self.render_fn).args - arg = self.get_arg_var() - index = self.get_index_var() - - if len(args) == 1: - # If the render function doesn't take the index as an argument. - component = self.render_fn(arg) - else: - # If the render function takes the index as an argument. - if len(args) != 2: - msg = "The render function must take 2 arguments." - raise ValueError(msg) - component = self.render_fn(arg, index) - - # Nested foreach components or cond must be wrapped in fragments. - if isinstance(component, (Foreach, Cond)): - component = Fragment.create(component) - - component = _into_component_once(component) - - if component is None: - msg = "The render function must return a component." - raise ValueError(msg) - - # Set the component key. - if component.key is None: - component.key = index - - return component +from reflex_core.components.tags.iter_tag import * diff --git a/reflex/components/tags/match_tag.py b/reflex/components/tags/match_tag.py index 723654fd487..fc26bd1b38e 100644 --- a/reflex/components/tags/match_tag.py +++ b/reflex/components/tags/match_tag.py @@ -1,31 +1,3 @@ -"""Tag to conditionally match cases.""" +"""Re-export from reflex_core.""" -import dataclasses -from collections.abc import Iterator, Mapping, Sequence -from typing import Any - -from reflex.components.tags.tag import Tag - - -@dataclasses.dataclass(frozen=True, kw_only=True) -class MatchTag(Tag): - """A match tag.""" - - # The condition to determine which case to match. - cond: str - - # The list of match cases to be matched. - match_cases: Sequence[tuple[Sequence[str], Mapping]] - - # The catchall case to match. - default: Any - - def __iter__(self) -> Iterator[tuple[str, Any]]: - """Iterate over the tag's attributes. - - Yields: - An iterator over the tag's attributes. - """ - yield ("cond", self.cond) - yield ("match_cases", self.match_cases) - yield ("default", self.default) +from reflex_core.components.tags.match_tag import * diff --git a/reflex/components/tags/tag.py b/reflex/components/tags/tag.py index f73de5860cf..61ae1502fcb 100644 --- a/reflex/components/tags/tag.py +++ b/reflex/components/tags/tag.py @@ -1,137 +1,3 @@ -"""A React tag.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import dataclasses -from collections.abc import Iterator, Mapping, Sequence -from typing import Any - -from reflex.event import EventChain -from reflex.utils import format -from reflex.vars.base import LiteralVar, Var - - -def render_prop(value: Any) -> Any: - """Render the prop. - - Args: - value: The value to render. - - Returns: - The rendered value. - """ - from reflex.components.component import BaseComponent - - if isinstance(value, BaseComponent): - return value.render() - if isinstance(value, Sequence) and not isinstance(value, str): - return [render_prop(v) for v in value] - if callable(value) and not isinstance(value, Var): - return None - return value - - -@dataclasses.dataclass(frozen=True) -class Tag: - """A React tag.""" - - # The name of the tag. - name: str = "" - - # The props of the tag. - props: Mapping[str, Any] = dataclasses.field(default_factory=dict) - - # Special props that aren't key value pairs. - special_props: Sequence[Var] = dataclasses.field(default_factory=list) - - # The children components. - children: Sequence[Any] = dataclasses.field(default_factory=list) - - def format_props(self) -> list[str]: - """Format the tag's props. - - Returns: - The formatted props list. - """ - return format.format_props(*self.special_props, **self.props) - - def set(self, **kwargs: Any): - """Return a new tag with the given fields set. - - Args: - **kwargs: The fields to set. - - Returns: - The tag with the fields set. - """ - return dataclasses.replace(self, **kwargs) - - def __iter__(self) -> Iterator[tuple[str, Any]]: - """Iterate over the tag's fields. - - Yields: - tuple[str, Any]: The field name and value. - """ - for field in dataclasses.fields(self): - if field.name == "props": - yield "props", self.format_props() - elif field.name != "special_props": - rendered_value = render_prop(getattr(self, field.name)) - if rendered_value is not None: - yield field.name, rendered_value - - def add_props(self, **kwargs: Any | None) -> Tag: - """Return a new tag with the given props added. - - Args: - **kwargs: The props to add. - - Returns: - The tag with the props added. - """ - return dataclasses.replace( - self, - props={ - **self.props, - **{ - format.to_camel_case(name, treat_hyphens_as_underscores=False): ( - prop - if isinstance(prop, (EventChain, Mapping)) - else LiteralVar.create(prop) - ) - for name, prop in kwargs.items() - if self.is_valid_prop(prop) - }, - }, - ) - - def remove_props(self, *args: str) -> Tag: - """Return a new tag with the given props removed. - - Args: - *args: The names of the props to remove. - - Returns: - The tag with the props removed. - """ - formatted_args = [format.to_camel_case(arg) for arg in args] - return dataclasses.replace( - self, - props={ - name: value - for name, value in self.props.items() - if name not in formatted_args - }, - ) - - @staticmethod - def is_valid_prop(prop: Var | None) -> bool: - """Check if the prop is valid. - - Args: - prop: The prop to check. - - Returns: - Whether the prop is valid. - """ - return prop is not None and not (isinstance(prop, dict) and len(prop) == 0) +from reflex_core.components.tags.tag import * diff --git a/reflex/components/tags/tagless.py b/reflex/components/tags/tagless.py index 3d8db827d88..7e40f8e3ee0 100644 --- a/reflex/components/tags/tagless.py +++ b/reflex/components/tags/tagless.py @@ -1,36 +1,3 @@ -"""A tag with no tag.""" +"""Re-export from reflex_core.""" -import dataclasses - -from reflex.components.tags import Tag -from reflex.utils import format - - -@dataclasses.dataclass(frozen=True, kw_only=True) -class Tagless(Tag): - """A tag with no tag.""" - - # The inner contents of the tag. - contents: str - - def __str__(self) -> str: - """Return the string representation of the tag. - - Returns: - The string representation of the tag. - """ - out = self.contents - space = format.wrap(" ", "{") - if len(self.contents) > 0 and self.contents[0] == " ": - out = space + out - if len(self.contents) > 0 and self.contents[-1] == " ": - out = out + space - return out - - def __iter__(self): - """Iterate over the tag's fields. - - Yields: - tuple[str, Any]: The field name and value. - """ - yield "contents", self.contents +from reflex_core.components.tags.tagless import * diff --git a/reflex/config.py b/reflex/config.py index 56e23fd13a0..76a1e9cb1d1 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -1,643 +1,3 @@ -"""The Reflex config.""" +"""Re-export from reflex_core.config.""" -import dataclasses -import importlib -import os -import sys -import threading -import urllib.parse -from collections.abc import Sequence -from importlib.util import find_spec -from pathlib import Path -from types import ModuleType -from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal - -from reflex import constants -from reflex.constants.base import LogLevel -from reflex.environment import EnvironmentVariables as EnvironmentVariables -from reflex.environment import EnvVar as EnvVar -from reflex.environment import ( - ExistingPath, - SequenceOptions, - _load_dotenv_from_files, - _paths_from_env_files, - interpret_env_var_value, -) -from reflex.environment import env_var as env_var -from reflex.environment import environment as environment -from reflex.plugins import Plugin -from reflex.plugins.sitemap import SitemapPlugin -from reflex.utils import console -from reflex.utils.exceptions import ConfigError - - -@dataclasses.dataclass(kw_only=True) -class DBConfig: - """Database config.""" - - engine: str - username: str | None = "" - password: str | None = "" - host: str | None = "" - port: int | None = None - database: str - - @classmethod - def postgresql( - cls, - database: str, - username: str, - password: str | None = None, - host: str | None = None, - port: int | None = 5432, - ) -> "DBConfig": - """Create an instance with postgresql engine. - - Args: - database: Database name. - username: Database username. - password: Database password. - host: Database host. - port: Database port. - - Returns: - DBConfig instance. - """ - return cls( - engine="postgresql", - username=username, - password=password, - host=host, - port=port, - database=database, - ) - - @classmethod - def postgresql_psycopg( - cls, - database: str, - username: str, - password: str | None = None, - host: str | None = None, - port: int | None = 5432, - ) -> "DBConfig": - """Create an instance with postgresql+psycopg engine. - - Args: - database: Database name. - username: Database username. - password: Database password. - host: Database host. - port: Database port. - - Returns: - DBConfig instance. - """ - return cls( - engine="postgresql+psycopg", - username=username, - password=password, - host=host, - port=port, - database=database, - ) - - @classmethod - def sqlite( - cls, - database: str, - ) -> "DBConfig": - """Create an instance with sqlite engine. - - Args: - database: Database name. - - Returns: - DBConfig instance. - """ - return cls( - engine="sqlite", - database=database, - ) - - def get_url(self) -> str: - """Get database URL. - - Returns: - The database URL. - """ - host = ( - f"{self.host}:{self.port}" if self.host and self.port else self.host or "" - ) - username = urllib.parse.quote_plus(self.username) if self.username else "" - password = urllib.parse.quote_plus(self.password) if self.password else "" - - if username: - path = f"{username}:{password}@{host}" if password else f"{username}@{host}" - else: - path = f"{host}" - - return f"{self.engine}://{path}/{self.database}" - - -# These vars are not logged because they may contain sensitive information. -_sensitive_env_vars = {"DB_URL", "ASYNC_DB_URL", "REDIS_URL"} - - -@dataclasses.dataclass(kw_only=True) -class BaseConfig: - """Base config for the Reflex app. - - Attributes: - app_name: The name of the app (should match the name of the app directory). - app_module_import: The path to the app module. - loglevel: The log level to use. - frontend_port: The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken. - frontend_path: The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app - backend_port: The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken. - api_url: The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production. - deploy_url: The url the frontend will be hosted on. - backend_host: The url the backend will be hosted on. - db_url: The database url used by rx.Model. - async_db_url: The async database url used by rx.Model. - redis_url: The redis url. - telemetry_enabled: Telemetry opt-in. - bun_path: The bun path. - static_page_generation_timeout: Timeout to do a production build of a frontend page. - cors_allowed_origins: Comma separated list of origins that are allowed to connect to the backend API. - vite_allowed_hosts: Allowed hosts for the Vite dev server. Set to True to allow all hosts, or provide a list of hostnames (e.g. ["myservice.local"]) to allow specific ones. Prevents 403 errors in Docker, Codespaces, reverse proxies, etc. - react_strict_mode: Whether to use React strict mode. - frontend_packages: Additional frontend packages to install. - state_manager_mode: Indicate which type of state manager to use. - redis_lock_expiration: Maximum expiration lock time for redis state manager. - redis_lock_warning_threshold: Maximum lock time before warning for redis state manager. - redis_token_expiration: Token expiration time for redis state manager. - env_file: Path to file containing key-values pairs to override in the environment; Dotenv format. - state_auto_setters: Whether to automatically create setters for state base vars. - show_built_with_reflex: Whether to display the sticky "Built with Reflex" badge on all pages. - is_reflex_cloud: Whether the app is running in the reflex cloud environment. - extra_overlay_function: Extra overlay function to run after the app is built. Formatted such that `from path_0.path_1... import path[-1]`, and calling it with no arguments would work. For example, "reflex.components.moment.moment". - plugins: List of plugins to use in the app. - disable_plugins: List of plugin types to disable in the app. - transport: The transport method for client-server communication. - """ - - app_name: str - - app_module_import: str | None = None - - loglevel: constants.LogLevel = constants.LogLevel.DEFAULT - - frontend_port: int | None = None - - frontend_path: str = "" - - backend_port: int | None = None - - api_url: str = f"http://localhost:{constants.DefaultPorts.BACKEND_PORT}" - - deploy_url: str | None = f"http://localhost:{constants.DefaultPorts.FRONTEND_PORT}" - - backend_host: str = "0.0.0.0" - - db_url: str | None = "sqlite:///reflex.db" - - async_db_url: str | None = None - - redis_url: str | None = None - - telemetry_enabled: bool = True - - bun_path: ExistingPath = constants.Bun.DEFAULT_PATH - - static_page_generation_timeout: int = 60 - - cors_allowed_origins: Annotated[ - Sequence[str], - SequenceOptions(delimiter=","), - ] = dataclasses.field(default=("*",)) - - vite_allowed_hosts: bool | list[str] = False - - react_strict_mode: bool = True - - frontend_packages: list[str] = dataclasses.field(default_factory=list) - - state_manager_mode: constants.StateManagerMode = constants.StateManagerMode.DISK - - redis_lock_expiration: int = constants.Expiration.LOCK - - redis_lock_warning_threshold: int = constants.Expiration.LOCK_WARNING_THRESHOLD - - redis_token_expiration: int = constants.Expiration.TOKEN - - # Attributes that were explicitly set by the user. - _non_default_attributes: set[str] = dataclasses.field( - default_factory=set, init=False - ) - - env_file: str | None = None - - state_auto_setters: bool | None = None - - show_built_with_reflex: bool | None = None - - is_reflex_cloud: bool = False - - extra_overlay_function: str | None = None - - plugins: list[Plugin] = dataclasses.field(default_factory=list) - - disable_plugins: list[type[Plugin]] = dataclasses.field(default_factory=list) - - transport: Literal["websocket", "polling"] = "websocket" - - # Whether to skip plugin checks. - _skip_plugins_checks: bool = dataclasses.field(default=False, repr=False) - - _prefixes: ClassVar[list[str]] = ["REFLEX_"] - - -_PLUGINS_ENABLED_BY_DEFAULT = [ - SitemapPlugin, -] - - -@dataclasses.dataclass(kw_only=True, init=False) -class Config(BaseConfig): - """Configuration class for Reflex applications. - - The config defines runtime settings for your app including server ports, database connections, - frontend packages, and deployment settings. - - By default, the config is defined in an `rxconfig.py` file in the root of your app: - - ```python - # rxconfig.py - import reflex as rx - - config = rx.Config( - app_name="myapp", - # Server configuration - frontend_port=3000, - backend_port=8000, - # Database - db_url="postgresql://user:pass@localhost:5432/mydb", - # Additional frontend packages - frontend_packages=["react-icons"], - # CORS settings for production - cors_allowed_origins=["https://mydomain.com"], - ) - ``` - - ## Environment Variable Overrides - - Any config value can be overridden by setting an environment variable with the `REFLEX_` - prefix and the parameter name in uppercase: - - ```bash - REFLEX_DB_URL="postgresql://user:pass@localhost/db" reflex run - REFLEX_FRONTEND_PORT=3001 reflex run - ``` - - ## Key Configuration Areas - - - **App Settings**: `app_name`, `loglevel`, `telemetry_enabled` - - **Server**: `frontend_port`, `backend_port`, `api_url`, `cors_allowed_origins` - - **Database**: `db_url`, `async_db_url`, `redis_url` - - **Frontend**: `frontend_packages`, `react_strict_mode` - - **State Management**: `state_manager_mode`, `state_auto_setters` - - **Plugins**: `plugins`, `disable_plugins` - - See the [configuration docs](https://reflex.dev/docs/advanced-onboarding/configuration) for complete details on all available options. - """ - - # Track whether the app name has already been validated for this Config instance. - _app_name_is_valid: bool = dataclasses.field(default=False, repr=False) - - def _post_init(self, **kwargs): - """Post-initialization method to set up the config. - - This method is called after the config is initialized. It sets up the - environment variables, updates the config from the environment, and - replaces default URLs if ports were set. - - Args: - **kwargs: The kwargs passed to the Pydantic init method. - - Raises: - ConfigError: If some values in the config are invalid. - """ - class_fields = self.class_fields() - for key, value in kwargs.items(): - if key not in class_fields: - setattr(self, key, value) - - # Clean up this code when we remove plain envvar in 0.8.0 - env_loglevel = os.environ.get("REFLEX_LOGLEVEL") - if env_loglevel is not None: - env_loglevel = LogLevel(env_loglevel.lower()) - if env_loglevel or self.loglevel != LogLevel.DEFAULT: - console.set_log_level(env_loglevel or self.loglevel) - - # Update the config from environment variables. - env_kwargs = self.update_from_env() - for key, env_value in env_kwargs.items(): - setattr(self, key, env_value) - - # Normalize disable_plugins: convert strings and Plugin subclasses to instances. - self._normalize_disable_plugins() - - # Add builtin plugins if not disabled. - if not self._skip_plugins_checks: - self._add_builtin_plugins() - - # Update default URLs if ports were set - kwargs.update(env_kwargs) - self._non_default_attributes = set(kwargs.keys()) - self._replace_defaults(**kwargs) - - if ( - self.state_manager_mode == constants.StateManagerMode.REDIS - and not self.redis_url - ): - msg = f"{self._prefixes[0]}REDIS_URL is required when using the redis state manager." - raise ConfigError(msg) - - def _normalize_disable_plugins(self): - """Normalize disable_plugins list entries to Plugin subclasses. - - Handles backward compatibility by converting strings (fully qualified - import paths) and Plugin instances to their associated classes. - """ - normalized: list[type[Plugin]] = [] - for entry in self.disable_plugins: - if isinstance(entry, type) and issubclass(entry, Plugin): - normalized.append(entry) - elif isinstance(entry, Plugin): - normalized.append(type(entry)) - elif isinstance(entry, str): - console.deprecate( - feature_name="Passing strings to disable_plugins", - reason="pass Plugin classes directly instead, e.g. disable_plugins=[SitemapPlugin]", - deprecation_version="0.8.28", - removal_version="0.9.0", - ) - try: - from reflex.environment import interpret_plugin_class_env - - normalized.append( - interpret_plugin_class_env(entry, "disable_plugins") - ) - except Exception: - console.warn( - f"Failed to import plugin from string {entry!r} in disable_plugins. " - "Please pass Plugin subclasses directly.", - ) - else: - console.warn( - f"reflex.Config.disable_plugins should contain Plugin subclasses, but got {entry!r}.", - ) - self.disable_plugins = normalized - - def _add_builtin_plugins(self): - """Add the builtin plugins to the config.""" - for plugin in _PLUGINS_ENABLED_BY_DEFAULT: - plugin_name = plugin.__module__ + "." + plugin.__qualname__ - if plugin not in self.disable_plugins: - if not any(isinstance(p, plugin) for p in self.plugins): - console.warn( - f"`{plugin_name}` plugin is enabled by default, but not explicitly added to the config. " - "If you want to use it, please add it to the `plugins` list in your config inside of `rxconfig.py`. " - f"To disable this plugin, add `{plugin.__name__}` to the `disable_plugins` list.", - ) - self.plugins.append(plugin()) - else: - if any(isinstance(p, plugin) for p in self.plugins): - console.warn( - f"`{plugin_name}` is disabled in the config, but it is still present in the `plugins` list. " - "Please remove it from the `plugins` list in your config inside of `rxconfig.py`.", - ) - - for disabled_plugin in self.disable_plugins: - if disabled_plugin not in _PLUGINS_ENABLED_BY_DEFAULT: - console.warn( - f"`{disabled_plugin!r}` is disabled in the config, but it is not a built-in plugin. " - "Please remove it from the `disable_plugins` list in your config inside of `rxconfig.py`.", - ) - - @classmethod - def class_fields(cls) -> set[str]: - """Get the fields of the config class. - - Returns: - The fields of the config class. - """ - return {field.name for field in dataclasses.fields(cls)} - - if not TYPE_CHECKING: - - def __init__(self, **kwargs): - """Initialize the config values. - - Args: - **kwargs: The kwargs to pass to the Pydantic init method. - - # noqa: DAR101 self - """ - class_fields = self.class_fields() - super().__init__(**{k: v for k, v in kwargs.items() if k in class_fields}) - self._post_init(**kwargs) - - def json(self) -> str: - """Get the config as a JSON string. - - Returns: - The config as a JSON string. - """ - import json - - from reflex.utils.serializers import serialize - - return json.dumps(self, default=serialize) - - @property - def app_module(self) -> ModuleType | None: - """Return the app module if `app_module_import` is set. - - Returns: - The app module. - """ - return ( - importlib.import_module(self.app_module_import) - if self.app_module_import - else None - ) - - @property - def module(self) -> str: - """Get the module name of the app. - - Returns: - The module name. - """ - if self.app_module_import is not None: - return self.app_module_import - return self.app_name + "." + self.app_name - - def update_from_env(self) -> dict[str, Any]: - """Update the config values based on set environment variables. - If there is a set env_file, it is loaded first. - - Returns: - The updated config values. - """ - if self.env_file: - _load_dotenv_from_files(_paths_from_env_files(self.env_file)) - - updated_values = {} - # Iterate over the fields. - for field in dataclasses.fields(self): - # The env var name is the key in uppercase. - environment_variable = None - for prefix in self._prefixes: - if environment_variable := os.environ.get( - f"{prefix}{field.name.upper()}" - ): - break - - # If the env var is set, override the config value. - if environment_variable and environment_variable.strip(): - # Interpret the value. - value = interpret_env_var_value( - environment_variable, - field.type, - field.name, - ) - - # Set the value. - updated_values[field.name] = value - - if field.name.upper() in _sensitive_env_vars: - environment_variable = "***" - - if value != getattr(self, field.name): - console.debug( - f"Overriding config value {field.name} with env var {field.name.upper()}={environment_variable}", - dedupe=True, - ) - return updated_values - - def get_event_namespace(self) -> str: - """Get the path that the backend Websocket server lists on. - - Returns: - The namespace for websocket. - """ - event_url = constants.Endpoint.EVENT.get_url() - return urllib.parse.urlsplit(event_url).path - - def _replace_defaults(self, **kwargs): - """Replace formatted defaults when the caller provides updates. - - Args: - **kwargs: The kwargs passed to the config or from the env. - """ - if "api_url" not in self._non_default_attributes and "backend_port" in kwargs: - self.api_url = f"http://localhost:{kwargs['backend_port']}" - - if ( - "deploy_url" not in self._non_default_attributes - and "frontend_port" in kwargs - ): - self.deploy_url = f"http://localhost:{kwargs['frontend_port']}" - - if "api_url" not in self._non_default_attributes: - # If running in Github Codespaces, override API_URL - codespace_name = os.getenv("CODESPACE_NAME") - github_codespaces_port_forwarding_domain = os.getenv( - "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" - ) - # If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port - replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN") - backend_port = kwargs.get("backend_port", self.backend_port) - if codespace_name and github_codespaces_port_forwarding_domain: - self.api_url = ( - f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}" - f".{github_codespaces_port_forwarding_domain}" - ) - elif replit_dev_domain and backend_port: - self.api_url = f"https://{replit_dev_domain}:{backend_port}" - - def _set_persistent(self, **kwargs): - """Set values in this config and in the environment so they persist into subprocess. - - Args: - **kwargs: The kwargs passed to the config. - """ - for key, value in kwargs.items(): - if value is not None: - os.environ[self._prefixes[0] + key.upper()] = str(value) - setattr(self, key, value) - self._non_default_attributes.update(kwargs) - self._replace_defaults(**kwargs) - - -def _get_config() -> Config: - """Get the app config. - - Returns: - The app config. - """ - # only import the module if it exists. If a module spec exists then - # the module exists. - spec = find_spec(constants.Config.MODULE) - if not spec: - # we need this condition to ensure that a ModuleNotFound error is not thrown when - # running unit/integration tests or during `reflex init`. - return Config(app_name="", _skip_plugins_checks=True) - rxconfig = importlib.import_module(constants.Config.MODULE) - return rxconfig.config - - -# Protect sys.path from concurrent modification -_config_lock = threading.RLock() - - -def get_config(reload: bool = False) -> Config: - """Get the app config. - - Args: - reload: Re-import the rxconfig module from disk - - Returns: - The app config. - """ - cached_rxconfig = sys.modules.get(constants.Config.MODULE, None) - if cached_rxconfig is not None: - if reload: - # Remove any cached module when `reload` is requested. - del sys.modules[constants.Config.MODULE] - else: - return cached_rxconfig.config - - with _config_lock: - orig_sys_path = sys.path.copy() - sys.path.clear() - sys.path.append(str(Path.cwd())) - try: - # Try to import the module with only the current directory in the path. - return _get_config() - except Exception: - # If the module import fails, try to import with the original sys.path. - sys.path.extend(orig_sys_path) - return _get_config() - finally: - # Find any entries added to sys.path by rxconfig.py itself. - extra_paths = [ - p for p in sys.path if p not in orig_sys_path and p != str(Path.cwd()) - ] - # Restore the original sys.path. - sys.path.clear() - sys.path.extend(extra_paths) - sys.path.extend(orig_sys_path) +from reflex_core.config import * diff --git a/reflex/constants/base.py b/reflex/constants/base.py index c8a9a1dfde5..cb4eb5d32ea 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -1,278 +1,3 @@ -"""Base file for constants that don't fit any other categories.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import platform -from enum import Enum -from importlib import metadata -from pathlib import Path -from types import SimpleNamespace -from typing import Literal - -from platformdirs import PlatformDirs - -IS_WINDOWS = platform.system() == "Windows" -IS_MACOS = platform.system() == "Darwin" -IS_LINUX = platform.system() == "Linux" - - -class Dirs(SimpleNamespace): - """Various directories/paths used by Reflex.""" - - # The frontend directories in a project. - # The web folder where the frontend app is compiled to. - WEB = ".web" - # The directory where uploaded files are stored. - UPLOADED_FILES = "uploaded_files" - # The name of the assets directory. - APP_ASSETS = "assets" - # The name of the assets directory for external resources (a subfolder of APP_ASSETS). - EXTERNAL_APP_ASSETS = "external" - # The name of the utils file. - UTILS = "utils" - # The name of the state file. - STATE_PATH = UTILS + "/state" - # The name of the components file. - COMPONENTS_PATH = UTILS + "/components" - # The name of the contexts file. - CONTEXTS_PATH = UTILS + "/context" - # The name of the output directory. - BUILD_DIR = "build" - # The name of the static files directory. - STATIC = BUILD_DIR + "/client" - # The name of the public html directory served at "/" - PUBLIC = "public" - # The directory where styles are located. - STYLES = "styles" - # The name of the pages directory. - PAGES = "app" - # The name of the routes directory. - ROUTES = "routes" - # The name of the env json file. - ENV_JSON = "env.json" - # The name of the reflex json file. - REFLEX_JSON = "reflex.json" - # The name of the postcss config file. - POSTCSS_JS = "postcss.config.js" - # The name of the states directory. - STATES = ".states" - # Where compilation artifacts for the backend are stored. - BACKEND = "backend" - # JSON-encoded list of page routes that need to be evaluated on the backend. - STATEFUL_PAGES = "stateful_pages.json" - # Marker file indicating that upload component was used in the frontend. - UPLOAD_IS_USED = "upload_is_used" - - -def _reflex_version() -> str: - """Get the Reflex version. - - Returns: - The Reflex version. - """ - try: - return metadata.version("reflex") - except metadata.PackageNotFoundError: - return "unknown" - - -class Reflex(SimpleNamespace): - """Base constants concerning Reflex.""" - - # App names and versions. - # The name of the Reflex package. - MODULE_NAME = "reflex" - # The current version of Reflex. - VERSION = _reflex_version() - - # The reflex json file. - JSON = "reflex.json" - - # Files and directories used to init a new project. - # The directory to store reflex dependencies. - # on windows, we use C:/Users//AppData/Local/reflex. - # on macOS, we use ~/Library/Application Support/reflex. - # on linux, we use ~/.local/share/reflex. - # If user sets REFLEX_DIR envroment variable use that instead. - DIR = PlatformDirs(MODULE_NAME, False).user_data_path - - LOGS_DIR = DIR / "logs" - - # The root directory of the reflex library. - ROOT_DIR = Path(__file__).parents[2] - - RELEASES_URL = "https://api.github.com/repos/reflex-dev/templates/releases" - - # The reflex stylesheet language supported - STYLESHEETS_SUPPORTED = ["css", "sass", "scss"] - - -class ReflexHostingCLI(SimpleNamespace): - """Base constants concerning Reflex Hosting CLI.""" - - # The name of the Reflex Hosting CLI package. - MODULE_NAME = "reflex-hosting-cli" - - -class Templates(SimpleNamespace): - """Constants related to Templates.""" - - # The default template - DEFAULT = "blank" - - # The AI template - AI = "ai" - - # The option for the user to choose a remote template. - CHOOSE_TEMPLATES = "choose-templates" - - # The URL to find reflex templates. - REFLEX_TEMPLATES_URL = ( - "https://reflex.dev/docs/getting-started/open-source-templates/" - ) - - # The reflex.build frontend host - REFLEX_BUILD_FRONTEND = "https://build.reflex.dev" - - # The reflex.build frontend with referrer - REFLEX_BUILD_FRONTEND_WITH_REFERRER = ( - f"{REFLEX_BUILD_FRONTEND}/?utm_source=reflex_cli" - ) - - class Dirs(SimpleNamespace): - """Folders used by the template system of Reflex.""" - - # The template directory used during reflex init. - BASE = Reflex.ROOT_DIR / Reflex.MODULE_NAME / ".templates" - # The web subdirectory of the template directory. - WEB_TEMPLATE = BASE / "web" - # Where the code for the templates is stored. - CODE = "code" - - -class Javascript(SimpleNamespace): - """Constants related to Javascript.""" - - # The node modules directory. - NODE_MODULES = "node_modules" - - -class ReactRouter(Javascript): - """Constants related to React Router.""" - - # The react router config file - CONFIG_FILE = "react-router.config.js" - - # The associated Vite config file - VITE_CONFIG_FILE = "vite.config.js" - - # Regex to check for message displayed when frontend comes up - DEV_FRONTEND_LISTENING_REGEX = r"Local:[\s]+" - - # Regex to pattern the route path in the config file - # INFO Accepting connections at http://localhost:3000 - PROD_FRONTEND_LISTENING_REGEX = r"Accepting connections at[\s]+" - - FRONTEND_LISTENING_REGEX = ( - rf"(?:{DEV_FRONTEND_LISTENING_REGEX}|{PROD_FRONTEND_LISTENING_REGEX})(.*)" - ) - - SPA_FALLBACK = "__spa-fallback.html" - - -# Color mode variables -class ColorMode(SimpleNamespace): - """Constants related to ColorMode.""" - - NAME = "rawColorMode" - RESOLVED_NAME = "resolvedColorMode" - USE = "useColorMode" - TOGGLE = "toggleColorMode" - SET = "setColorMode" - - -LITERAL_ENV = Literal["dev", "prod"] - - -# Env modes -class Env(str, Enum): - """The environment modes.""" - - DEV = "dev" - PROD = "prod" - - -# Log levels -class LogLevel(str, Enum): - """The log levels.""" - - DEBUG = "debug" - DEFAULT = "default" - INFO = "info" - WARNING = "warning" - ERROR = "error" - CRITICAL = "critical" - - @classmethod - def from_string(cls, level: str | None) -> LogLevel | None: - """Convert a string to a log level. - - Args: - level: The log level as a string. - - Returns: - The log level. - """ - if not level: - return None - try: - return LogLevel[level.upper()] - except KeyError: - return None - - def __le__(self, other: LogLevel) -> bool: - """Compare log levels. - - Args: - other: The other log level. - - Returns: - True if the log level is less than or equal to the other log level. - """ - levels = list(LogLevel) - return levels.index(self) <= levels.index(other) - - def subprocess_level(self): - """Return the log level for the subprocess. - - Returns: - The log level for the subprocess - """ - return self if self != LogLevel.DEFAULT else LogLevel.WARNING - - -# Server socket configuration variables -POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000 - - -class Ping(SimpleNamespace): - """PING constants.""" - - # The 'ping' interval - INTERVAL = 25 - # The 'ping' timeout - TIMEOUT = 120 - - -# Keys in the client_side_storage dict -COOKIES = "cookies" -LOCAL_STORAGE = "local_storage" -SESSION_STORAGE = "session_storage" - -# Testing variables. -# Testing os env set by pytest when running a test case. -PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST" -APP_HARNESS_FLAG = "APP_HARNESS_FLAG" - -REFLEX_VAR_OPENING_TAG = "" -REFLEX_VAR_CLOSING_TAG = "" +from reflex_core.constants.base import * diff --git a/reflex/constants/colors.py b/reflex/constants/colors.py index f769f8099b6..8810ebc5429 100644 --- a/reflex/constants/colors.py +++ b/reflex/constants/colors.py @@ -1,99 +1,3 @@ -"""The colors used in Reflex are a wrapper around https://www.radix-ui.com/colors.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING, Literal, get_args - -if TYPE_CHECKING: - from reflex.vars import Var - -ColorType = Literal[ - "gray", - "mauve", - "slate", - "sage", - "olive", - "sand", - "tomato", - "red", - "ruby", - "crimson", - "pink", - "plum", - "purple", - "violet", - "iris", - "indigo", - "blue", - "cyan", - "teal", - "jade", - "green", - "grass", - "brown", - "orange", - "sky", - "mint", - "lime", - "yellow", - "amber", - "gold", - "bronze", - "accent", - "black", - "white", -] - -COLORS = frozenset(get_args(ColorType)) - -ShadeType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] -MIN_SHADE_VALUE = 1 -MAX_SHADE_VALUE = 12 - - -def format_color( - color: ColorType | Var[str], shade: ShadeType | Var[int], alpha: bool | Var[bool] -) -> str: - """Format a color as a CSS color string. - - Args: - color: The color to use. - shade: The shade of the color to use. - alpha: Whether to use the alpha variant of the color. - - Returns: - The formatted color. - """ - if isinstance(alpha, bool): - return f"var(--{color}-{'a' if alpha else ''}{shade})" - - from reflex.components.core import cond - - alpha_var = cond(alpha, "a", "") - return f"var(--{color}-{alpha_var}{shade})" - - -@dataclass -class Color: - """A color in the Reflex color palette.""" - - # The color palette to use - color: ColorType | Var[str] - - # The shade of the color to use - shade: ShadeType | Var[int] = 7 - - # Whether to use the alpha variant of the color - alpha: bool | Var[bool] = False - - def __format__(self, format_spec: str) -> str: - """Format the color as a CSS color string. - - Args: - format_spec: The format specifier to use. - - Returns: - The formatted color. - """ - return format_color(self.color, self.shade, self.alpha) +from reflex_core.constants.colors import * diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index 0926e57f320..5b1103a6a31 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -1,210 +1,3 @@ -"""Compiler variables.""" +"""Re-export from reflex_core.""" -import dataclasses -import enum -from enum import Enum -from types import SimpleNamespace - -from reflex.constants import Dirs -from reflex.utils.imports import ImportVar - -# The prefix used to create setters for state vars. -SETTER_PREFIX = "set_" - -# The file used to specify no compilation. -NOCOMPILE_FILE = "nocompile" - - -class Ext(SimpleNamespace): - """Extension used in Reflex.""" - - # The extension for JS files. - JS = ".js" - # The extension for JSX files. - JSX = ".jsx" - # The extension for python files. - PY = ".py" - # The extension for css files. - CSS = ".css" - # The extension for zip files. - ZIP = ".zip" - # The extension for executable files on Windows. - EXE = ".exe" - # The extension for markdown files. - MD = ".md" - - -class CompileVars(SimpleNamespace): - """The variables used during compilation.""" - - # The expected variable name where the rx.App is stored. - APP = "app" - # The expected variable name where the API object is stored for deployment. - API = "api" - # The name of the router variable. - ROUTER = "router" - # The name of the socket variable. - SOCKET = "socket" - # The name of the variable to hold API results. - RESULT = "result" - # The name of the final variable. - FINAL = "final" - # The name of the process variable. - PROCESSING = "processing" - # The name of the state variable. - STATE = "state" - # The name of the events variable. - EVENTS = "events" - # The name of the initial hydrate event. - HYDRATE = "hydrate" - # The name of the is_hydrated variable. - IS_HYDRATED = "is_hydrated" - # The name of the function to add events to the queue. - ADD_EVENTS = "addEvents" - # The name of the function to apply event actions before invoking a target. - APPLY_EVENT_ACTIONS = "applyEventActions" - # The name of the var storing any connection error. - CONNECT_ERROR = "connectErrors" - # The name of the function for converting a dict to an event. - TO_EVENT = "ReflexEvent" - # The name of the internal on_load event. - ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal" - # The name of the internal event to update generic state vars. - UPDATE_VARS_INTERNAL = ( - "reflex___state____update_vars_internal_state.update_vars_internal" - ) - # The name of the frontend event exception state - FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state" - # The full name of the frontend exception state - FRONTEND_EXCEPTION_STATE_FULL = ( - f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}" - ) - - -class PageNames(SimpleNamespace): - """The name of basic pages deployed in the frontend.""" - - # The name of the index page. - INDEX_ROUTE = "index" - # The name of the app root page. - APP_ROOT = "root.jsx" - # The root stylesheet filename. - STYLESHEET_ROOT = "__reflex_global_styles" - # The name of the document root page. - DOCUMENT_ROOT = "_document.js" - # The name of the theme page. - THEME = "theme" - # The module containing components. - COMPONENTS = "components" - # The module containing shared stateful components - STATEFUL_COMPONENTS = "stateful_components" - - -class ComponentName(Enum): - """Component names.""" - - BACKEND = "Backend" - FRONTEND = "Frontend" - - def zip(self): - """Give the zip filename for the component. - - Returns: - The lower-case filename with zip extension. - """ - return self.value.lower() + Ext.ZIP - - -class CompileContext(str, Enum): - """The context in which the compiler is running.""" - - RUN = "run" - EXPORT = "export" - DEPLOY = "deploy" - UNDEFINED = "undefined" - - -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), - ImportVar(tag=CompileVars.APPLY_EVENT_ACTIONS), - ], - } - - -class Hooks(SimpleNamespace): - """Common sets of hook declarations.""" - - EVENTS = f"const [{CompileVars.ADD_EVENTS}, {CompileVars.CONNECT_ERROR}] = useContext(EventLoopContext);" - - class HookPosition(enum.Enum): - """The position of the hook in the component.""" - - INTERNAL = "internal" - PRE_TRIGGER = "pre_trigger" - POST_TRIGGER = "post_trigger" - - -class MemoizationDisposition(enum.Enum): - """The conditions under which a component should be memoized.""" - - # If the component uses state or events, it should be memoized. - STATEFUL = "stateful" - ALWAYS = "always" - NEVER = "never" - - -@dataclasses.dataclass(frozen=True) -class MemoizationMode: - """The mode for memoizing a Component.""" - - # The conditions under which the component should be memoized. - disposition: MemoizationDisposition = MemoizationDisposition.STATEFUL - - # Whether children of this component should be memoized first. - recursive: bool = True - - -DATA_UNDERSCORE = "data_" -DATA_DASH = "data-" -ARIA_UNDERSCORE = "aria_" -ARIA_DASH = "aria-" - -SPECIAL_ATTRS = ( - DATA_UNDERSCORE, - DATA_DASH, - ARIA_UNDERSCORE, - ARIA_DASH, -) - - -class SpecialAttributes(enum.Enum): - """Special attributes for components. - - These are placed in custom_attrs and rendered as-is rather than converting - to a style prop. - """ - - @classmethod - def is_special(cls, attr: str) -> bool: - """Check if the attribute is special. - - Args: - attr: the attribute to check - - Returns: - True if the attribute is special. - """ - return attr.startswith(SPECIAL_ATTRS) - - -class ResetStylesheet(SimpleNamespace): - """Constants for CSS reset stylesheet.""" - - # The filename of the CSS reset file. - FILENAME = "__reflex_style_reset.css" +from reflex_core.constants.compiler import * diff --git a/reflex/constants/config.py b/reflex/constants/config.py index 6ae12a92aeb..27d575f6658 100644 --- a/reflex/constants/config.py +++ b/reflex/constants/config.py @@ -1,76 +1,3 @@ -"""Config constants.""" +"""Re-export from reflex_core.constants.config.""" -from pathlib import Path -from types import SimpleNamespace - -from reflex.constants.base import Dirs, Reflex - -from .compiler import Ext - -# Alembic migrations -ALEMBIC_CONFIG = "alembic.ini" - - -class Config(SimpleNamespace): - """Config constants.""" - - # The name of the reflex config module. - MODULE = "rxconfig" - # The python config file. - FILE = Path(f"{MODULE}{Ext.PY}") - - -class Expiration(SimpleNamespace): - """Expiration constants.""" - - # Token expiration time in seconds - TOKEN = 60 * 60 - # Maximum time in milliseconds that a state can be locked for exclusive access. - LOCK = 10000 - # The PING timeout - PING = 120 - # The maximum time in milliseconds to hold a lock before throwing a warning. - LOCK_WARNING_THRESHOLD = 1000 - - -class GitIgnore(SimpleNamespace): - """Gitignore constants.""" - - # The gitignore file. - FILE = Path(".gitignore") - # Files to gitignore. - DEFAULTS = { - Dirs.WEB, - Dirs.STATES, - "*.db", - "__pycache__/", - "*.py[cod]", - "assets/external/", - } - - -class PyprojectToml(SimpleNamespace): - """Pyproject.toml constants.""" - - # The pyproject.toml file. - FILE = "pyproject.toml" - - -class RequirementsTxt(SimpleNamespace): - """Requirements.txt constants.""" - - # The requirements.txt file. - FILE = "requirements.txt" - # The partial text used to form requirement that pins a reflex version - DEFAULTS_STUB = f"{Reflex.MODULE_NAME}==" - - -class DefaultPorts(SimpleNamespace): - """Default port constants.""" - - FRONTEND_PORT = 3000 - BACKEND_PORT = 8000 - - -# The deployment URL. -PRODUCTION_BACKEND_URL = "https://{username}-{app_name}.api.pynecone.app" +from reflex_core.constants.config import * diff --git a/reflex/constants/custom_components.py b/reflex/constants/custom_components.py index a499327b19d..6adaad1cf43 100644 --- a/reflex/constants/custom_components.py +++ b/reflex/constants/custom_components.py @@ -1,35 +1,3 @@ -"""Constants for the custom components.""" +"""Re-export from reflex_core.constants.custom_components.""" -from __future__ import annotations - -from pathlib import Path -from types import SimpleNamespace - - -class CustomComponents(SimpleNamespace): - """Constants for the custom components.""" - - # The name of the custom components source directory. - SRC_DIR = Path("custom_components") - # The name of the custom components pyproject.toml file. - PYPROJECT_TOML = Path("pyproject.toml") - # The name of the custom components package README file. - PACKAGE_README = Path("README.md") - # The name of the custom components package .gitignore file. - PACKAGE_GITIGNORE = ".gitignore" - # The name of the distribution directory as result of a build. - DIST_DIR = "dist" - # The name of the init file. - INIT_FILE = "__init__.py" - # Suffixes for the distribution files. - DISTRIBUTION_FILE_SUFFIXES = [".tar.gz", ".whl"] - # The name to the URL of python package repositories. - REPO_URLS = { - # Note: the trailing slash is required for below URLs. - "pypi": "https://upload.pypi.org/legacy/", - "testpypi": "https://test.pypi.org/legacy/", - } - # The .gitignore file for the custom component project. - FILE = Path(".gitignore") - # Files to gitignore. - DEFAULTS = {"__pycache__/", "*.py[cod]", "*.egg-info/", "dist/"} +from reflex_core.constants.custom_components import * diff --git a/reflex/constants/event.py b/reflex/constants/event.py index 6a0f71ec161..32dc8581e7f 100644 --- a/reflex/constants/event.py +++ b/reflex/constants/event.py @@ -1,102 +1,3 @@ -"""Event-related constants.""" +"""Re-export from reflex_core.""" -from enum import Enum -from types import SimpleNamespace - - -class Endpoint(Enum): - """Endpoints for the reflex backend API.""" - - PING = "ping" - EVENT = "_event" - UPLOAD = "_upload" - AUTH_CODESPACE = "auth-codespace" - HEALTH = "_health" - ALL_ROUTES = "_all_routes" - - def __str__(self) -> str: - """Get the string representation of the endpoint. - - Returns: - The path for the endpoint. - """ - return f"/{self.value}" - - def get_url(self) -> str: - """Get the URL for the endpoint. - - Returns: - The full URL for the endpoint. - """ - # Import here to avoid circular imports. - from reflex.config import get_config - - # Get the API URL from the config. - config = get_config() - url = "".join([config.api_url, str(self)]) - - # The event endpoint is a websocket. - if self == Endpoint.EVENT: - # Replace the protocol with ws. - url = url.replace("https://", "wss://").replace("http://", "ws://") - - # Return the url. - return url - - -class SocketEvent(SimpleNamespace): - """Socket events sent by the reflex backend API.""" - - PING = "ping" - EVENT = "event" - - def __str__(self) -> str: - """Get the string representation of the event name. - - Returns: - The event name string. - """ - return str(self.value) - - -class EventTriggers(SimpleNamespace): - """All trigger names used in Reflex.""" - - ON_FOCUS = "on_focus" - ON_BLUR = "on_blur" - ON_CANCEL = "on_cancel" - ON_CLICK = "on_click" - ON_CHANGE = "on_change" - ON_CHANGE_END = "on_change_end" - ON_CHANGE_START = "on_change_start" - ON_COMPLETE = "on_complete" - ON_CONTEXT_MENU = "on_context_menu" - ON_DOUBLE_CLICK = "on_double_click" - ON_DROP = "on_drop" - ON_EDIT = "on_edit" - ON_KEY_DOWN = "on_key_down" - ON_KEY_UP = "on_key_up" - ON_MOUSE_DOWN = "on_mouse_down" - ON_MOUSE_ENTER = "on_mouse_enter" - ON_MOUSE_LEAVE = "on_mouse_leave" - ON_MOUSE_MOVE = "on_mouse_move" - ON_MOUSE_OUT = "on_mouse_out" - ON_MOUSE_OVER = "on_mouse_over" - ON_MOUSE_UP = "on_mouse_up" - ON_OPEN_CHANGE = "on_open_change" - ON_OPEN_AUTO_FOCUS = "on_open_auto_focus" - ON_CLOSE_AUTO_FOCUS = "on_close_auto_focus" - ON_FOCUS_OUTSIDE = "on_focus_outside" - ON_ESCAPE_KEY_DOWN = "on_escape_key_down" - ON_POINTER_DOWN_OUTSIDE = "on_pointer_down_outside" - ON_INTERACT_OUTSIDE = "on_interact_outside" - ON_SCROLL = "on_scroll" - ON_SCROLL_END = "on_scroll_end" - ON_SUBMIT = "on_submit" - ON_MOUNT = "on_mount" - ON_UNMOUNT = "on_unmount" - ON_CLEAR_SERVER_ERRORS = "on_clear_server_errors" - ON_VALUE_COMMIT = "on_value_commit" - ON_SELECT = "on_select" - ON_ANIMATION_START = "on_animation_start" - ON_ANIMATION_END = "on_animation_end" +from reflex_core.constants.event import * diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index aab166c7d39..06036d6872a 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -1,165 +1,3 @@ -"""File for constants related to the installation process. (Bun/Node).""" +"""Re-export from reflex_core.constants.installer.""" -from __future__ import annotations - -import os -from types import SimpleNamespace - -from .base import IS_WINDOWS -from .utils import classproperty - - -# Bun config. -class Bun(SimpleNamespace): - """Bun constants.""" - - # The Bun version. - VERSION = "1.3.10" - - # Min Bun Version - MIN_VERSION = "1.3.0" - - # URL to bun install script. - INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh" - - # URL to windows install script. - WINDOWS_INSTALL_URL = ( - "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/install.ps1" - ) - - # Path of the bunfig file - CONFIG_PATH = "bunfig.toml" - - @classproperty - @classmethod - def ROOT_PATH(cls): - """The directory to store the bun. - - Returns: - The directory to store the bun. - """ - from reflex.environment import environment - - return environment.REFLEX_DIR.get() / "bun" - - @classproperty - @classmethod - def DEFAULT_PATH(cls): - """Default bun path. - - Returns: - The default bun path. - """ - return cls.ROOT_PATH / "bin" / ("bun" if not IS_WINDOWS else "bun.exe") - - DEFAULT_CONFIG = """ -[install] -registry = "{registry}" -""" - - -# Node / NPM config -class Node(SimpleNamespace): - """Node/ NPM constants.""" - - # The minimum required node version. - MIN_VERSION = "20.19.0" - - # Path of the node config file. - CONFIG_PATH = ".npmrc" - - DEFAULT_CONFIG = """ -registry={registry} -fetch-retries=0 -""" - - -def _determine_react_router_version() -> str: - default_version = "7.13.1" - if (version := os.getenv("REACT_ROUTER_VERSION")) and version != default_version: - from reflex.utils import console - - console.warn( - f"You have requested react-router@{version} but the supported version is {default_version}, abandon all hope ye who enter here." - ) - return version - return default_version - - -def _determine_react_version() -> str: - default_version = "19.2.4" - if (version := os.getenv("REACT_VERSION")) and version != default_version: - from reflex.utils import console - - console.warn( - f"You have requested react@{version} but the supported version is {default_version}, abandon all hope ye who enter here." - ) - return version - return default_version - - -class PackageJson(SimpleNamespace): - """Constants used to build the package.json file.""" - - class Commands(SimpleNamespace): - """The commands to define in package.json.""" - - DEV = "react-router dev --host" - EXPORT = "react-router build" - - @staticmethod - def get_prod_command(frontend_path: str = "") -> str: - """Get the prod command with the correct 404.html path for the given frontend_path. - - Args: - frontend_path: The frontend path prefix (e.g. "/app"). - - Returns: - The sirv command with the correct --single fallback path. - """ - stripped = frontend_path.strip("/") - fallback = f"{stripped}/404.html" if stripped else "404.html" - return f"sirv ./build/client --single {fallback} --host" - - PATH = "package.json" - - _react_version = _determine_react_version() - - _react_router_version = _determine_react_router_version() - - @classproperty - @classmethod - def DEPENDENCIES(cls) -> dict[str, str]: - """The dependencies to include in package.json. - - Returns: - A dictionary of dependencies with their versions. - """ - return { - "json5": "2.2.3", - "react-router": cls._react_router_version, - "react-router-dom": cls._react_router_version, - "@react-router/node": cls._react_router_version, - "sirv-cli": "3.0.1", - "react": cls._react_version, - "react-helmet": "6.1.0", - "react-dom": cls._react_version, - "isbot": "5.1.36", - "socket.io-client": "4.8.3", - "universal-cookie": "7.2.2", - } - - DEV_DEPENDENCIES = { - "@emotion/react": "11.14.0", - "autoprefixer": "10.4.27", - "postcss": "8.5.8", - "postcss-import": "16.1.1", - "@react-router/dev": _react_router_version, - "@react-router/fs-routes": _react_router_version, - "vite": "8.0.0", - } - OVERRIDES = { - # This should always match the `react` version in DEPENDENCIES for recharts compatibility. - "react-is": _react_version, - "cookie": "1.1.1", - } +from reflex_core.constants.installer import * diff --git a/reflex/constants/route.py b/reflex/constants/route.py index 30e7b32170e..1c27808e7a0 100644 --- a/reflex/constants/route.py +++ b/reflex/constants/route.py @@ -1,94 +1,3 @@ -"""Route constants.""" +"""Re-export from reflex_core.constants.route.""" -import re -from types import SimpleNamespace - - -class RouteArgType(SimpleNamespace): - """Type of dynamic route arg extracted from URI route.""" - - SINGLE = "arg_single" - LIST = "arg_list" - - -# the name of the backend var containing path and client information -ROUTER = "router" -ROUTER_DATA = "router_data" - - -class RouteVar(SimpleNamespace): - """Names of variables used in the router_data dict stored in State.""" - - CLIENT_IP = "ip" - CLIENT_TOKEN = "token" - HEADERS = "headers" - PATH = "pathname" - ORIGIN = "asPath" - SESSION_ID = "sid" - QUERY = "query" - COOKIE = "cookie" - - -# This subset of router_data is included in chained on_load events. -ROUTER_DATA_INCLUDE = {RouteVar.PATH, RouteVar.ORIGIN, RouteVar.QUERY} - - -class RouteRegex(SimpleNamespace): - """Regex used for extracting route args in route.""" - - _DOT_DOT_DOT = r"\.\.\." - _OPENING_BRACKET = r"\[" - _CLOSING_BRACKET = r"\]" - _ARG_NAME = r"[a-zA-Z_]\w*" - - # The regex for a valid arg name, e.g. "slug" in "[slug]" - _ARG_NAME_PATTERN = re.compile(_ARG_NAME) - - SLUG = re.compile(r"[a-zA-Z0-9_-]+") - # match a single arg (i.e. "[slug]"), returns the name of the arg - ARG = re.compile(rf"{_OPENING_BRACKET}({_ARG_NAME}){_CLOSING_BRACKET}") - # match a single optional arg (i.e. "[[slug]]"), returns the name of the arg - OPTIONAL_ARG = re.compile( - rf"{_OPENING_BRACKET * 2}({_ARG_NAME}){_CLOSING_BRACKET * 2}" - ) - - # match a single non-optional catch-all arg (i.e. "[...slug]"), returns the name of the arg - STRICT_CATCHALL = re.compile( - rf"{_OPENING_BRACKET}{_DOT_DOT_DOT}({_ARG_NAME}){_CLOSING_BRACKET}" - ) - - # match a single optional catch-all arg (i.e. "[[...slug]]"), returns the name of the arg - OPTIONAL_CATCHALL = re.compile( - rf"{_OPENING_BRACKET * 2}{_DOT_DOT_DOT}({_ARG_NAME}){_CLOSING_BRACKET * 2}" - ) - - SPLAT_CATCHALL = "[[...splat]]" - SINGLE_SEGMENT = "__SINGLE_SEGMENT__" - DOUBLE_SEGMENT = "__DOUBLE_SEGMENT__" - DOUBLE_CATCHALL_SEGMENT = "__DOUBLE_CATCHALL_SEGMENT__" - - -class DefaultPage(SimpleNamespace): - """Default page constants.""" - - # The default title to show for Reflex apps. - TITLE = "{} | {}" - # The default description to show for Reflex apps. - DESCRIPTION = "" - # The default image to show for Reflex apps. - IMAGE = "favicon.ico" - # The default meta list to show for Reflex apps. - META_LIST = [] - - -# 404 variables -class Page404(SimpleNamespace): - """Page 404 constants.""" - - SLUG = "404" - TITLE = "404 - Not Found" - IMAGE = "favicon.ico" - DESCRIPTION = "The page was not found" - - -ROUTE_NOT_FOUND = "routeNotFound" +from reflex_core.constants.route import * diff --git a/reflex/constants/state.py b/reflex/constants/state.py index 3f6ebec2f17..9e688c8e8aa 100644 --- a/reflex/constants/state.py +++ b/reflex/constants/state.py @@ -1,19 +1,3 @@ -"""State-related constants.""" +"""Re-export from reflex_core.""" -from enum import Enum - - -class StateManagerMode(str, Enum): - """State manager constants.""" - - DISK = "disk" - MEMORY = "memory" - REDIS = "redis" - - -# Used for things like console_log, etc. -FRONTEND_EVENT_STATE = "__reflex_internal_frontend_event_state" - -FIELD_MARKER = "_rx_state_" -MEMO_MARKER = "_rx_memo_" -CAMEL_CASE_MEMO_MARKER = "RxMemo" +from reflex_core.constants.state import * diff --git a/reflex/constants/utils.py b/reflex/constants/utils.py index b07041582a9..54e33d3d9d8 100644 --- a/reflex/constants/utils.py +++ b/reflex/constants/utils.py @@ -1,31 +1,3 @@ -"""Utility functions for constants.""" +"""Re-export from reflex_core.constants.utils.""" -from collections.abc import Callable -from typing import Any, Generic, TypeVar - -T = TypeVar("T") -V = TypeVar("V") - - -class classproperty(Generic[T, V]): - """A class property decorator.""" - - def __init__(self, getter: Callable[[type[T]], V]) -> None: - """Initialize the class property. - - Args: - getter: The getter function. - """ - self.getter = getattr(getter, "__func__", getter) - - def __get__(self, instance: Any, owner: type[T]) -> V: - """Get the value of the class property. - - Args: - instance: The instance of the class. - owner: The class itself. - - Returns: - The value of the class property. - """ - return self.getter(owner) +from reflex_core.constants.utils import * diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index cce9058194c..11e6c5fc487 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -12,9 +12,9 @@ from typing import Any import click +from reflex_core import constants +from reflex_core.constants import CustomComponents -from reflex import constants -from reflex.constants import CustomComponents from reflex.utils import console, frontend_skeleton @@ -102,7 +102,7 @@ def _source_template(component_class_name: str, module_name: str) -> str: # This is because they they may not be compatible with Server-Side Rendering (SSR). # To handle this in Reflex, all you need to do is subclass `NoSSRComponent` instead. # For example: -# from reflex.components.component import NoSSRComponent +# from reflex_core.components.component import NoSSRComponent # class {component_class_name}(NoSSRComponent): # pass @@ -328,7 +328,8 @@ def _populate_demo_app(name_variants: NameVariants): Args: name_variants: the tuple including various names such as package name, class name needed for the project. """ - from reflex import constants + from reflex_core import constants + from reflex.reflex import _init demo_app_dir = Path(name_variants.demo_app_dir) @@ -612,7 +613,7 @@ def _run_commands_in_subprocess(cmds: list[str]) -> bool: def _make_pyi_files(): """Create pyi files for the custom component.""" - from reflex.utils.pyi_generator import PyiGenerator + from reflex_core.utils.pyi_generator import PyiGenerator for top_level_dir in Path.cwd().iterdir(): if not top_level_dir.is_dir() or top_level_dir.name.startswith("."): diff --git a/reflex/environment.py b/reflex/environment.py index c9895facbfd..85890d88341 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -1,877 +1,3 @@ -"""Environment variable management.""" +"""Re-export from reflex_core.environment.""" -from __future__ import annotations - -import concurrent.futures -import dataclasses -import enum -import importlib -import multiprocessing -import os -import platform -from collections.abc import Callable, Sequence -from functools import lru_cache -from pathlib import Path -from typing import ( - TYPE_CHECKING, - Annotated, - Any, - Generic, - Literal, - TypeVar, - get_args, - get_origin, - get_type_hints, -) - -from reflex import constants -from reflex.constants.base import LogLevel -from reflex.plugins import Plugin -from reflex.utils.exceptions import EnvironmentVarValueError -from reflex.utils.types import GenericType, is_union, value_inside_optional - - -def get_default_value_for_field(field: dataclasses.Field) -> Any: - """Get the default value for a field. - - Args: - field: The field. - - Returns: - The default value. - - Raises: - ValueError: If no default value is found. - """ - if field.default != dataclasses.MISSING: - return field.default - if field.default_factory != dataclasses.MISSING: - return field.default_factory() - msg = f"Missing value for environment variable {field.name} and no default value found" - raise ValueError(msg) - - -# TODO: Change all interpret_.* signatures to value: str, field: dataclasses.Field once we migrate rx.Config to dataclasses -def interpret_boolean_env(value: str, field_name: str) -> bool: - """Interpret a boolean environment variable value. - - Args: - value: The environment variable value. - field_name: The field name. - - Returns: - The interpreted value. - - Raises: - EnvironmentVarValueError: If the value is invalid. - """ - true_values = ["true", "1", "yes", "y"] - false_values = ["false", "0", "no", "n"] - - if value.lower() in true_values: - return True - if value.lower() in false_values: - return False - msg = f"Invalid boolean value: {value!r} for {field_name}" - raise EnvironmentVarValueError(msg) - - -def interpret_int_env(value: str, field_name: str) -> int: - """Interpret an integer environment variable value. - - Args: - value: The environment variable value. - field_name: The field name. - - Returns: - The interpreted value. - - Raises: - EnvironmentVarValueError: If the value is invalid. - """ - try: - return int(value) - except ValueError as ve: - msg = f"Invalid integer value: {value!r} for {field_name}" - raise EnvironmentVarValueError(msg) from ve - - -def interpret_float_env(value: str, field_name: str) -> float: - """Interpret a float environment variable value. - - Args: - value: The environment variable value. - field_name: The field name. - - Returns: - The interpreted value. - - Raises: - EnvironmentVarValueError: If the value is invalid. - """ - try: - return float(value) - except ValueError as ve: - msg = f"Invalid float value: {value!r} for {field_name}" - raise EnvironmentVarValueError(msg) from ve - - -def interpret_existing_path_env(value: str, field_name: str) -> ExistingPath: - """Interpret a path environment variable value as an existing path. - - Args: - value: The environment variable value. - field_name: The field name. - - Returns: - The interpreted value. - - Raises: - EnvironmentVarValueError: If the path does not exist. - """ - path = Path(value) - if not path.exists(): - msg = f"Path does not exist: {path!r} for {field_name}" - raise EnvironmentVarValueError(msg) - return path - - -def interpret_path_env(value: str, field_name: str) -> Path: - """Interpret a path environment variable value. - - Args: - value: The environment variable value. - field_name: The field name. - - Returns: - The interpreted value. - """ - return Path(value) - - -def interpret_plugin_class_env(value: str, field_name: str) -> type[Plugin]: - """Interpret an environment variable value as a Plugin subclass. - - Resolves a fully qualified import path to the Plugin subclass it refers to. - - Args: - value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin"). - field_name: The field name. - - Returns: - The Plugin subclass. - - Raises: - EnvironmentVarValueError: If the value is invalid. - """ - if "." not in value: - msg = f"Invalid plugin value: {value!r} for {field_name}. Plugin name must be in the format 'package.module.PluginName'." - raise EnvironmentVarValueError(msg) - - import_path, plugin_name = value.rsplit(".", 1) - - try: - module = importlib.import_module(import_path) - except ImportError as e: - msg = f"Failed to import module {import_path!r} for {field_name}: {e}" - raise EnvironmentVarValueError(msg) from e - - try: - plugin_class = getattr(module, plugin_name, None) - except Exception as e: - msg = f"Failed to get plugin class {plugin_name!r} from module {import_path!r} for {field_name}: {e}" - raise EnvironmentVarValueError(msg) from e - - if not isinstance(plugin_class, type) or not issubclass(plugin_class, Plugin): - msg = f"Invalid plugin class: {plugin_name!r} for {field_name}. Must be a subclass of Plugin." - raise EnvironmentVarValueError(msg) - - return plugin_class - - -def interpret_plugin_env(value: str, field_name: str) -> Plugin: - """Interpret a plugin environment variable value. - - Resolves a fully qualified import path and returns an instance of the Plugin. - - Args: - value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin"). - field_name: The field name. - - Returns: - An instance of the Plugin subclass. - - Raises: - EnvironmentVarValueError: If the value is invalid. - """ - plugin_class = interpret_plugin_class_env(value, field_name) - - try: - return plugin_class() - except Exception as e: - msg = f"Failed to instantiate plugin {plugin_class.__name__!r} for {field_name}: {e}" - raise EnvironmentVarValueError(msg) from e - - -def interpret_enum_env(value: str, field_type: GenericType, field_name: str) -> Any: - """Interpret an enum environment variable value. - - Args: - value: The environment variable value. - field_type: The field type. - field_name: The field name. - - Returns: - The interpreted value. - - Raises: - EnvironmentVarValueError: If the value is invalid. - """ - try: - return field_type(value) - except ValueError as ve: - msg = f"Invalid enum value: {value!r} for {field_name}" - raise EnvironmentVarValueError(msg) from ve - - -@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) -class SequenceOptions: - """Options for interpreting Sequence environment variables.""" - - delimiter: str = ":" - strip: bool = False - - -DEFAULT_SEQUENCE_OPTIONS = SequenceOptions() - - -def interpret_env_var_value( - value: str, field_type: GenericType, field_name: str -) -> Any: - """Interpret an environment variable value based on the field type. - - Args: - value: The environment variable value. - field_type: The field type. - field_name: The field name. - - Returns: - The interpreted value. - - Raises: - ValueError: If the value is invalid. - EnvironmentVarValueError: If the value is invalid for the specific type. - """ - field_type = value_inside_optional(field_type) - - # Unwrap Annotated to get the base type for env var interpretation. - # Preserve SequenceOptions and PathExistsFlag markers. - annotated_metadata: tuple[Any, ...] = () - if get_origin(field_type) is Annotated: - annotated_args = get_args(field_type) - annotated_metadata = annotated_args[1:] - field_type = annotated_args[0] - - if is_union(field_type): - errors = [] - for arg in (union_types := get_args(field_type)): - try: - return interpret_env_var_value(value, arg, field_name) - except (ValueError, EnvironmentVarValueError) as e: # noqa: PERF203 - errors.append(e) - msg = f"Could not interpret {value!r} for {field_name} as any of {union_types}: {errors}" - raise EnvironmentVarValueError(msg) - - value = value.strip() - - if field_type is bool: - return interpret_boolean_env(value, field_name) - if field_type is str: - return value - if field_type is LogLevel: - loglevel = LogLevel.from_string(value) - if loglevel is None: - msg = f"Invalid log level value: {value} for {field_name}" - raise EnvironmentVarValueError(msg) - return loglevel - if field_type is int: - return interpret_int_env(value, field_name) - if field_type is float: - return interpret_float_env(value, field_name) - if field_type is Path: - if PathExistsFlag in annotated_metadata: - return interpret_existing_path_env(value, field_name) - return interpret_path_env(value, field_name) - if field_type is ExistingPath: - return interpret_existing_path_env(value, field_name) - if field_type is Plugin: - return interpret_plugin_env(value, field_name) - if get_origin(field_type) is type: - type_args = get_args(field_type) - if ( - type_args - and isinstance(type_args[0], type) - and issubclass(type_args[0], Plugin) - ): - return interpret_plugin_class_env(value, field_name) - if get_origin(field_type) is Literal: - literal_values = get_args(field_type) - for literal_value in literal_values: - if isinstance(literal_value, str) and literal_value == value: - return literal_value - if isinstance(literal_value, bool): - try: - interpreted_bool = interpret_boolean_env(value, field_name) - if interpreted_bool == literal_value: - return interpreted_bool - except EnvironmentVarValueError: - continue - if isinstance(literal_value, int): - try: - interpreted_int = interpret_int_env(value, field_name) - if interpreted_int == literal_value: - return interpreted_int - except EnvironmentVarValueError: - continue - msg = f"Invalid literal value: {value!r} for {field_name}, expected one of {literal_values}" - raise EnvironmentVarValueError(msg) - # If the field was Annotated with SequenceOptions, extract the options - sequence_options = DEFAULT_SEQUENCE_OPTIONS - for arg in annotated_metadata: - if isinstance(arg, SequenceOptions): - sequence_options = arg - break - if get_origin(field_type) in (list, Sequence): - items = value.split(sequence_options.delimiter) - if sequence_options.strip: - items = [item.strip() for item in items] - return [ - interpret_env_var_value( - v, - get_args(field_type)[0], - f"{field_name}[{i}]", - ) - for i, v in enumerate(items) - ] - if isinstance(field_type, type) and issubclass(field_type, enum.Enum): - return interpret_enum_env(value, field_type, field_name) - - msg = f"Invalid type for environment variable {field_name}: {field_type}. This is probably an issue in Reflex." - raise ValueError(msg) - - -T = TypeVar("T") - - -class EnvVar(Generic[T]): - """Environment variable.""" - - name: str - default: Any - type_: T - - def __init__(self, name: str, default: Any, type_: T) -> None: - """Initialize the environment variable. - - Args: - name: The environment variable name. - default: The default value. - type_: The type of the value. - """ - self.name = name - self.default = default - self.type_ = type_ - - def interpret(self, value: str) -> T: - """Interpret the environment variable value. - - Args: - value: The environment variable value. - - Returns: - The interpreted value. - """ - return interpret_env_var_value(value, self.type_, self.name) - - def getenv(self) -> T | None: - """Get the interpreted environment variable value. - - Returns: - The environment variable value. - """ - env_value = os.getenv(self.name, None) - if env_value and env_value.strip(): - return self.interpret(env_value) - return None - - def is_set(self) -> bool: - """Check if the environment variable is set. - - Returns: - True if the environment variable is set. - """ - return bool(os.getenv(self.name, "").strip()) - - def get(self) -> T: - """Get the interpreted environment variable value or the default value if not set. - - Returns: - The interpreted value. - """ - env_value = self.getenv() - if env_value is not None: - return env_value - return self.default - - def set(self, value: T | None) -> None: - """Set the environment variable. None unsets the variable. - - Args: - value: The value to set. - """ - if value is None: - _ = os.environ.pop(self.name, None) - else: - if isinstance(value, enum.Enum): - value = value.value - if isinstance(value, list): - str_value = ":".join(str(v) for v in value) - else: - str_value = str(value) - os.environ[self.name] = str_value - - -@lru_cache -def get_type_hints_environment(cls: type) -> dict[str, Any]: - """Get the type hints for the environment variables. - - Args: - cls: The class. - - Returns: - The type hints. - """ - return get_type_hints(cls) - - -class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] - """Descriptor for environment variables.""" - - name: str - default: Any - internal: bool = False - - def __init__(self, default: Any, internal: bool = False) -> None: - """Initialize the descriptor. - - Args: - default: The default value. - internal: Whether the environment variable is reflex internal. - """ - self.default = default - self.internal = internal - - def __set_name__(self, owner: Any, name: str): - """Set the name of the descriptor. - - Args: - owner: The owner class. - name: The name of the descriptor. - """ - self.name = name - - def __get__( - self, instance: EnvironmentVariables, owner: type[EnvironmentVariables] - ): - """Get the EnvVar instance. - - Args: - instance: The instance. - owner: The owner class. - - Returns: - The EnvVar instance. - """ - type_ = get_args(get_type_hints_environment(owner)[self.name])[0] - env_name = self.name - if self.internal: - env_name = f"__{env_name}" - return EnvVar(name=env_name, default=self.default, type_=type_) - - -if TYPE_CHECKING: - - def env_var(default: Any, internal: bool = False) -> EnvVar: - """Typing helper for the env_var descriptor. - - Args: - default: The default value. - internal: Whether the environment variable is reflex internal. - - Returns: - The EnvVar instance. - """ - return default - - -class PathExistsFlag: - """Flag to indicate that a path must exist.""" - - -ExistingPath = Annotated[Path, PathExistsFlag] - - -class PerformanceMode(enum.Enum): - """Performance mode for the app.""" - - WARN = "warn" - RAISE = "raise" - OFF = "off" - - -class ExecutorType(enum.Enum): - """Executor for compiling the frontend.""" - - THREAD = "thread" - PROCESS = "process" - MAIN_THREAD = "main_thread" - - @classmethod - def get_executor_from_environment(cls): - """Get the executor based on the environment variables. - - Returns: - The executor. - """ - from reflex.utils import console - - executor_type = environment.REFLEX_COMPILE_EXECUTOR.get() - - reflex_compile_processes = environment.REFLEX_COMPILE_PROCESSES.get() - reflex_compile_threads = environment.REFLEX_COMPILE_THREADS.get() - # By default, use the main thread. Unless the user has specified a different executor. - # Using a process pool is much faster, but not supported on all platforms. It's gated behind a flag. - if executor_type is None: - if ( - platform.system() not in ("Linux", "Darwin") - and reflex_compile_processes is not None - ): - console.warn("Multiprocessing is only supported on Linux and MacOS.") - - if ( - platform.system() in ("Linux", "Darwin") - and reflex_compile_processes is not None - ): - if reflex_compile_processes == 0: - console.warn( - "Number of processes must be greater than 0. If you want to use the default number of processes, set REFLEX_COMPILE_EXECUTOR to 'process'. Defaulting to None." - ) - reflex_compile_processes = None - elif reflex_compile_processes < 0: - console.warn( - "Number of processes must be greater than 0. Defaulting to None." - ) - reflex_compile_processes = None - executor_type = ExecutorType.PROCESS - elif reflex_compile_threads is not None: - if reflex_compile_threads == 0: - console.warn( - "Number of threads must be greater than 0. If you want to use the default number of threads, set REFLEX_COMPILE_EXECUTOR to 'thread'. Defaulting to None." - ) - reflex_compile_threads = None - elif reflex_compile_threads < 0: - console.warn( - "Number of threads must be greater than 0. Defaulting to None." - ) - reflex_compile_threads = None - executor_type = ExecutorType.THREAD - else: - executor_type = ExecutorType.MAIN_THREAD - - match executor_type: - case ExecutorType.PROCESS: - executor = concurrent.futures.ProcessPoolExecutor( - max_workers=reflex_compile_processes, - mp_context=multiprocessing.get_context("fork"), - ) - case ExecutorType.THREAD: - executor = concurrent.futures.ThreadPoolExecutor( - max_workers=reflex_compile_threads - ) - case ExecutorType.MAIN_THREAD: - FUTURE_RESULT_TYPE = TypeVar("FUTURE_RESULT_TYPE") - - class MainThreadExecutor: - def __enter__(self): - return self - - def __exit__(self, *args): - pass - - def submit( - self, fn: Callable[..., FUTURE_RESULT_TYPE], *args, **kwargs - ) -> concurrent.futures.Future[FUTURE_RESULT_TYPE]: - future_job = concurrent.futures.Future() - future_job.set_result(fn(*args, **kwargs)) - return future_job - - executor = MainThreadExecutor() - - return executor - - -class EnvironmentVariables: - """Environment variables class to instantiate environment variables.""" - - # Indicate the current command that was invoked in the reflex CLI. - REFLEX_COMPILE_CONTEXT: EnvVar[constants.CompileContext] = env_var( - constants.CompileContext.UNDEFINED, internal=True - ) - - # Whether to use npm over bun to install and run the frontend. - REFLEX_USE_NPM: EnvVar[bool] = env_var(False) - - # The npm registry to use. - NPM_CONFIG_REGISTRY: EnvVar[str | None] = env_var(None) - - # Whether to use Granian for the backend. By default, the backend uses Uvicorn if available. - REFLEX_USE_GRANIAN: EnvVar[bool] = env_var(False) - - # Whether to use the system installed bun. If set to false, bun will be bundled with the app. - REFLEX_USE_SYSTEM_BUN: EnvVar[bool] = env_var(False) - - # The working directory for the frontend directory. - REFLEX_WEB_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.WEB)) - - # The working directory for the states directory. - REFLEX_STATES_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.STATES)) - - # Path to the alembic config file - ALEMBIC_CONFIG: EnvVar[ExistingPath] = env_var(Path(constants.ALEMBIC_CONFIG)) - - # Include schemas in alembic migrations. - ALEMBIC_INCLUDE_SCHEMAS: EnvVar[bool] = env_var(False) - - # Disable SSL verification for HTTPX requests. - SSL_NO_VERIFY: EnvVar[bool] = env_var(False) - - # The directory to store uploaded files. - REFLEX_UPLOADED_FILES_DIR: EnvVar[Path] = env_var( - Path(constants.Dirs.UPLOADED_FILES) - ) - - REFLEX_COMPILE_EXECUTOR: EnvVar[ExecutorType | None] = env_var(None) - - # Whether to use separate processes to compile the frontend and how many. If not set, defaults to thread executor. - REFLEX_COMPILE_PROCESSES: EnvVar[int | None] = env_var(None) - - # Whether to use separate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`. - REFLEX_COMPILE_THREADS: EnvVar[int | None] = env_var(None) - - # The directory to store reflex dependencies. - REFLEX_DIR: EnvVar[Path] = env_var(constants.Reflex.DIR) - - # Whether to print the SQL queries if the log level is INFO or lower. - SQLALCHEMY_ECHO: EnvVar[bool] = env_var(False) - - # Whether to check db connections before using them. - SQLALCHEMY_POOL_PRE_PING: EnvVar[bool] = env_var(True) - - # The size of the database connection pool. - SQLALCHEMY_POOL_SIZE: EnvVar[int] = env_var(5) - - # The maximum overflow size of the database connection pool. - SQLALCHEMY_MAX_OVERFLOW: EnvVar[int] = env_var(10) - - # Recycle connections after this many seconds. - SQLALCHEMY_POOL_RECYCLE: EnvVar[int] = env_var(-1) - - # The timeout for acquiring a connection from the pool. - SQLALCHEMY_POOL_TIMEOUT: EnvVar[int] = env_var(30) - - # Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration. - REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False) - - # Whether to skip purging the web directory in dev mode. - REFLEX_PERSIST_WEB_DIR: EnvVar[bool] = env_var(False) - - # This env var stores the execution mode of the app - REFLEX_ENV_MODE: EnvVar[constants.Env] = env_var(constants.Env.DEV) - - # Whether to run the backend only. Exclusive with REFLEX_FRONTEND_ONLY. - REFLEX_BACKEND_ONLY: EnvVar[bool] = env_var(False) - - # Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY. - REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False) - - # The port to run the frontend on. - REFLEX_FRONTEND_PORT: EnvVar[int | None] = env_var(None) - - # The port to run the backend on. - REFLEX_BACKEND_PORT: EnvVar[int | None] = env_var(None) - - # If this env var is set to "yes", App.compile will be a no-op - REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True) - - # Whether to run app harness tests in headless mode. - APP_HARNESS_HEADLESS: EnvVar[bool] = env_var(False) - - # Which app harness driver to use. - APP_HARNESS_DRIVER: EnvVar[str] = env_var("Chrome") - - # Arguments to pass to the app harness driver. - APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("") - - # Whether to check for outdated package versions. - REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True) - - # In which performance mode to run the app. - REFLEX_PERF_MODE: EnvVar[PerformanceMode] = env_var(PerformanceMode.WARN) - - # The maximum size of the reflex state in kilobytes. - REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) - - # Whether to use the turbopack bundler. - REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(False) - - # Additional paths to include in the hot reload. Separated by a colon. - REFLEX_HOT_RELOAD_INCLUDE_PATHS: EnvVar[list[Path]] = env_var([]) - - # Paths to exclude from the hot reload. Takes precedence over include paths. Separated by a colon. - REFLEX_HOT_RELOAD_EXCLUDE_PATHS: EnvVar[list[Path]] = env_var([]) - - # Enables different behavior for when the backend would do a cold start if it was inactive. - REFLEX_DOES_BACKEND_COLD_START: EnvVar[bool] = env_var(False) - - # The timeout for the backend to do a cold start in seconds. - REFLEX_BACKEND_COLD_START_TIMEOUT: EnvVar[int] = env_var(10) - - # Used by flexgen to enumerate the pages. - REFLEX_ADD_ALL_ROUTES_ENDPOINT: EnvVar[bool] = env_var(False) - - # The address to bind the HTTP client to. You can set this to "::" to enable IPv6. - REFLEX_HTTP_CLIENT_BIND_ADDRESS: EnvVar[str | None] = env_var(None) - - # Maximum size of the message in the websocket server in bytes. - REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE: EnvVar[int] = env_var( - constants.POLLING_MAX_HTTP_BUFFER_SIZE - ) - - # The interval to send a ping to the websocket server in seconds. - REFLEX_SOCKET_INTERVAL: EnvVar[int] = env_var(constants.Ping.INTERVAL) - - # The timeout to wait for a pong from the websocket server in seconds. - REFLEX_SOCKET_TIMEOUT: EnvVar[int] = env_var(constants.Ping.TIMEOUT) - - # Whether to run Granian in a spawn process. This enables Reflex to pick up on environment variable changes between hot reloads. - REFLEX_STRICT_HOT_RELOAD: EnvVar[bool] = env_var(False) - - # The path to the reflex log file. If not set, the log file will be stored in the reflex user directory. - REFLEX_LOG_FILE: EnvVar[Path | None] = env_var(None) - - # Enable full logging of debug messages to reflex user directory. - REFLEX_ENABLE_FULL_LOGGING: EnvVar[bool] = env_var(False) - - # Whether to enable hot module replacement - VITE_HMR: EnvVar[bool] = env_var(True) - - # Whether to force a full reload on changes. - VITE_FORCE_FULL_RELOAD: EnvVar[bool] = env_var(False) - - # Whether to enable Rolldown's experimental HMR. - VITE_EXPERIMENTAL_HMR: EnvVar[bool] = env_var(False) - - # Whether to generate sourcemaps for the frontend. - VITE_SOURCEMAP: EnvVar[Literal[False, True, "inline", "hidden"]] = env_var(False) # noqa: RUF038 - - # Whether to enable SSR for the frontend. - REFLEX_SSR: EnvVar[bool] = env_var(True) - - # Whether to mount the compiled frontend app in the backend server in production. - REFLEX_MOUNT_FRONTEND_COMPILED_APP: EnvVar[bool] = env_var(False, internal=True) - - # How long to delay writing updated states to disk. (Higher values mean less writes, but more chance of lost data.) - REFLEX_STATE_MANAGER_DISK_DEBOUNCE_SECONDS: EnvVar[float] = env_var(2.0) - - # How long to wait between automatic reload on frontend error to avoid reload loops. - REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS: EnvVar[int] = env_var(10_000) - - # Whether to enable debug logging for the redis state manager. - REFLEX_STATE_MANAGER_REDIS_DEBUG: EnvVar[bool] = env_var(False) - - # Whether to opportunistically hold the redis lock to allow fast in-memory access while uncontended. - REFLEX_OPLOCK_ENABLED: EnvVar[bool] = env_var(False) - - # How long to opportunistically hold the redis lock in milliseconds (must be less than the token expiration). - REFLEX_OPLOCK_HOLD_TIME_MS: EnvVar[int] = env_var(0) - - -environment = EnvironmentVariables() - -try: - from dotenv import load_dotenv -except ImportError: - load_dotenv = None - - -def _paths_from_env_files(env_files: str) -> list[Path]: - """Convert a string of paths separated by os.pathsep into a list of Path objects. - - Args: - env_files: The string of paths. - - Returns: - A list of Path objects. - """ - # load env files in reverse order - return list( - reversed([ - Path(path) - for path_element in env_files.split(os.pathsep) - if (path := path_element.strip()) - ]) - ) - - -def _load_dotenv_from_files(files: list[Path]): - """Load environment variables from a list of files. - - Args: - files: A list of Path objects representing the environment variable files. - """ - from reflex.utils import console - - if not files: - return - - if load_dotenv is None: - console.error( - """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.1.0"`.""" - ) - return - - for env_file in files: - if env_file.exists(): - load_dotenv(env_file, override=True) - - -def _paths_from_environment() -> list[Path]: - """Get the paths from the REFLEX_ENV_FILE environment variable. - - Returns: - A list of Path objects. - """ - env_files = os.environ.get("REFLEX_ENV_FILE") - if not env_files: - return [] - - return _paths_from_env_files(env_files) - - -def _load_dotenv_from_env(): - """Load environment variables from paths specified in REFLEX_ENV_FILE.""" - _load_dotenv_from_files(_paths_from_environment()) - - -# Load the env files at import time if they are set in the ENV_FILE environment variable. -_load_dotenv_from_env() +from reflex_core.environment import * diff --git a/reflex/event.py b/reflex/event.py index 887976df909..cf383dbb67a 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -1,2754 +1,8 @@ -"""Define event classes to connect the frontend and backend.""" +"""Re-export from reflex_core.""" -import dataclasses -import inspect import sys -import types -import warnings -from base64 import b64encode -from collections.abc import Callable, Mapping, Sequence -from functools import lru_cache, partial -from typing import ( - TYPE_CHECKING, - Annotated, - Any, - Generic, - Literal, - NoReturn, - Protocol, - TypeVar, - get_args, - get_origin, - get_type_hints, - overload, -) -from typing_extensions import Self, TypeAliasType, TypedDict, TypeVarTuple, Unpack +from reflex_core.event import * # pyright: ignore[reportWildcardImportFromLibrary] +from reflex_core.event import event -from reflex import constants -from reflex.components.field import BaseField -from reflex.constants.compiler import CompileVars, Hooks, Imports -from reflex.constants.state import FRONTEND_EVENT_STATE -from reflex.utils import format -from reflex.utils.decorator import once -from reflex.utils.exceptions import ( - EventFnArgMismatchError, - EventHandlerArgTypeMismatchError, - MissingAnnotationError, -) -from reflex.utils.types import ( - ArgsSpec, - GenericType, - Unset, - safe_issubclass, - typehint_issubclass, -) -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var -from reflex.vars.function import ( - ArgsFunctionOperation, - ArgsFunctionOperationBuilder, - BuilderFunctionVar, - FunctionArgs, - FunctionStringVar, - FunctionVar, - VarOperationCall, -) -from reflex.vars.object import ObjectVar - - -@dataclasses.dataclass( - init=True, - frozen=True, -) -class Event: - """An event that describes any state change in the app. - - Attributes: - token: The token to specify the client that the event is for. - name: The event name. - router_data: The routing data where event occurred. - payload: The event payload. - """ - - token: str - - name: str - - router_data: dict[str, Any] = dataclasses.field(default_factory=dict) - - payload: dict[str, Any] = dataclasses.field(default_factory=dict) - - @property - def substate_token(self) -> str: - """Get the substate token for the event. - - Returns: - The substate token. - """ - substate = self.name.rpartition(".")[0] - return f"{self.token}_{substate}" - - -_EVENT_FIELDS: set[str] = {f.name for f in dataclasses.fields(Event)} -_EMPTY_EVENTS = LiteralVar.create([]) -_EMPTY_EVENT_ACTIONS = LiteralVar.create({}) - -BACKGROUND_TASK_MARKER = "_reflex_background_task" -EVENT_ACTIONS_MARKER = "_rx_event_actions" -UPLOAD_FILES_CLIENT_HANDLER = "uploadFiles" - - -def _handler_name(handler: "EventHandler") -> str: - """Get a stable fully qualified handler name for errors. - - Args: - handler: The handler to name. - - Returns: - The fully qualified handler name. - """ - if handler.state_full_name: - return f"{handler.state_full_name}.{handler.fn.__name__}" - return handler.fn.__qualname__ - - -def resolve_upload_handler_param(handler: "EventHandler") -> tuple[str, Any]: - """Validate and resolve the UploadFile list parameter for a handler. - - Args: - handler: The event handler to inspect. - - Returns: - The parameter name and annotation for the upload file argument. - - Raises: - UploadTypeError: If the handler is a background task. - UploadValueError: If the handler does not accept ``list[rx.UploadFile]``. - """ - from reflex._upload import UploadFile - from reflex.utils.exceptions import UploadTypeError, UploadValueError - - handler_name = _handler_name(handler) - if handler.is_background: - msg = ( - f"@rx.event(background=True) is not supported for upload handler " - f"`{handler_name}`." - ) - raise UploadTypeError(msg) - - func = handler.fn.func if isinstance(handler.fn, partial) else handler.fn - for name, annotation in get_type_hints(func).items(): - if name == "return" or get_origin(annotation) is not list: - continue - args = get_args(annotation) - if len(args) == 1 and typehint_issubclass(args[0], UploadFile): - return name, annotation - - msg = ( - f"`{handler_name}` handler should have a parameter annotated as " - "list[rx.UploadFile]" - ) - raise UploadValueError(msg) - - -def resolve_upload_chunk_handler_param(handler: "EventHandler") -> tuple[str, type]: - """Validate and resolve the UploadChunkIterator parameter for a handler. - - Args: - handler: The event handler to inspect. - - Returns: - The parameter name and annotation for the iterator argument. - - Raises: - UploadTypeError: If the handler is not a background task. - UploadValueError: If the handler does not accept an UploadChunkIterator. - """ - from reflex._upload import UploadChunkIterator - from reflex.utils.exceptions import UploadTypeError, UploadValueError - - handler_name = _handler_name(handler) - if not handler.is_background: - msg = f"@rx.event(background=True) is required for upload_files_chunk handler `{handler_name}`." - raise UploadTypeError(msg) - - func = handler.fn.func if isinstance(handler.fn, partial) else handler.fn - for name, annotation in get_type_hints(func).items(): - if name == "return": - continue - if annotation is UploadChunkIterator: - return name, annotation - - msg = ( - f"`{handler_name}` handler should have a parameter annotated as " - "rx.UploadChunkIterator" - ) - raise UploadValueError(msg) - - -@dataclasses.dataclass( - init=True, - frozen=True, - kw_only=True, -) -class EventActionsMixin: - """Mixin for DOM event actions. - - Attributes: - event_actions: Whether to `preventDefault` or `stopPropagation` on the event. - """ - - event_actions: dict[str, bool | int] = dataclasses.field(default_factory=dict) - - @property - def stop_propagation(self) -> Self: - """Stop the event from bubbling up the DOM tree. - - Returns: - New EventHandler-like with stopPropagation set to True. - """ - return dataclasses.replace( - self, - event_actions={**self.event_actions, "stopPropagation": True}, - ) - - @property - def prevent_default(self) -> Self: - """Prevent the default behavior of the event. - - Returns: - New EventHandler-like with preventDefault set to True. - """ - return dataclasses.replace( - self, - event_actions={**self.event_actions, "preventDefault": True}, - ) - - def throttle(self, limit_ms: int) -> Self: - """Throttle the event handler. - - Args: - limit_ms: The time in milliseconds to throttle the event handler. - - Returns: - New EventHandler-like with throttle set to limit_ms. - """ - return dataclasses.replace( - self, - event_actions={**self.event_actions, "throttle": limit_ms}, - ) - - def debounce(self, delay_ms: int) -> Self: - """Debounce the event handler. - - Args: - delay_ms: The time in milliseconds to debounce the event handler. - - Returns: - New EventHandler-like with debounce set to delay_ms. - """ - return dataclasses.replace( - self, - event_actions={**self.event_actions, "debounce": delay_ms}, - ) - - @property - def temporal(self) -> Self: - """Do not queue the event if the backend is down. - - Returns: - New EventHandler-like with temporal set to True. - """ - return dataclasses.replace( - self, - event_actions={**self.event_actions, "temporal": True}, - ) - - -@dataclasses.dataclass( - init=True, - frozen=True, - kw_only=True, -) -class EventHandler(EventActionsMixin): - """An event handler responds to an event to update the state. - - Attributes: - fn: The function to call in response to the event. - state_full_name: The full name of the state class this event handler is attached to. Empty string means this event handler is a server side event. - """ - - fn: Any = dataclasses.field(default=None) - - state_full_name: str = dataclasses.field(default="") - - def __hash__(self): - """Get the hash of the event handler. - - Returns: - The hash of the event handler. - """ - return hash((tuple(self.event_actions.items()), self.fn, self.state_full_name)) - - def get_parameters(self) -> Mapping[str, inspect.Parameter]: - """Get the parameters of the function. - - Returns: - The parameters of the function. - """ - if self.fn is None: - return {} - return inspect.signature(self.fn).parameters - - @property - def _parameters(self) -> Mapping[str, inspect.Parameter]: - """Get the parameters of the function. - - Returns: - The parameters of the function. - """ - if (parameters := getattr(self, "__parameters", None)) is None: - parameters = {**self.get_parameters()} - object.__setattr__(self, "__parameters", parameters) - return parameters - - @classmethod - def __class_getitem__(cls, args_spec: str) -> Annotated: - """Get a typed EventHandler. - - Args: - args_spec: The args_spec of the EventHandler. - - Returns: - The EventHandler class item. - """ - return Annotated[cls, args_spec] - - @property - def is_background(self) -> bool: - """Whether the event handler is a background task. - - Returns: - True if the event handler is marked as a background task. - """ - return getattr(self.fn, BACKGROUND_TASK_MARKER, False) - - def __call__(self, *args: Any, **kwargs: Any) -> "EventSpec": - """Pass arguments to the handler to get an event spec. - - This method configures event handlers that take in arguments. - - Args: - *args: The arguments to pass to the handler. - **kwargs: The keyword arguments to pass to the handler. - - Returns: - The event spec, containing both the function and args. - - Raises: - EventHandlerTypeError: If the arguments are invalid. - """ - from reflex.utils.exceptions import EventHandlerTypeError - - # Get the function args. - fn_args = list(self._parameters)[1:] - - if not isinstance( - repeated_arg := next( - (kwarg for kwarg in kwargs if kwarg in fn_args[: len(args)]), Unset() - ), - Unset, - ): - msg = f"Event handler {self.fn.__name__} received repeated argument {repeated_arg}." - raise EventHandlerTypeError(msg) - - if not isinstance( - extra_arg := next( - (kwarg for kwarg in kwargs if kwarg not in fn_args), Unset() - ), - Unset, - ): - msg = ( - f"Event handler {self.fn.__name__} received extra argument {extra_arg}." - ) - raise EventHandlerTypeError(msg) - - fn_args = fn_args[: len(args)] + list(kwargs) - - fn_args = (Var(_js_expr=arg) for arg in fn_args) - - # Construct the payload. - values = [] - for arg in [*args, *kwargs.values()]: - # Special case for file uploads. - if isinstance(arg, (FileUpload, UploadFilesChunk)): - return arg.as_event_spec(handler=self) - - # Otherwise, convert to JSON. - try: - values.append(LiteralVar.create(arg)) - except TypeError as e: - msg = f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." - raise EventHandlerTypeError(msg) from e - payload = tuple(zip(fn_args, values, strict=False)) - - # Return the event spec. - return EventSpec( - handler=self, args=payload, event_actions=self.event_actions.copy() - ) - - -@dataclasses.dataclass( - init=True, - frozen=True, - kw_only=True, -) -class EventSpec(EventActionsMixin): - """An event specification. - - Whereas an Event object is passed during runtime, a spec is used - during compile time to outline the structure of an event. - - Attributes: - handler: The event handler. - client_handler_name: The handler on the client to process event. - args: The arguments to pass to the function. - """ - - handler: EventHandler = dataclasses.field(default=None) # pyright: ignore [reportAssignmentType] - - client_handler_name: str = dataclasses.field(default="") - - args: tuple[tuple[Var, Var], ...] = dataclasses.field(default_factory=tuple) - - def __init__( - self, - handler: EventHandler, - event_actions: dict[str, bool | int] | None = None, - client_handler_name: str = "", - args: tuple[tuple[Var, Var], ...] = (), - ): - """Initialize an EventSpec. - - Args: - event_actions: The event actions. - handler: The event handler. - client_handler_name: The client handler name. - args: The arguments to pass to the function. - """ - if event_actions is None: - event_actions = {} - object.__setattr__(self, "event_actions", event_actions) - object.__setattr__(self, "handler", handler) - object.__setattr__(self, "client_handler_name", client_handler_name) - object.__setattr__(self, "args", args or ()) - - def with_args(self, args: tuple[tuple[Var, Var], ...]) -> "EventSpec": - """Copy the event spec, with updated args. - - Args: - args: The new args to pass to the function. - - Returns: - A copy of the event spec, with the new args. - """ - return type(self)( - handler=self.handler, - client_handler_name=self.client_handler_name, - args=args, - event_actions=self.event_actions.copy(), - ) - - def add_args(self, *args: Var) -> "EventSpec": - """Add arguments to the event spec. - - Args: - *args: The arguments to add positionally. - - Returns: - The event spec with the new arguments. - - Raises: - EventHandlerTypeError: If the arguments are invalid. - """ - from reflex.utils.exceptions import EventHandlerTypeError - - # Get the remaining unfilled function args. - fn_args = list(self.handler._parameters)[1 + len(self.args) :] - fn_args = (Var(_js_expr=arg) for arg in fn_args) - - # Construct the payload. - values = [] - arg = None - try: - for arg in args: - values.append(LiteralVar.create(value=arg)) # noqa: PERF401, RUF100 - except TypeError as e: - msg = f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." - raise EventHandlerTypeError(msg) from e - new_payload = tuple(zip(fn_args, values, strict=False)) - return self.with_args(self.args + new_payload) - - -@dataclasses.dataclass( - frozen=True, -) -class CallableEventSpec(EventSpec): - """Decorate an EventSpec-returning function to act as both a EventSpec and a function. - - This is used as a compatibility shim for replacing EventSpec objects in the - API with functions that return a family of EventSpec. - """ - - fn: Callable[..., EventSpec] | None = None - - def __init__(self, fn: Callable[..., EventSpec] | None = None, **kwargs): - """Initialize a CallableEventSpec. - - Args: - fn: The function to decorate. - **kwargs: The kwargs to pass to the EventSpec constructor. - """ - if fn is not None: - default_event_spec = fn() - super().__init__( - event_actions=default_event_spec.event_actions, - client_handler_name=default_event_spec.client_handler_name, - args=default_event_spec.args, - handler=default_event_spec.handler, - **kwargs, - ) - object.__setattr__(self, "fn", fn) - else: - super().__init__(**kwargs) - - def __call__(self, *args, **kwargs) -> EventSpec: - """Call the decorated function. - - Args: - *args: The args to pass to the function. - **kwargs: The kwargs to pass to the function. - - Returns: - The EventSpec returned from calling the function. - - Raises: - EventHandlerTypeError: If the CallableEventSpec has no associated function. - """ - from reflex.utils.exceptions import EventHandlerTypeError - - if self.fn is None: - msg = "CallableEventSpec has no associated function." - raise EventHandlerTypeError(msg) - return self.fn(*args, **kwargs) - - -@dataclasses.dataclass( - init=True, - frozen=True, -) -class EventChain(EventActionsMixin): - """Container for a chain of events that will be executed in order.""" - - events: "Sequence[EventSpec | EventVar | FunctionVar | EventCallback]" = ( - dataclasses.field(default_factory=list) - ) - - args_spec: Callable | Sequence[Callable] | None = dataclasses.field(default=None) - - invocation: Var | None = dataclasses.field(default=None) - - @classmethod - def create( - cls, - value: "EventType", - args_spec: ArgsSpec | Sequence[ArgsSpec], - key: str | None = None, - **event_chain_kwargs, - ) -> "EventChain | Var": - """Create an event chain from a variety of input types. - - Args: - value: The value to create the event chain from. - args_spec: The args_spec of the event trigger being bound. - key: The key of the event trigger being bound. - **event_chain_kwargs: Additional kwargs to pass to the EventChain constructor. - - Returns: - The event chain. - - Raises: - ValueError: If the value is not a valid event chain. - """ - # If it's an event chain var, return it. - if isinstance(value, Var): - # Only pass through literal/prebuilt chains. Other EventChainVar values may be - # FunctionVars cast with `.to(EventChain)` and still need wrapping so - # event_chain_kwargs can compose onto the resulting chain. - if isinstance(value, LiteralEventChainVar): - if event_chain_kwargs: - warnings.warn( - f"event_chain_kwargs {event_chain_kwargs!r} are ignored for " - "EventChainVar values.", - stacklevel=2, - ) - return value - if isinstance(value, (EventVar, FunctionVar)): - value = [value] - elif safe_issubclass(value._var_type, (EventChain, EventSpec)): - return cls.create( - value=value.guess_type(), - args_spec=args_spec, - key=key, - **event_chain_kwargs, - ) - else: - msg = f"Invalid event chain: {value!s} of type {value._var_type}" - raise ValueError(msg) - elif isinstance(value, EventChain): - # Trust that the caller knows what they're doing passing an EventChain directly - return value - - # If the input is a single event handler, wrap it in a list. - if isinstance(value, (EventHandler, EventSpec)): - value = [value] - - events: list[EventSpec | EventVar | FunctionVar] = [] - - # If the input is a list of event handlers, create an event chain. - if isinstance(value, list): - for v in value: - if isinstance(v, (EventHandler, EventSpec)): - # Call the event handler to get the event. - events.append(call_event_handler(v, args_spec, key=key)) - elif isinstance(v, (EventVar, EventChainVar)): - events.append(v) - elif isinstance(v, FunctionVar): - # Apply the args_spec transformations as partial arguments to the function. - events.append(v.partial(*parse_args_spec(args_spec)[0])) - elif isinstance(v, Callable): - # Call the lambda to get the event chain. - events.extend(call_event_fn(v, args_spec, key=key)) - else: - msg = f"Invalid event: {v}" - raise ValueError(msg) - - # If the input is a callable, create an event chain. - elif isinstance(value, Callable): - events.extend(call_event_fn(value, args_spec, key=key)) - - # Otherwise, raise an error. - else: - msg = f"Invalid event chain: {value}" - raise ValueError(msg) - - # Add args to the event specs if necessary. - events = [ - (e.with_args(get_handler_args(e)) if isinstance(e, EventSpec) else e) - for e in events - ] - - # Return the event chain. - return cls( - events=events, - args_spec=args_spec, - **event_chain_kwargs, - ) - - -@dataclasses.dataclass( - init=True, - frozen=True, -) -class JavascriptHTMLInputElement: - """Interface for a Javascript HTMLInputElement https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement.""" - - value: str = "" - checked: bool = False - - -@dataclasses.dataclass( - init=True, - frozen=True, -) -class JavascriptInputEvent: - """Interface for a Javascript InputEvent https://developer.mozilla.org/en-US/docs/Web/API/InputEvent.""" - - target: JavascriptHTMLInputElement = JavascriptHTMLInputElement() - - -@dataclasses.dataclass( - init=True, - frozen=True, -) -class JavascriptKeyboardEvent: - """Interface for a Javascript KeyboardEvent https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.""" - - key: str = "" - altKey: bool = False # noqa: N815 - ctrlKey: bool = False # noqa: N815 - metaKey: bool = False # noqa: N815 - shiftKey: bool = False # noqa: N815 - - -def input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[str]]: - """Get the value from an input event. - - Args: - e: The input event. - - Returns: - The value from the input event. - """ - return (e.target.value,) - - -def int_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[int]]: - """Get the value from an input event as an int. - - Args: - e: The input event. - - Returns: - The value from the input event as an int. - """ - return (Var("Number").to(FunctionVar).call(e.target.value).to(int),) - - -def float_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[float]]: - """Get the value from an input event as a float. - - Args: - e: The input event. - - Returns: - The value from the input event as a float. - """ - return (Var("Number").to(FunctionVar).call(e.target.value).to(float),) - - -def checked_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[bool]]: - """Get the checked state from an input event. - - Args: - e: The input event. - - Returns: - The checked state from the input event. - """ - return (e.target.checked,) - - -FORM_DATA = Var(_js_expr="form_data") - - -def on_submit_event() -> tuple[Var[dict[str, Any]]]: - """Event handler spec for the on_submit event. - - Returns: - The event handler spec. - """ - return (FORM_DATA,) - - -def on_submit_string_event() -> tuple[Var[dict[str, str]]]: - """Event handler spec for the on_submit event. - - Returns: - The event handler spec. - """ - return (FORM_DATA,) - - -class KeyInputInfo(TypedDict): - """Information about a key input event.""" - - alt_key: bool - ctrl_key: bool - meta_key: bool - shift_key: bool - - -def key_event( - e: ObjectVar[JavascriptKeyboardEvent], -) -> tuple[Var[str], Var[KeyInputInfo]]: - """Get the key from a keyboard event. - - Args: - e: The keyboard event. - - Returns: - The key from the keyboard event. - """ - return ( - e.key.to(str), - Var.create( - { - "alt_key": e.altKey, - "ctrl_key": e.ctrlKey, - "meta_key": e.metaKey, - "shift_key": e.shiftKey, - }, - ).to(KeyInputInfo), - ) - - -@dataclasses.dataclass( - init=True, - frozen=True, -) -class JavascriptMouseEvent: - """Interface for a Javascript MouseEvent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.""" - - button: int = 0 - buttons: list[int] = dataclasses.field(default_factory=list) - clientX: int = 0 # noqa: N815 - clientY: int = 0 # noqa: N815 - altKey: bool = False # noqa: N815 - ctrlKey: bool = False # noqa: N815 - metaKey: bool = False # noqa: N815 - shiftKey: bool = False # noqa: N815 - - -class JavascriptPointerEvent(JavascriptMouseEvent): - """Interface for a Javascript PointerEvent https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent. - - Inherits from JavascriptMouseEvent. - """ - - -class MouseEventInfo(TypedDict): - """Information about a mouse event.""" - - button: int - buttons: int - client_x: int - client_y: int - alt_key: bool - ctrl_key: bool - meta_key: bool - shift_key: bool - - -class PointerEventInfo(MouseEventInfo): - """Information about a pointer event.""" - - -def pointer_event_spec( - e: ObjectVar[JavascriptPointerEvent], -) -> tuple[Var[PointerEventInfo]]: - """Get the pointer event information. - - Args: - e: The pointer event. - - Returns: - The pointer event information. - """ - return ( - Var.create( - { - "button": e.button, - "buttons": e.buttons, - "client_x": e.clientX, - "client_y": e.clientY, - "alt_key": e.altKey, - "ctrl_key": e.ctrlKey, - "meta_key": e.metaKey, - "shift_key": e.shiftKey, - }, - ).to(PointerEventInfo), - ) - - -def no_args_event_spec() -> tuple[()]: - """Empty event handler. - - Returns: - An empty tuple. - """ - return () - - -T = TypeVar("T") -U = TypeVar("U") - - -class IdentityEventReturn(Generic[T], Protocol): - """Protocol for an identity event return.""" - - def __call__(self, *values: Var[T]) -> tuple[Var[T], ...]: - """Return the input values. - - Args: - *values: The values to return. - - Returns: - The input values. - """ - return values - - -@overload -def passthrough_event_spec( # pyright: ignore [reportOverlappingOverload] - event_type: type[T], / -) -> Callable[[Var[T]], tuple[Var[T]]]: ... - - -@overload -def passthrough_event_spec( - event_type_1: type[T], event_type2: type[U], / -) -> Callable[[Var[T], Var[U]], tuple[Var[T], Var[U]]]: ... - - -@overload -def passthrough_event_spec(*event_types: type[T]) -> IdentityEventReturn[T]: ... - - -def passthrough_event_spec(*event_types: type[T]) -> IdentityEventReturn[T]: # pyright: ignore [reportInconsistentOverload] - """A helper function that returns the input event as output. - - Args: - *event_types: The types of the events. - - Returns: - A function that returns the input event as output. - """ - - def inner(*values: Var[T]) -> tuple[Var[T], ...]: - return values - - inner_type = tuple(Var[event_type] for event_type in event_types) - return_annotation = tuple[inner_type] - - inner.__signature__ = inspect.signature(inner).replace( # pyright: ignore [reportFunctionMemberAccess] - parameters=[ - inspect.Parameter( - f"ev_{i}", - kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, - annotation=Var[event_type], - ) - for i, event_type in enumerate(event_types) - ], - return_annotation=return_annotation, - ) - for i, event_type in enumerate(event_types): - inner.__annotations__[f"ev_{i}"] = Var[event_type] - inner.__annotations__["return"] = return_annotation - - return inner - - -@dataclasses.dataclass( - init=True, - frozen=True, -) -class FileUpload: - """Class to represent a file upload.""" - - upload_id: str | None = None - on_upload_progress: EventHandler | Callable | None = None - extra_headers: dict[str, str] | None = None - - @staticmethod - def on_upload_progress_args_spec(_prog: Var[dict[str, int | float | bool]]): - """Args spec for on_upload_progress event handler. - - Returns: - The arg mapping passed to backend event handler - """ - return [_prog] - - def _as_event_spec( - self, - handler: EventHandler, - *, - client_handler_name: str, - upload_param_name: str, - ) -> EventSpec: - """Create an upload EventSpec. - - Args: - handler: The event handler. - client_handler_name: The client handler name. - upload_param_name: The upload argument name in the event handler. - - Returns: - The upload EventSpec. - - Raises: - ValueError: If the on_upload_progress is not a valid event handler. - """ - from reflex.components.core.upload import ( - DEFAULT_UPLOAD_ID, - upload_files_context_var_data, - ) - - upload_id = self.upload_id if self.upload_id is not None else DEFAULT_UPLOAD_ID - upload_files_var = Var( - _js_expr="filesById", - _var_type=dict[str, Any], - _var_data=VarData.merge(upload_files_context_var_data), - ).to(ObjectVar)[LiteralVar.create(upload_id)] - spec_args = [ - ( - Var(_js_expr="files"), - upload_files_var, - ), - ( - Var(_js_expr="upload_param_name"), - LiteralVar.create(upload_param_name), - ), - ( - Var(_js_expr="upload_id"), - LiteralVar.create(upload_id), - ), - ( - Var(_js_expr="extra_headers"), - LiteralVar.create( - self.extra_headers if self.extra_headers is not None else {} - ), - ), - ] - if upload_param_name != "files": - spec_args.insert( - 1, - ( - Var(_js_expr=upload_param_name), - upload_files_var, - ), - ) - if self.on_upload_progress is not None: - on_upload_progress = self.on_upload_progress - if isinstance(on_upload_progress, EventHandler): - events = [ - call_event_handler( - on_upload_progress, - self.on_upload_progress_args_spec, - ), - ] - elif isinstance(on_upload_progress, Callable): - # Call the lambda to get the event chain. - events = call_event_fn( - on_upload_progress, self.on_upload_progress_args_spec - ) - else: - msg = f"{on_upload_progress} is not a valid event handler." - raise ValueError(msg) - if isinstance(events, Var): - msg = f"{on_upload_progress} cannot return a var {events}." - raise ValueError(msg) - on_upload_progress_chain = EventChain( - events=[*events], - args_spec=self.on_upload_progress_args_spec, - ) - formatted_chain = str(format.format_prop(on_upload_progress_chain)) - spec_args.append( - ( - Var(_js_expr="on_upload_progress"), - FunctionStringVar( - formatted_chain.strip("{}"), - ).to(FunctionVar, EventChain), - ), - ) - return EventSpec( - handler=handler, - client_handler_name=client_handler_name, - args=tuple(spec_args), - event_actions=handler.event_actions.copy(), - ) - - def as_event_spec(self, handler: EventHandler) -> EventSpec: - """Get the EventSpec for the file upload. - - Args: - handler: The event handler. - - Returns: - The event spec for the handler. - """ - from reflex.utils.exceptions import UploadValueError - - try: - upload_param_name, _annotation = resolve_upload_handler_param(handler) - except UploadValueError: - upload_param_name = "files" - return self._as_event_spec( - handler, - client_handler_name=UPLOAD_FILES_CLIENT_HANDLER, - upload_param_name=upload_param_name, - ) - - -# Alias for rx.upload_files -upload_files = FileUpload - - -@dataclasses.dataclass( - init=True, - frozen=True, -) -class UploadFilesChunk(FileUpload): - """Class to represent a streaming file upload.""" - - def as_event_spec(self, handler: EventHandler) -> EventSpec: - """Get the EventSpec for the streaming file upload. - - Args: - handler: The event handler. - - Returns: - The event spec for the handler. - """ - upload_param_name, _annotation = resolve_upload_chunk_handler_param(handler) - return self._as_event_spec( - handler, - client_handler_name=UPLOAD_FILES_CLIENT_HANDLER, - upload_param_name=upload_param_name, - ) - - -# Alias for rx.upload_files_chunk -upload_files_chunk = UploadFilesChunk - - -# Special server-side events. -def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec: - """A server-side event. - - Args: - name: The name of the event. - sig: The function signature of the event. - **kwargs: The arguments to pass to the event. - - Returns: - An event spec for a server-side event. - """ - - def fn(): - return None - - fn.__qualname__ = name - fn.__signature__ = sig # pyright: ignore [reportFunctionMemberAccess] - return EventSpec( - handler=EventHandler(fn=fn, state_full_name=FRONTEND_EVENT_STATE), - args=tuple( - ( - Var(_js_expr=k), - LiteralVar.create(v), - ) - for k, v in kwargs.items() - ), - ) - - -@overload -def redirect( - path: str | Var[str], - *, - is_external: Literal[False] = False, - replace: bool = False, -) -> EventSpec: ... - - -@overload -def redirect( - path: str | Var[str], - *, - is_external: Literal[True], - popup: bool = False, -) -> EventSpec: ... - - -def redirect( - path: str | Var[str], - *, - is_external: bool = False, - popup: bool = False, - replace: bool = False, -) -> EventSpec: - """Redirect to a new path. - - Args: - path: The path to redirect to. - is_external: Whether to open in new tab or not. - popup: Whether to open in a new window 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=is_external, - popup=popup, - replace=replace, - ) - - -def console_log(message: str | Var[str]) -> EventSpec: - """Do a console.log on the browser. - - Args: - message: The message to log. - - Returns: - An event to log the message. - """ - return run_script(Var("console").to(dict).log.to(FunctionVar).call(message)) - - -@once -def noop() -> EventSpec: - """Do nothing. - - Returns: - An event to do nothing. - """ - return run_script(Var.create(None)) - - -def back() -> EventSpec: - """Do a history.back on the browser. - - Returns: - An event to go back one page. - """ - return run_script( - Var("window").to(dict).history.to(dict).back.to(FunctionVar).call() - ) - - -def window_alert(message: str | Var[str]) -> EventSpec: - """Create a window alert on the browser. - - Args: - message: The message to alert. - - Returns: - An event to alert the message. - """ - return run_script(Var("window").to(dict).alert.to(FunctionVar).call(message)) - - -def set_focus(ref: str) -> EventSpec: - """Set focus to specified ref. - - Args: - ref: The ref. - - Returns: - An event to set focus on the ref - """ - return server_side( - "_set_focus", - get_fn_signature(set_focus), - ref=LiteralVar.create(format.format_ref(ref)), - ) - - -def blur_focus(ref: str) -> EventSpec: - """Blur focus of specified ref. - - Args: - ref: The ref. - - Returns: - An event to blur focus on the ref - """ - return server_side( - "_blur_focus", - get_fn_signature(blur_focus), - ref=LiteralVar.create(format.format_ref(ref)), - ) - - -def scroll_to(elem_id: str, align_to_top: bool | Var[bool] = True) -> EventSpec: - """Select the id of a html element for scrolling into view. - - Args: - elem_id: The id of the element to scroll to. - align_to_top: Whether to scroll to the top (True) or bottom (False) of the element. - - Returns: - An EventSpec to scroll the page to the selected element. - """ - get_element_by_id = FunctionStringVar.create("document.getElementById") - - return run_script( - get_element_by_id - .call(elem_id) - .to(ObjectVar) - .scrollIntoView.to(FunctionVar) - .call(align_to_top), - ) - - -def set_value(ref: str, value: Any) -> EventSpec: - """Set the value of a ref. - - Args: - ref: The ref. - value: The value to set. - - Returns: - An event to set the ref. - """ - return server_side( - "_set_value", - get_fn_signature(set_value), - ref=LiteralVar.create(format.format_ref(ref)), - value=value, - ) - - -def remove_cookie(key: str, options: dict[str, Any] | None = None) -> EventSpec: - """Remove a cookie on the frontend. - - Args: - key: The key identifying the cookie to be removed. - options: Support all the cookie options from RFC 6265 - - Returns: - EventSpec: An event to remove a cookie. - """ - options = options or {} - options["path"] = options.get("path", "/") - return server_side( - "_remove_cookie", - get_fn_signature(remove_cookie), - key=key, - options=options, - ) - - -def clear_local_storage() -> EventSpec: - """Set a value in the local storage on the frontend. - - Returns: - EventSpec: An event to clear the local storage. - """ - return server_side( - "_clear_local_storage", - get_fn_signature(clear_local_storage), - ) - - -def remove_local_storage(key: str) -> EventSpec: - """Set a value in the local storage on the frontend. - - Args: - key: The key identifying the variable in the local storage to remove. - - Returns: - EventSpec: An event to remove an item based on the provided key in local storage. - """ - return server_side( - "_remove_local_storage", - get_fn_signature(remove_local_storage), - key=key, - ) - - -def clear_session_storage() -> EventSpec: - """Set a value in the session storage on the frontend. - - Returns: - EventSpec: An event to clear the session storage. - """ - return server_side( - "_clear_session_storage", - get_fn_signature(clear_session_storage), - ) - - -def remove_session_storage(key: str) -> EventSpec: - """Set a value in the session storage on the frontend. - - Args: - key: The key identifying the variable in the session storage to remove. - - Returns: - EventSpec: An event to remove an item based on the provided key in session storage. - """ - return server_side( - "_remove_session_storage", - get_fn_signature(remove_session_storage), - key=key, - ) - - -def set_clipboard(content: str | Var[str]) -> EventSpec: - """Set the text in content in the clipboard. - - Args: - content: The text to add to clipboard. - - Returns: - EventSpec: An event to set some content in the clipboard. - """ - return run_script( - Var("navigator") - .to(dict) - .clipboard.to(dict) - .writeText.to(FunctionVar) - .call(content) - ) - - -def download( - url: str | Var | None = None, - filename: str | Var | None = None, - data: str | bytes | Var | None = None, - mime_type: str | Var | None = None, -) -> EventSpec: - """Download the file at a given path or with the specified data. - - Args: - url: The URL to the file to download. - filename: The name that the file should be saved as after download. - data: The data to download. - mime_type: The mime type of the data to download. - - Returns: - EventSpec: An event to download the associated file. - - Raises: - ValueError: If the URL provided is invalid, both URL and data are provided, - or the data is not an expected type. - """ - from reflex.components.core.cond import cond - - if isinstance(url, str): - if not url.startswith("/"): - msg = "The URL argument should start with a /" - raise ValueError(msg) - - # if filename is not provided, infer it from url - if filename is None: - filename = url.rpartition("/")[-1] - - if filename is None: - filename = "" - - if data is not None: - if url is not None: - msg = "Cannot provide both URL and data to download." - raise ValueError(msg) - - if isinstance(data, str): - if mime_type is None: - mime_type = "text/plain" - # Caller provided a plain text string to download. - url = f"data:{mime_type};base64," + b64encode(data.encode("utf-8")).decode( - "utf-8" - ) - elif isinstance(data, Var): - if mime_type is None: - mime_type = "text/plain" - # Need to check on the frontend if the Var already looks like a data: URI. - - is_data_url = (data.js_type() == "string") & ( - data.to(str).startswith("data:") - ) - - # If it's a data: URI, use it as is, otherwise convert the Var to JSON in a data: URI. - url = cond( - is_data_url, - data.to(str), - f"data:{mime_type}," + data.to_string(), - ) - elif isinstance(data, bytes): - if mime_type is None: - mime_type = "application/octet-stream" - # Caller provided bytes, so base64 encode it as a data: URI. - b64_data = b64encode(data).decode("utf-8") - url = f"data:{mime_type};base64," + b64_data - else: - msg = f"Invalid data type {type(data)} for download. Use `str` or `bytes`." - raise ValueError(msg) - - return server_side( - "_download", - get_fn_signature(download), - url=url, - filename=filename, - ) - - -def call_script( - javascript_code: str | Var[str], - callback: "EventType[Any] | None" = None, -) -> EventSpec: - """Create an event handler that executes arbitrary javascript code. - - Args: - javascript_code: The code to execute. - callback: EventHandler that will receive the result of evaluating the javascript code. - - Returns: - EventSpec: An event that will execute the client side javascript. - """ - callback_kwargs = {"callback": None} - if callback is not None: - callback_kwargs = { - "callback": str( - format.format_queue_events( - callback, - args_spec=lambda result: [result], - ) - ), - } - if isinstance(javascript_code, str): - # When there is VarData, include it and eval the JS code inline on the client. - javascript_code, original_code = ( - LiteralVar.create(javascript_code), - javascript_code, - ) - if not javascript_code._get_all_var_data(): - # Without VarData, cast to string and eval the code in the event loop. - javascript_code = str(Var(_js_expr=original_code)) - - return server_side( - "_call_script", - get_fn_signature(call_script), - javascript_code=javascript_code, - **callback_kwargs, - ) - - -def call_function( - javascript_code: str | Var, - callback: "EventType[Any] | None" = None, -) -> EventSpec: - """Create an event handler that executes arbitrary javascript code. - - Args: - javascript_code: The code to execute. - callback: EventHandler that will receive the result of evaluating the javascript code. - - Returns: - EventSpec: An event that will execute the client side javascript. - """ - callback_kwargs = {"callback": None} - if callback is not None: - callback_kwargs = { - "callback": str( - format.format_queue_events( - callback, - args_spec=lambda result: [result], - ), - ) - } - - javascript_code = ( - Var(javascript_code) if isinstance(javascript_code, str) else javascript_code - ) - - return server_side( - "_call_function", - get_fn_signature(call_function), - function=javascript_code, - **callback_kwargs, - ) - - -def run_script( - javascript_code: str | Var, - callback: "EventType[Any] | None" = None, -) -> EventSpec: - """Create an event handler that executes arbitrary javascript code. - - Args: - javascript_code: The code to execute. - callback: EventHandler that will receive the result of evaluating the javascript code. - - Returns: - EventSpec: An event that will execute the client side javascript. - """ - javascript_code = ( - Var(javascript_code) if isinstance(javascript_code, str) else javascript_code - ) - - return call_function(ArgsFunctionOperation.create((), javascript_code), callback) - - -def get_event(state: "BaseState", event: str): - """Get the event from the given state. - - Args: - state: The state. - event: The event. - - Returns: - The event. - """ - return f"{state.get_name()}.{event}" - - -def get_hydrate_event(state: "BaseState") -> str: - """Get the name of the hydrate event for the state. - - Args: - state: The state. - - Returns: - The name of the hydrate event. - """ - return get_event(state, constants.CompileVars.HYDRATE) - - -def _values_returned_from_event(event_spec_annotations: list[Any]) -> list[Any]: - return [ - event_spec_return_type - for event_spec_annotation in event_spec_annotations - if (event_spec_return_type := event_spec_annotation.get("return")) is not None - and get_origin(event_spec_return_type) is tuple - ] - - -def _check_event_args_subclass_of_callback( - callback_params_names: list[str], - provided_event_types: list[Any], - callback_param_name_to_type: dict[str, Any], - callback_name: str = "", - key: str = "", -): - """Check if the event handler arguments are subclass of the callback. - - Args: - callback_params_names: The names of the callback parameters. - provided_event_types: The event types. - callback_param_name_to_type: The callback parameter name to type mapping. - callback_name: The name of the callback. - key: The key. - - Raises: - TypeError: If the event handler arguments are invalid. - EventHandlerArgTypeMismatchError: If the event handler arguments do not match the callback. - - # noqa: DAR401 delayed_exceptions[] - # noqa: DAR402 EventHandlerArgTypeMismatchError - """ - from reflex.utils import console - - type_match_found: dict[str, bool] = {} - delayed_exceptions: list[EventHandlerArgTypeMismatchError] = [] - - for event_spec_index, event_spec_return_type in enumerate(provided_event_types): - args = get_args(event_spec_return_type) - - args_types_without_vars = [ - arg if get_origin(arg) is not Var else get_args(arg)[0] for arg in args - ] - - # check that args of event handler are matching the spec if type hints are provided - for i, arg in enumerate(callback_params_names[: len(args_types_without_vars)]): - if arg not in callback_param_name_to_type: - continue - - type_match_found.setdefault(arg, False) - - try: - compare_result = typehint_issubclass( - args_types_without_vars[i], callback_param_name_to_type[arg] - ) - except TypeError as te: - callback_name_context = f" of {callback_name}" if callback_name else "" - key_context = f" for {key}" if key else "" - msg = f"Could not compare types {args_types_without_vars[i]} and {callback_param_name_to_type[arg]} for argument {arg}{callback_name_context}{key_context}." - raise TypeError(msg) from te - - if compare_result: - type_match_found[arg] = True - continue - type_match_found[arg] = False - as_annotated_in = ( - f" as annotated in {callback_name}" if callback_name else "" - ) - delayed_exceptions.append( - EventHandlerArgTypeMismatchError( - f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {callback_param_name_to_type[arg]}{as_annotated_in} instead." - ) - ) - - if all(type_match_found.values()): - delayed_exceptions.clear() - if event_spec_index: - args = get_args(provided_event_types[0]) - - args_types_without_vars = [ - arg if get_origin(arg) is not Var else get_args(arg)[0] - for arg in args - ] - - expect_string = ", ".join( - repr(arg) for arg in args_types_without_vars - ).replace("[", "\\[") - - given_string = ", ".join( - repr(callback_param_name_to_type.get(arg, Any)) - for arg in callback_params_names - ).replace("[", "\\[") - - as_annotated_in = ( - f" as annotated in {callback_name}" if callback_name else "" - ) - - console.warn( - f"Event handler {key} expects ({expect_string}) -> () but got ({given_string}) -> (){as_annotated_in} instead. " - f"This may lead to unexpected behavior but is intentionally ignored for {key}." - ) - break - - if delayed_exceptions: - raise delayed_exceptions[0] - - -def call_event_handler( - event_callback: EventHandler | EventSpec, - event_spec: ArgsSpec | Sequence[ArgsSpec], - key: str | None = None, -) -> EventSpec: - """Call an event handler to get the event spec. - - This function will inspect the function signature of the event handler. - If it takes in an arg, the arg will be passed to the event handler. - Otherwise, the event handler will be called with no args. - - Args: - event_callback: The event handler. - event_spec: The lambda that define the argument(s) to pass to the event handler. - key: The key to pass to the event handler. - - Returns: - The event spec from calling the event handler. - """ - event_spec_args, event_annotations = parse_args_spec(event_spec) - - event_spec_return_types = _values_returned_from_event(event_annotations) - - if isinstance(event_callback, EventSpec): - parameters = event_callback.handler._parameters - - check_fn_match_arg_spec( - event_callback.handler.fn, - parameters, - event_spec_args, - key, - bool(event_callback.handler.state_full_name) + len(event_callback.args), - event_callback.handler.fn.__qualname__, - ) - - event_callback_spec_args = list(parameters) - - try: - type_hints_of_provided_callback = get_type_hints(event_callback.handler.fn) - except NameError: - type_hints_of_provided_callback = {} - - argument_names = [str(arg) for arg, value in event_callback.args] - - _check_event_args_subclass_of_callback( - [ - arg - for arg in event_callback_spec_args[ - bool(event_callback.handler.state_full_name) : - ] - if arg not in argument_names - ], - event_spec_return_types, - type_hints_of_provided_callback, - event_callback.handler.fn.__qualname__, - key or "", - ) - - # Handle partial application of EventSpec args - return event_callback.add_args(*event_spec_args) - - parameters = event_callback._parameters - - check_fn_match_arg_spec( - event_callback.fn, - parameters, - event_spec_args, - key, - bool(event_callback.state_full_name), - event_callback.fn.__qualname__, - ) - - if event_spec_return_types: - event_callback_spec_args = list(parameters) - - try: - type_hints_of_provided_callback = get_type_hints(event_callback.fn) - except NameError: - type_hints_of_provided_callback = {} - - _check_event_args_subclass_of_callback( - event_callback_spec_args[1:], - event_spec_return_types, - type_hints_of_provided_callback, - event_callback.fn.__qualname__, - key or "", - ) - - return event_callback(*event_spec_args) - - -def unwrap_var_annotation(annotation: GenericType): - """Unwrap a Var annotation or return it as is if it's not Var[X]. - - Args: - annotation: The annotation to unwrap. - - Returns: - The unwrapped annotation. - """ - if get_origin(annotation) in (Var, ObjectVar) and (args := get_args(annotation)): - return args[0] - return annotation - - -def resolve_annotation(annotations: dict[str, Any], arg_name: str, spec: ArgsSpec): - """Resolve the annotation for the given argument name. - - Args: - annotations: The annotations. - arg_name: The argument name. - spec: The specs which the annotations come from. - - Returns: - The resolved annotation. - - Raises: - MissingAnnotationError: If the annotation is missing for non-lambda methods. - """ - annotation = annotations.get(arg_name) - if annotation is None: - if not isinstance(spec, types.LambdaType): - raise MissingAnnotationError(var_name=arg_name) - return dict[str, dict] - return annotation - - -@lru_cache -def parse_args_spec(arg_spec: ArgsSpec | Sequence[ArgsSpec]): - """Parse the args provided in the ArgsSpec of an event trigger. - - Args: - arg_spec: The spec of the args. - - Returns: - The parsed args. - """ - # if there's multiple, the first is the default - if isinstance(arg_spec, Sequence): - annotations = [get_type_hints(one_arg_spec) for one_arg_spec in arg_spec] - arg_spec = arg_spec[0] - else: - annotations = [get_type_hints(arg_spec)] - - spec = inspect.getfullargspec(arg_spec) - - return list( - arg_spec(*[ - Var(f"_{l_arg}").to( - unwrap_var_annotation( - resolve_annotation(annotations[0], l_arg, spec=arg_spec) - ) - ) - for l_arg in spec.args - ]) - ), annotations - - -def args_specs_from_fields( - fields_dict: Mapping[str, BaseField], -) -> dict[str, ArgsSpec | Sequence[ArgsSpec]]: - """Get the event triggers and arg specs from the given fields. - - Args: - fields_dict: The fields, keyed by name - - Returns: - The args spec for any field annotated as EventHandler. - """ - return { - name: ( - metadata[0] - if ( - (metadata := getattr(field.annotated_type, "__metadata__", None)) - is not None - ) - else no_args_event_spec - ) - for name, field in fields_dict.items() - if field.type_origin is EventHandler - } - - -def check_fn_match_arg_spec( - user_func: Callable, - user_func_parameters: Mapping[str, inspect.Parameter], - event_spec_args: Sequence[Var], - key: str | None = None, - number_of_bound_args: int = 0, - func_name: str | None = None, -): - """Ensures that the function signature matches the passed argument specification - or raises an EventFnArgMismatchError if they do not. - - Args: - user_func: The function to be validated. - user_func_parameters: The parameters of the function to be validated. - event_spec_args: The argument specification for the event trigger. - key: The key of the event trigger. - number_of_bound_args: The number of bound arguments to the function. - func_name: The name of the function to be validated. - - Raises: - EventFnArgMismatchError: Raised if the number of mandatory arguments do not match - """ - user_args = list(user_func_parameters) - # Drop the first argument if it's a bound method - if inspect.ismethod(user_func) and user_func.__self__ is not None: - user_args = user_args[1:] - - user_default_args = [ - p.default - for p in user_func_parameters.values() - if p.default is not inspect.Parameter.empty - ] - number_of_user_args = len(user_args) - number_of_bound_args - number_of_user_default_args = len(user_default_args) if user_default_args else 0 - - number_of_event_args = len(event_spec_args) - - if number_of_user_args - number_of_user_default_args > number_of_event_args: - msg = ( - f"Event {key} only provides {number_of_event_args} arguments, but " - f"{func_name or user_func} requires at least {number_of_user_args - number_of_user_default_args} " - "arguments to be passed to the event handler.\n" - "See https://reflex.dev/docs/events/event-arguments/" - ) - raise EventFnArgMismatchError(msg) - - -def call_event_fn( - fn: Callable, - arg_spec: ArgsSpec | Sequence[ArgsSpec], - key: str | None = None, -) -> "list[EventSpec | FunctionVar | EventVar]": - """Call a function to a list of event specs. - - The function should return a single event-like value or a heterogeneous - sequence of event-like values. - - Args: - fn: The function to call. - arg_spec: The argument spec for the event trigger. - key: The key to pass to the event handler. - - Returns: - The event-like values from calling the function. - - Raises: - EventHandlerValueError: If the lambda returns an unusable value. - """ - # Import here to avoid circular imports. - from reflex.event import EventHandler, EventSpec - from reflex.utils.exceptions import EventHandlerValueError - - parsed_args, _ = parse_args_spec(arg_spec) - - parameters = inspect.signature(fn).parameters - - # Check that fn signature matches arg_spec - check_fn_match_arg_spec(fn, parameters, parsed_args, key=key) - - number_of_fn_args = len(parameters) - - # Call the function with the parsed args. - out = fn(*[*parsed_args][:number_of_fn_args]) - - # Normalize common heterogeneous event collections into individual events - # while keeping other scalar values for validation below. - out = list(out) if isinstance(out, (list, tuple)) else [out] - - # Convert any event specs to event specs. - events = [] - for e in out: - if isinstance(e, EventHandler): - # An un-called EventHandler gets all of the args of the event trigger. - e = call_event_handler(e, arg_spec, key=key) - - if isinstance(e, EventChain): - # Nested EventChain is treated like a FunctionVar. - e = Var.create(e) - - # Make sure the event spec is valid. - if not isinstance(e, (EventSpec, FunctionVar, EventVar)): - hint = "" - if isinstance(e, VarOperationCall): - hint = " Hint: use `fn.partial(...)` instead of calling the FunctionVar directly." - msg = ( - f"Invalid event chain for {key}: {fn} -> {e}: A lambda inside an EventChain " - "list must return `EventSpec | EventHandler | EventChain | EventVar | FunctionVar` " - "or a heterogeneous sequence of these types. " - f"Got: {type(e)}.{hint}" - ) - raise EventHandlerValueError(msg) - - # Add the event spec to the chain. - events.append(e) - - # Return the events. - return events - - -def get_handler_args( - event_spec: EventSpec, -) -> tuple[tuple[Var, Var], ...]: - """Get the handler args for the given event spec. - - Args: - event_spec: The event spec. - - Returns: - The handler args. - """ - args = event_spec.handler._parameters - - return event_spec.args if len(args) > 1 else () - - -def fix_events( - events: list[EventSpec | EventHandler] | None, - token: str, - router_data: dict[str, Any] | None = None, -) -> list[Event]: - """Fix a list of events returned by an event handler. - - Args: - events: The events to fix. - token: The user token. - router_data: The optional router data to set in the event. - - Returns: - The fixed events. - - Raises: - ValueError: If the event type is not what was expected. - """ - # If the event handler returns nothing, return an empty list. - if events is None: - return [] - - # If the handler returns a single event, wrap it in a list. - if not isinstance(events, list): - events = [events] - - # Fix the events created by the handler. - out = [] - for e in events: - if callable(e) and getattr(e, "__name__", "") == "": - # A lambda was returned, assume the user wants to call it with no args. - e = e() - if isinstance(e, Event): - # If the event is already an event, append it to the list. - out.append(e) - continue - if not isinstance(e, (EventHandler, EventSpec)): - e = EventHandler(fn=e) - # Otherwise, create an event from the event spec. - if isinstance(e, EventHandler): - e = e() - if not isinstance(e, EventSpec): - msg = f"Unexpected event type, {type(e)}." - raise ValueError(msg) - name = format.format_event_handler(e.handler) - payload = {k._js_expr: v._decode() for k, v in e.args} - - # Filter router_data to reduce payload size - event_router_data = { - k: v - for k, v in (router_data or {}).items() - if k in constants.route.ROUTER_DATA_INCLUDE - } - # Create an event and append it to the list. - out.append( - Event( - token=token, - name=name, - payload=payload, - router_data=event_router_data, - ) - ) - - return out - - -def get_fn_signature(fn: Callable) -> inspect.Signature: - """Get the signature of a function. - - Args: - fn: The function. - - Returns: - The signature of the function. - """ - signature = inspect.signature(fn) - new_param = inspect.Parameter( - FRONTEND_EVENT_STATE, inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Any - ) - return signature.replace(parameters=(new_param, *signature.parameters.values())) - - -# These chains can be used for their side effects when no other events are desired. -stop_propagation = noop().stop_propagation -prevent_default = noop().prevent_default - - -class EventVar(ObjectVar, python_types=(EventSpec, EventHandler)): - """Base class for event vars.""" - - def bool(self) -> NoReturn: - """Get the boolean value of the var. - - Raises: - TypeError: EventVar cannot be converted to a boolean. - """ - msg = f"Cannot convert {self._js_expr} of type {type(self).__name__} to bool." - raise TypeError(msg) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralEventVar(VarOperationCall, LiteralVar, EventVar): - """A literal event var.""" - - _var_value: EventSpec = dataclasses.field(default=None) # pyright: ignore [reportAssignmentType] - - def __hash__(self) -> int: - """Get the hash of the var. - - Returns: - The hash of the var. - """ - return hash((type(self).__name__, self._js_expr)) - - @classmethod - def create( - cls, - value: EventSpec | EventHandler, - _var_data: VarData | None = None, - ) -> "LiteralEventVar": - """Create a new LiteralEventVar instance. - - Args: - value: The value of the var. - _var_data: The data of the var. - - Returns: - The created LiteralEventVar instance. - - Raises: - EventFnArgMismatchError: If the event handler takes arguments. - """ - if isinstance(value, EventHandler): - - def no_args(): - return () - - try: - value = call_event_handler(value, no_args) - except EventFnArgMismatchError: - msg = f"Event handler {value.fn.__qualname__} used inside of a rx.cond() must not take any arguments." - raise EventFnArgMismatchError(msg) from None - - return cls( - _js_expr="", - _var_type=EventSpec, - _var_data=_var_data, - _var_value=value, - _func=FunctionStringVar("ReflexEvent"), - _args=( - # event handler name - ".".join( - filter( - None, - format.get_event_handler_parts(value.handler), - ) - ), - # event handler args - {str(name): value for name, value in value.args}, - # event actions - value.event_actions, - # client handler name - *([value.client_handler_name] if value.client_handler_name else []), - ), - ) - - -class EventChainVar(BuilderFunctionVar, python_types=EventChain): - """Base class for event chain vars.""" - - def bool(self) -> NoReturn: - """Get the boolean value of the var. - - Raises: - TypeError: EventChainVar cannot be converted to a boolean. - """ - msg = f"Cannot convert {self._js_expr} of type {type(self).__name__} to bool." - raise TypeError(msg) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -# Note: LiteralVar is second in the inheritance list allowing it act like a -# CachedVarOperation (ArgsFunctionOperation) and get the _js_expr from the -# _cached_var_name property. -class LiteralEventChainVar(ArgsFunctionOperationBuilder, LiteralVar, EventChainVar): - """A literal event chain var.""" - - _var_value: EventChain = dataclasses.field(default=None) # pyright: ignore [reportAssignmentType] - - def __hash__(self) -> int: - """Get the hash of the var. - - Returns: - The hash of the var. - """ - return hash((type(self).__name__, self._js_expr)) - - @classmethod - def create( - cls, - value: EventChain, - _var_data: VarData | None = None, - ) -> "LiteralEventChainVar": - """Create a new LiteralEventChainVar instance. - - Args: - value: The value of the var. - _var_data: The data of the var. - - Returns: - The created LiteralEventChainVar instance. - - Raises: - ValueError: If the invocation is not a FunctionVar. - """ - arg_spec = ( - value.args_spec[0] - if isinstance(value.args_spec, Sequence) - else value.args_spec - ) - sig = inspect.signature(arg_spec) # pyright: ignore [reportArgumentType] - arg_vars = () - if sig.parameters: - arg_def = tuple(f"_{p}" for p in sig.parameters) - arg_vars = tuple(Var(_js_expr=arg) for arg in arg_def) - arg_def_expr = LiteralVar.create(list(arg_vars)) - else: - # add a default argument for addEvents if none were specified in value.args_spec - # used to trigger the preventDefault() on the event. - arg_def = ("...args",) - arg_def_expr = Var(_js_expr="args") - - if value.invocation is None: - invocation = FunctionStringVar.create( - CompileVars.ADD_EVENTS, - _var_data=VarData( - imports=Imports.EVENTS, - hooks={Hooks.EVENTS: None}, - ), - ) - else: - invocation = value.invocation - - if invocation is not None and not isinstance(invocation, FunctionVar): - msg = f"EventChain invocation must be a FunctionVar, got {invocation!s} of type {invocation._var_type!s}." - raise ValueError(msg) - assert invocation is not None - - call_args = arg_vars if sig.parameters else (Var(_js_expr="...args"),) - statements = [ - ( - event.call(*call_args) - if isinstance(event, FunctionVar) - else invocation.call( - LiteralVar.create([LiteralVar.create(event)]), - arg_def_expr, - _EMPTY_EVENT_ACTIONS, - ) - ) - for event in value.events - ] - - if not statements: - statements.append( - invocation.call( - _EMPTY_EVENTS, - arg_def_expr, - _EMPTY_EVENT_ACTIONS, - ) - ) - - if len(statements) == 1 and not value.event_actions: - return_expr = statements[0] - else: - statement_block = Var( - _js_expr=f"{{{''.join(f'{statement};' for statement in statements)}}}", - ) - if value.event_actions: - apply_event_actions = FunctionStringVar.create( - CompileVars.APPLY_EVENT_ACTIONS, - _var_data=VarData( - imports=Imports.EVENTS, - hooks={Hooks.EVENTS: None}, - ), - ) - return_expr = apply_event_actions.call( - ArgsFunctionOperation.create((), statement_block), - value.event_actions, - *call_args, - ) - else: - return_expr = statement_block - - return cls( - _js_expr="", - _var_type=EventChain, - _var_data=_var_data, - _args=FunctionArgs(arg_def), - _return_expr=return_expr, - _var_value=value, - ) - - -P = TypeVarTuple("P") -Q = TypeVarTuple("Q") -V = TypeVar("V") -V2 = TypeVar("V2") -V3 = TypeVar("V3") -V4 = TypeVar("V4") -V5 = TypeVar("V5") - - -class EventCallback(Generic[Unpack[P]], EventActionsMixin): - """A descriptor that wraps a function to be used as an event.""" - - def __init__(self, func: Callable[[Any, Unpack[P]], Any]): - """Initialize the descriptor with the function to be wrapped. - - Args: - func: The function to be wrapped. - """ - self.func = func - - @overload - def __call__( - self: "EventCallback[Unpack[Q]]", - ) -> "EventCallback[Unpack[Q]]": ... - - @overload - def __call__( - self: "EventCallback[V, Unpack[Q]]", value: V | Var[V] - ) -> "EventCallback[Unpack[Q]]": ... - - @overload - def __call__( - self: "EventCallback[V, V2, Unpack[Q]]", - value: V | Var[V], - value2: V2 | Var[V2], - ) -> "EventCallback[Unpack[Q]]": ... - - @overload - def __call__( - self: "EventCallback[V, V2, V3, Unpack[Q]]", - value: V | Var[V], - value2: V2 | Var[V2], - value3: V3 | Var[V3], - ) -> "EventCallback[Unpack[Q]]": ... - - @overload - def __call__( - self: "EventCallback[V, V2, V3, V4, Unpack[Q]]", - value: V | Var[V], - value2: V2 | Var[V2], - value3: V3 | Var[V3], - value4: V4 | Var[V4], - ) -> "EventCallback[Unpack[Q]]": ... - - def __call__(self, *values) -> "EventCallback": # pyright: ignore [reportInconsistentOverload] - """Call the function with the values. - - Args: - *values: The values to call the function with. - - Returns: - The function with the values. - """ - return self.func(*values) # pyright: ignore [reportArgumentType] - - @overload - def __get__( - self: "EventCallback[Unpack[P]]", instance: None, owner: Any - ) -> "EventCallback[Unpack[P]]": ... - - @overload - def __get__(self, instance: Any, owner: Any) -> "Callable[[Unpack[P]]]": ... - - def __get__(self, instance: Any, owner: Any) -> Callable: - """Get the function with the instance bound to it. - - Args: - instance: The instance to bind to the function. - owner: The owner of the function. - - Returns: - The function with the instance bound to it - """ - if instance is None: - return self.func - - return partial(self.func, instance) - - -class LambdaEventCallback(Protocol[Unpack[P]]): - """A protocol for a lambda event callback.""" - - __code__: types.CodeType - - @overload - def __call__(self: "LambdaEventCallback[()]") -> Any: ... - - @overload - def __call__(self: "LambdaEventCallback[V]", value: "Var[V]", /) -> Any: ... - - @overload - def __call__( - self: "LambdaEventCallback[V, V2]", value: Var[V], value2: Var[V2], / - ) -> Any: ... - - @overload - def __call__( - self: "LambdaEventCallback[V, V2, V3]", - value: Var[V], - value2: Var[V2], - value3: Var[V3], - /, - ) -> Any: ... - - def __call__(self, *args: Var) -> Any: - """Call the lambda with the args. - - Args: - *args: The args to call the lambda with. - """ - - -ARGS = TypeVarTuple("ARGS") - - -LAMBDA_OR_STATE = TypeAliasType( - "LAMBDA_OR_STATE", - LambdaEventCallback[Unpack[ARGS]] | EventCallback[Unpack[ARGS]], - type_params=(ARGS,), -) - -ItemOrList = V | list[V] - -BASIC_EVENT_TYPES = TypeAliasType( - "BASIC_EVENT_TYPES", EventSpec | EventHandler | Var[Any], type_params=() -) - -IndividualEventType = TypeAliasType( - "IndividualEventType", - LAMBDA_OR_STATE[Unpack[ARGS]] | BASIC_EVENT_TYPES, - type_params=(ARGS,), -) - -EventType = TypeAliasType( - "EventType", - ItemOrList[LAMBDA_OR_STATE[Unpack[ARGS]] | BASIC_EVENT_TYPES], - type_params=(ARGS,), -) - - -if TYPE_CHECKING: - from reflex.state import BaseState - - BASE_STATE = TypeVar("BASE_STATE", bound=BaseState) -else: - BASE_STATE = TypeVar("BASE_STATE") - - -class EventNamespace: - """A namespace for event related classes.""" - - # Core Event Classes - Event = Event - EventActionsMixin = EventActionsMixin - EventHandler = EventHandler - EventSpec = EventSpec - CallableEventSpec = CallableEventSpec - EventChain = EventChain - EventVar = EventVar - LiteralEventVar = LiteralEventVar - EventChainVar = EventChainVar - LiteralEventChainVar = LiteralEventChainVar - EventCallback = EventCallback - LambdaEventCallback = LambdaEventCallback - - # Javascript Event Classes - JavascriptHTMLInputElement = JavascriptHTMLInputElement - JavascriptInputEvent = JavascriptInputEvent - JavascriptKeyboardEvent = JavascriptKeyboardEvent - JavascriptMouseEvent = JavascriptMouseEvent - JavascriptPointerEvent = JavascriptPointerEvent - - # Type Info Classes - KeyInputInfo = KeyInputInfo - MouseEventInfo = MouseEventInfo - PointerEventInfo = PointerEventInfo - IdentityEventReturn = IdentityEventReturn - - # File Upload - FileUpload = FileUpload - UploadFilesChunk = UploadFilesChunk - - # Type Aliases - EventType = EventType - LAMBDA_OR_STATE = LAMBDA_OR_STATE - BASIC_EVENT_TYPES = BASIC_EVENT_TYPES - IndividualEventType = IndividualEventType - - # Constants - BACKGROUND_TASK_MARKER = BACKGROUND_TASK_MARKER - EVENT_ACTIONS_MARKER = EVENT_ACTIONS_MARKER - _EVENT_FIELDS = _EVENT_FIELDS - FORM_DATA = FORM_DATA - upload_files = upload_files - upload_files_chunk = upload_files_chunk - stop_propagation = stop_propagation - prevent_default = prevent_default - - # Private/Internal Functions - resolve_upload_handler_param = staticmethod(resolve_upload_handler_param) - resolve_upload_chunk_handler_param = staticmethod( - resolve_upload_chunk_handler_param - ) - _values_returned_from_event = staticmethod(_values_returned_from_event) - _check_event_args_subclass_of_callback = staticmethod( - _check_event_args_subclass_of_callback - ) - - @overload - def __new__( - cls, - func: None = None, - *, - background: bool | None = None, - stop_propagation: bool | None = None, - prevent_default: bool | None = None, - throttle: int | None = None, - debounce: int | None = None, - temporal: bool | None = None, - ) -> Callable[ - [Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]] # pyright: ignore [reportInvalidTypeVarUse] - ]: ... - - @overload - def __new__( - cls, - func: Callable[[BASE_STATE, Unpack[P]], Any], - *, - background: bool | None = None, - stop_propagation: bool | None = None, - prevent_default: bool | None = None, - throttle: int | None = None, - debounce: int | None = None, - temporal: bool | None = None, - ) -> EventCallback[Unpack[P]]: ... - - def __new__( - cls, - func: Callable[[BASE_STATE, Unpack[P]], Any] | None = None, - *, - background: bool | None = None, - stop_propagation: bool | None = None, - prevent_default: bool | None = None, - throttle: int | None = None, - debounce: int | None = None, - temporal: bool | None = None, - ) -> ( - EventCallback[Unpack[P]] - | Callable[[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]]] - ): - """Wrap a function to be used as an event. - - Args: - func: The function to wrap. - background: Whether the event should be run in the background. Defaults to False. - stop_propagation: Whether to stop the event from bubbling up the DOM tree. - prevent_default: Whether to prevent the default behavior of the event. - throttle: Throttle the event handler to limit calls (in milliseconds). - debounce: Debounce the event handler to delay calls (in milliseconds). - temporal: Whether the event should be dropped when the backend is down. - - Returns: - The wrapped function. - - Raises: - TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402 - """ - - def _build_event_actions(): - """Build event_actions dict from decorator parameters. - - Returns: - Dict of event actions to apply, or empty dict if none specified. - """ - if not any([ - stop_propagation, - prevent_default, - throttle, - debounce, - temporal, - ]): - return {} - - event_actions = {} - if stop_propagation is not None: - event_actions["stopPropagation"] = stop_propagation - if prevent_default is not None: - event_actions["preventDefault"] = prevent_default - if throttle is not None: - event_actions["throttle"] = throttle - if debounce is not None: - event_actions["debounce"] = debounce - if temporal is not None: - event_actions["temporal"] = temporal - return event_actions - - def wrapper( - func: Callable[[BASE_STATE, Unpack[P]], T], - ) -> EventCallback[Unpack[P]]: - if background is True: - if not inspect.iscoroutinefunction( - func - ) and not inspect.isasyncgenfunction(func): - msg = "Background task must be async function or generator." - raise TypeError(msg) - setattr(func, BACKGROUND_TASK_MARKER, True) - if getattr(func, "__name__", "").startswith("_"): - msg = "Event handlers cannot be private." - raise ValueError(msg) - - qualname: str | None = getattr(func, "__qualname__", None) - - if qualname and ( - len(func_path := qualname.split(".")) == 1 - or func_path[-2] == "" - ): - from reflex.state import BaseState - - types = get_type_hints(func) - state_arg_name = next(iter(inspect.signature(func).parameters), None) - state_cls = state_arg_name and types.get(state_arg_name) - if state_cls and issubclass(state_cls, BaseState): - name = ( - (func.__module__ + "." + qualname) - .replace(".", "_") - .replace("", "_") - .removeprefix("_") - ) - object.__setattr__(func, "__name__", name) - object.__setattr__(func, "__qualname__", name) - state_cls._add_event_handler(name, func) - event_callback = getattr(state_cls, name) - - # Apply decorator event actions - event_actions = _build_event_actions() - if event_actions: - # Create new EventCallback with updated event_actions - event_callback = dataclasses.replace( - event_callback, event_actions=event_actions - ) - - return event_callback - - # Store decorator event actions on the function for later processing - event_actions = _build_event_actions() - if event_actions: - setattr(func, EVENT_ACTIONS_MARKER, event_actions) - return func # pyright: ignore [reportReturnType] - - if func is not None: - return wrapper(func) - return wrapper - - get_event = staticmethod(get_event) - get_hydrate_event = staticmethod(get_hydrate_event) - fix_events = staticmethod(fix_events) - call_event_handler = staticmethod(call_event_handler) - call_event_fn = staticmethod(call_event_fn) - get_handler_args = staticmethod(get_handler_args) - check_fn_match_arg_spec = staticmethod(check_fn_match_arg_spec) - resolve_annotation = staticmethod(resolve_annotation) - parse_args_spec = staticmethod(parse_args_spec) - args_specs_from_fields = staticmethod(args_specs_from_fields) - unwrap_var_annotation = staticmethod(unwrap_var_annotation) - get_fn_signature = staticmethod(get_fn_signature) - - # Event Spec Functions - passthrough_event_spec = staticmethod(passthrough_event_spec) - input_event = staticmethod(input_event) - int_input_event = staticmethod(int_input_event) - float_input_event = staticmethod(float_input_event) - checked_input_event = staticmethod(checked_input_event) - key_event = staticmethod(key_event) - pointer_event_spec = staticmethod(pointer_event_spec) - no_args_event_spec = staticmethod(no_args_event_spec) - on_submit_event = staticmethod(on_submit_event) - on_submit_string_event = staticmethod(on_submit_string_event) - - # Server Side Events - server_side = staticmethod(server_side) - redirect = staticmethod(redirect) - console_log = staticmethod(console_log) - noop = staticmethod(noop) - back = staticmethod(back) - window_alert = staticmethod(window_alert) - set_focus = staticmethod(set_focus) - blur_focus = staticmethod(blur_focus) - scroll_to = staticmethod(scroll_to) - set_value = staticmethod(set_value) - remove_cookie = staticmethod(remove_cookie) - clear_local_storage = staticmethod(clear_local_storage) - remove_local_storage = staticmethod(remove_local_storage) - clear_session_storage = staticmethod(clear_session_storage) - remove_session_storage = staticmethod(remove_session_storage) - set_clipboard = staticmethod(set_clipboard) - download = staticmethod(download) - call_script = staticmethod(call_script) - call_function = staticmethod(call_function) - run_script = staticmethod(run_script) - __file__ = __file__ - - -event = EventNamespace -event.event = event # pyright: ignore[reportAttributeAccessIssue] sys.modules[__name__] = event # pyright: ignore[reportArgumentType] diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index 8ae5f3ee8a6..1dbaf20ae88 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -2,8 +2,9 @@ from types import SimpleNamespace -from reflex.components.datadisplay.shiki_code_block import code_block as code_block -from reflex.utils.console import warn +from reflex_components_code.shiki_code_block import code_block as code_block +from reflex_core.utils.console import warn + from reflex.utils.misc import run_in_thread from . import hooks as hooks diff --git a/reflex/experimental/client_state.py b/reflex/experimental/client_state.py index 7e7063fbd97..8f5b6565612 100644 --- a/reflex/experimental/client_state.py +++ b/reflex/experimental/client_state.py @@ -7,12 +7,12 @@ from collections.abc import Callable from typing import Any -from reflex import constants -from reflex.event import EventChain, EventHandler, EventSpec, run_script -from reflex.utils.imports import ImportVar -from reflex.vars import VarData, get_unique_variable_name -from reflex.vars.base import LiteralVar, Var -from reflex.vars.function import ArgsFunctionOperationBuilder, FunctionVar +from reflex_core import constants +from reflex_core.event import EventChain, EventHandler, EventSpec, run_script +from reflex_core.utils.imports import ImportVar +from reflex_core.vars import VarData, get_unique_variable_name +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.function import ArgsFunctionOperationBuilder, FunctionVar NoValue = object() diff --git a/reflex/experimental/hooks.py b/reflex/experimental/hooks.py index c00dd3bf925..131680b7772 100644 --- a/reflex/experimental/hooks.py +++ b/reflex/experimental/hooks.py @@ -2,9 +2,9 @@ from __future__ import annotations -from reflex.utils.imports import ImportVar -from reflex.vars import VarData -from reflex.vars.base import Var +from reflex_core.utils.imports import ImportVar +from reflex_core.vars import VarData +from reflex_core.vars.base import Var def _compose_react_imports(tags: list[str]) -> dict[str, list[ImportVar]]: diff --git a/reflex/experimental/memo.py b/reflex/experimental/memo.py index ee1912d1757..c145f85cc1d 100644 --- a/reflex/experimental/memo.py +++ b/reflex/experimental/memo.py @@ -8,26 +8,27 @@ from functools import cache, update_wrapper from typing import Any, get_args, get_origin, get_type_hints -from reflex import constants -from reflex.components.base.bare import Bare -from reflex.components.base.fragment import Fragment -from reflex.components.component import Component -from reflex.components.dynamic import bundled_libraries -from reflex.constants.compiler import SpecialAttributes -from reflex.constants.state import CAMEL_CASE_MEMO_MARKER -from reflex.utils import format -from reflex.utils import types as type_utils -from reflex.utils.imports import ImportVar -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var -from reflex.vars.function import ( +from reflex_components_core.base.bare import Bare +from reflex_components_core.base.fragment import Fragment +from reflex_core import constants +from reflex_core.components.component import Component +from reflex_core.components.dynamic import bundled_libraries +from reflex_core.constants.compiler import SpecialAttributes +from reflex_core.constants.state import CAMEL_CASE_MEMO_MARKER +from reflex_core.utils import format +from reflex_core.utils.imports import ImportVar +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.function import ( ArgsFunctionOperation, DestructuredArg, FunctionStringVar, FunctionVar, ReflexCallable, ) -from reflex.vars.object import RestProp +from reflex_core.vars.object import RestProp + +from reflex.utils import types as type_utils @dataclasses.dataclass(frozen=True, slots=True, kw_only=True) diff --git a/reflex/istate/data.py b/reflex/istate/data.py index ba75a8df4dc..0ff2fd0c6b9 100644 --- a/reflex/istate/data.py +++ b/reflex/istate/data.py @@ -6,9 +6,9 @@ from typing import TYPE_CHECKING from urllib.parse import _NetlocResultMixinStr, parse_qsl, urlsplit -from reflex import constants -from reflex.utils import console, format -from reflex.utils.serializers import serializer +from reflex_core import constants +from reflex_core.utils import console, format +from reflex_core.utils.serializers import serializer @dataclasses.dataclass(frozen=True, init=False) diff --git a/reflex/istate/manager/__init__.py b/reflex/istate/manager/__init__.py index 327d08811b0..cde9ca5d32d 100644 --- a/reflex/istate/manager/__init__.py +++ b/reflex/istate/manager/__init__.py @@ -6,14 +6,14 @@ from collections.abc import AsyncIterator from typing import TypedDict +from reflex_core import constants +from reflex_core.config import get_config +from reflex_core.event import Event +from reflex_core.utils.exceptions import InvalidStateManagerModeError from typing_extensions import ReadOnly, Unpack -from reflex import constants -from reflex.config import get_config -from reflex.event import Event from reflex.state import BaseState from reflex.utils import console, prerequisites -from reflex.utils.exceptions import InvalidStateManagerModeError class StateModificationContext(TypedDict, total=False): diff --git a/reflex/istate/manager/disk.py b/reflex/istate/manager/disk.py index 30db2f25481..17d66ca9762 100644 --- a/reflex/istate/manager/disk.py +++ b/reflex/istate/manager/disk.py @@ -9,9 +9,9 @@ from hashlib import md5 from pathlib import Path +from reflex_core.environment import environment from typing_extensions import Unpack, override -from reflex.environment import environment from reflex.istate.manager import ( StateManager, StateModificationContext, diff --git a/reflex/istate/manager/redis.py b/reflex/istate/manager/redis.py index 35b87b92eaa..a6dd72321c5 100644 --- a/reflex/istate/manager/redis.py +++ b/reflex/istate/manager/redis.py @@ -13,22 +13,22 @@ from redis import ResponseError from redis.asyncio import Redis +from reflex_core.config import get_config +from reflex_core.environment import environment +from reflex_core.utils import console +from reflex_core.utils.exceptions import ( + InvalidLockWarningThresholdError, + LockExpiredError, + StateSchemaMismatchError, +) from typing_extensions import Unpack, override -from reflex.config import get_config -from reflex.environment import environment from reflex.istate.manager import ( StateManager, StateModificationContext, _default_token_expiration, ) from reflex.state import BaseState, _split_substate_key, _substate_key -from reflex.utils import console -from reflex.utils.exceptions import ( - InvalidLockWarningThresholdError, - LockExpiredError, - StateSchemaMismatchError, -) from reflex.utils.tasks import ensure_task diff --git a/reflex/istate/proxy.py b/reflex/istate/proxy.py index 7f9cb78394b..df1feeb162d 100644 --- a/reflex/istate/proxy.py +++ b/reflex/istate/proxy.py @@ -15,14 +15,13 @@ from typing import TYPE_CHECKING, Any, SupportsIndex, TypeVar import wrapt +from reflex_core.event import Event +from reflex_core.utils.exceptions import ImmutableStateError +from reflex_core.utils.serializers import can_serialize, serialize, serializer +from reflex_core.vars.base import Var from typing_extensions import Self -from reflex.base import Base -from reflex.event import Event from reflex.utils import prerequisites -from reflex.utils.exceptions import ImmutableStateError -from reflex.utils.serializers import can_serialize, serialize, serializer -from reflex.vars.base import Var if TYPE_CHECKING: from reflex.state import BaseState, StateUpdate @@ -351,20 +350,10 @@ def mark_dirty(self): raise NotImplementedError(msg) -if find_spec("pydantic"): - import pydantic - - NEVER_WRAP_BASE_ATTRS = set(Base.__dict__) - {"set"} | set( - pydantic.BaseModel.__dict__ - ) -else: - NEVER_WRAP_BASE_ATTRS = {} - MUTABLE_TYPES = ( list, dict, set, - Base, ) if find_spec("sqlalchemy"): @@ -373,10 +362,9 @@ def mark_dirty(self): MUTABLE_TYPES += (DeclarativeBase,) if find_spec("pydantic"): - from pydantic import BaseModel as BaseModelV2 - from pydantic.v1 import BaseModel as BaseModelV1 + from pydantic import BaseModel - MUTABLE_TYPES += (BaseModelV1, BaseModelV2) + MUTABLE_TYPES += (BaseModel,) class MutableProxy(wrapt.ObjectProxy): @@ -577,11 +565,7 @@ def __getattr__(self, __name: str) -> Any: ) if ( - ( - not isinstance(self.__wrapped__, Base) - or __name not in NEVER_WRAP_BASE_ATTRS - ) - and (func := getattr(value, "__func__", None)) is not None + (func := getattr(value, "__func__", None)) is not None and not inspect.isclass(getattr(value, "__self__", None)) # skip SQLAlchemy instrumented methods and not getattr(value, "_sa_instrumented", False) diff --git a/reflex/istate/shared.py b/reflex/istate/shared.py index 01bf154d913..b62056167f0 100644 --- a/reflex/istate/shared.py +++ b/reflex/istate/shared.py @@ -5,11 +5,12 @@ from collections.abc import AsyncIterator from typing import Self, TypeVar -from reflex.constants import ROUTER_DATA -from reflex.event import Event, get_hydrate_event +from reflex_core.constants import ROUTER_DATA +from reflex_core.event import Event, get_hydrate_event +from reflex_core.utils import console +from reflex_core.utils.exceptions import ReflexRuntimeError + from reflex.state import BaseState, State, _override_base_method, _substate_key -from reflex.utils import console -from reflex.utils.exceptions import ReflexRuntimeError UPDATE_OTHER_CLIENT_TASKS: set[asyncio.Task] = set() LINKED_STATE = TypeVar("LINKED_STATE", bound="SharedStateBaseInternal") diff --git a/reflex/istate/storage.py b/reflex/istate/storage.py index a3dd98bafbf..c0cfb1b775a 100644 --- a/reflex/istate/storage.py +++ b/reflex/istate/storage.py @@ -4,7 +4,7 @@ from typing import Any -from reflex.utils import format +from reflex_core.utils import format class ClientStorageBase: diff --git a/reflex/middleware/hydrate_middleware.py b/reflex/middleware/hydrate_middleware.py index ec18939dee3..f5b75057050 100644 --- a/reflex/middleware/hydrate_middleware.py +++ b/reflex/middleware/hydrate_middleware.py @@ -5,8 +5,9 @@ import dataclasses from typing import TYPE_CHECKING -from reflex import constants -from reflex.event import Event, get_hydrate_event +from reflex_core import constants +from reflex_core.event import Event, get_hydrate_event + from reflex.middleware.middleware import Middleware from reflex.state import BaseState, StateUpdate, _resolve_delta diff --git a/reflex/middleware/middleware.py b/reflex/middleware/middleware.py index 6a5843b181e..f31761011cc 100644 --- a/reflex/middleware/middleware.py +++ b/reflex/middleware/middleware.py @@ -5,7 +5,8 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING -from reflex.event import Event +from reflex_core.event import Event + from reflex.state import BaseState, StateUpdate if TYPE_CHECKING: diff --git a/reflex/model.py b/reflex/model.py index af6c3805e02..95650842a11 100644 --- a/reflex/model.py +++ b/reflex/model.py @@ -8,11 +8,12 @@ from importlib.util import find_spec from typing import TYPE_CHECKING, Any, ClassVar -from reflex.config import get_config -from reflex.environment import environment -from reflex.utils import console +from reflex_core.config import get_config +from reflex_core.environment import environment +from reflex_core.utils import console +from reflex_core.utils.serializers import serializer + from reflex.utils.compat import sqlmodel_field_has_primary_key -from reflex.utils.serializers import serializer if TYPE_CHECKING: from typing import TypeVar diff --git a/reflex/page.py b/reflex/page.py index 9b09682436f..13ca05f803d 100644 --- a/reflex/page.py +++ b/reflex/page.py @@ -10,7 +10,7 @@ from collections.abc import Callable from typing import Any - from reflex.event import EventType + from reflex_core.event import EventType DECORATED_PAGES: dict[str, list] = defaultdict(list) @@ -45,7 +45,7 @@ def page( Returns: The decorated function. """ - from reflex.config import get_config + from reflex_core.config import get_config def decorator(render_fn: Callable): kwargs = {} diff --git a/reflex/plugins/__init__.py b/reflex/plugins/__init__.py index 754409046b8..b796e670257 100644 --- a/reflex/plugins/__init__.py +++ b/reflex/plugins/__init__.py @@ -1,10 +1,15 @@ -"""Reflex Plugin System.""" +"""Re-export from reflex_core.plugins.""" -from ._screenshot import ScreenshotPlugin as _ScreenshotPlugin -from .base import CommonContext, Plugin, PreCompileContext -from .sitemap import SitemapPlugin -from .tailwind_v3 import TailwindV3Plugin -from .tailwind_v4 import TailwindV4Plugin +from reflex_core.plugins import * +from reflex_core.plugins import ( + CommonContext, + Plugin, + PreCompileContext, + SitemapPlugin, + TailwindV3Plugin, + TailwindV4Plugin, + _ScreenshotPlugin, +) __all__ = [ "CommonContext", diff --git a/reflex/plugins/_screenshot.py b/reflex/plugins/_screenshot.py index f0741c9fd0e..60041fc06dd 100644 --- a/reflex/plugins/_screenshot.py +++ b/reflex/plugins/_screenshot.py @@ -1,144 +1,3 @@ -"""Plugin to enable screenshot functionality.""" +"""Re-export from reflex_core.plugins._screenshot.""" -from typing import TYPE_CHECKING - -from reflex.plugins.base import Plugin as BasePlugin - -if TYPE_CHECKING: - from starlette.requests import Request - from starlette.responses import Response - from typing_extensions import Unpack - - from reflex.app import App - from reflex.plugins.base import PostCompileContext - from reflex.state import BaseState - -ACTIVE_CONNECTIONS = "/_active_connections" -CLONE_STATE = "/_clone_state" - - -def _deep_copy(state: "BaseState") -> "BaseState": - """Create a deep copy of the state. - - Args: - state: The state to copy. - - Returns: - A deep copy of the state. - """ - import copy - - copy_of_state = copy.deepcopy(state) - - def copy_substate(substate: "BaseState") -> "BaseState": - substate_copy = _deep_copy(substate) - - substate_copy.parent_state = copy_of_state - - return substate_copy - - copy_of_state.substates = { - substate_name: copy_substate(substate) - for substate_name, substate in state.substates.items() - } - - return copy_of_state - - -class ScreenshotPlugin(BasePlugin): - """Plugin to handle screenshot functionality.""" - - def post_compile(self, **context: "Unpack[PostCompileContext]") -> None: - """Called after the compilation of the plugin. - - Args: - context: The context for the plugin. - """ - app = context["app"] - self._add_active_connections_endpoint(app) - self._add_clone_state_endpoint(app) - - @staticmethod - def _add_active_connections_endpoint(app: "App") -> None: - """Add an endpoint to the app that returns the active connections. - - Args: - app: The application instance to which the endpoint will be added. - """ - if not app._api: - return - - def active_connections(_request: "Request") -> "Response": - from starlette.responses import JSONResponse - - if not app.event_namespace: - return JSONResponse({}) - - return JSONResponse(app.event_namespace.token_to_sid) - - app._api.add_route( - ACTIVE_CONNECTIONS, - active_connections, - methods=["GET"], - ) - - @staticmethod - def _add_clone_state_endpoint(app: "App") -> None: - """Add an endpoint to the app that clones the current state. - - Args: - app: The application instance to which the endpoint will be added. - """ - if not app._api: - return - - async def clone_state(request: "Request") -> "Response": - import uuid - - from starlette.responses import JSONResponse - - from reflex.state import _substate_key - - if not app.event_namespace: - return JSONResponse({}) - - token_to_clone = await request.json() - - if not isinstance(token_to_clone, str): - return JSONResponse( - {"error": "Token to clone must be a string."}, status_code=400 - ) - - old_state = await app.state_manager.get_state(token_to_clone) - - new_state = _deep_copy(old_state) - - new_token = uuid.uuid4().hex - - all_states = [new_state] - - found_new = True - - while found_new: - found_new = False - - for state in list(all_states): - for substate in state.substates.values(): - substate._was_touched = True - - if substate not in all_states: - all_states.append(substate) - - found_new = True - - await app.state_manager.set_state( - _substate_key(new_token, new_state), new_state - ) - - return JSONResponse(new_token) - - app._api.add_route( - CLONE_STATE, - clone_state, - methods=["POST"], - ) +from reflex_core.plugins._screenshot import * diff --git a/reflex/plugins/base.py b/reflex/plugins/base.py index 52dfa8d7805..fb786a6ca47 100644 --- a/reflex/plugins/base.py +++ b/reflex/plugins/base.py @@ -1,126 +1,3 @@ -"""Base class for all plugins.""" +"""Re-export from reflex_core.plugins.base.""" -from collections.abc import Callable, Sequence -from pathlib import Path -from typing import TYPE_CHECKING, ParamSpec, Protocol, TypedDict - -from typing_extensions import Unpack - -if TYPE_CHECKING: - from reflex.app import App, UnevaluatedPage - - -class CommonContext(TypedDict): - """Common context for all plugins.""" - - -P = ParamSpec("P") - - -class AddTaskProtocol(Protocol): - """Protocol for adding a task to the pre-compile context.""" - - def __call__( - self, - task: Callable[P, list[tuple[str, str]] | tuple[str, str] | None], - /, - *args: P.args, - **kwargs: P.kwargs, - ) -> None: - """Add a task to the pre-compile context. - - Args: - task: The task to add. - args: The arguments to pass to the task - kwargs: The keyword arguments to pass to the task - """ - - -class PreCompileContext(CommonContext): - """Context for pre-compile hooks.""" - - add_save_task: AddTaskProtocol - add_modify_task: Callable[[str, Callable[[str], str]], None] - unevaluated_pages: Sequence["UnevaluatedPage"] - - -class PostCompileContext(CommonContext): - """Context for post-compile hooks.""" - - app: "App" - - -class Plugin: - """Base class for all plugins.""" - - def get_frontend_development_dependencies( - self, **context: Unpack[CommonContext] - ) -> list[str] | set[str] | tuple[str, ...]: - """Get the NPM packages required by the plugin for development. - - Args: - context: The context for the plugin. - - Returns: - A list of packages required by the plugin for development. - """ - return [] - - def get_frontend_dependencies( - self, **context: Unpack[CommonContext] - ) -> list[str] | set[str] | tuple[str, ...]: - """Get the NPM packages required by the plugin. - - Args: - context: The context for the plugin. - - Returns: - A list of packages required by the plugin. - """ - return [] - - def get_static_assets( - self, **context: Unpack[CommonContext] - ) -> Sequence[tuple[Path, str | bytes]]: - """Get the static assets required by the plugin. - - Args: - context: The context for the plugin. - - Returns: - A list of static assets required by the plugin. - """ - return [] - - def get_stylesheet_paths(self, **context: Unpack[CommonContext]) -> Sequence[str]: - """Get the paths to the stylesheets required by the plugin relative to the styles directory. - - Args: - context: The context for the plugin. - - Returns: - A list of paths to the stylesheets required by the plugin. - """ - return [] - - def pre_compile(self, **context: Unpack[PreCompileContext]) -> None: - """Called before the compilation of the plugin. - - Args: - context: The context for the plugin. - """ - - def post_compile(self, **context: Unpack[PostCompileContext]) -> None: - """Called after the compilation of the plugin. - - Args: - context: The context for the plugin. - """ - - def __repr__(self): - """Return a string representation of the plugin. - - Returns: - A string representation of the plugin. - """ - return f"{self.__class__.__name__}()" +from reflex_core.plugins.base import * diff --git a/reflex/plugins/shared_tailwind.py b/reflex/plugins/shared_tailwind.py index 0ea8a6d394a..81e736d0f97 100644 --- a/reflex/plugins/shared_tailwind.py +++ b/reflex/plugins/shared_tailwind.py @@ -1,235 +1,3 @@ -"""Tailwind CSS configuration types for Reflex plugins.""" +"""Re-export from reflex_core.plugins.shared_tailwind.""" -import dataclasses -from collections.abc import Mapping -from copy import deepcopy -from typing import Any, Literal, TypedDict - -from typing_extensions import NotRequired, Unpack - -from .base import Plugin as PluginBase - -TailwindPluginImport = TypedDict( - "TailwindPluginImport", - { - "name": str, - "from": str, - }, -) - -TailwindPluginWithCallConfig = TypedDict( - "TailwindPluginWithCallConfig", - { - "name": str, - "import": NotRequired[TailwindPluginImport], - "call": str, - "args": NotRequired[dict[str, Any]], - }, -) - -TailwindPluginWithoutCallConfig = TypedDict( - "TailwindPluginWithoutCallConfig", - { - "name": str, - "import": NotRequired[TailwindPluginImport], - }, -) - -TailwindPluginConfig = ( - TailwindPluginWithCallConfig | TailwindPluginWithoutCallConfig | str -) - - -def remove_version_from_plugin(plugin: TailwindPluginConfig) -> TailwindPluginConfig: - """Remove the version from a plugin name. - - Args: - plugin: The plugin to remove the version from. - - Returns: - The plugin without the version. - """ - from reflex.utils.format import format_library_name - - if isinstance(plugin, str): - return format_library_name(plugin) - - if plugin_import := plugin.get("import"): - plugin_import["from"] = format_library_name(plugin_import["from"]) - - plugin["name"] = format_library_name(plugin["name"]) - - return plugin - - -class TailwindConfig(TypedDict): - """Tailwind CSS configuration options. - - See: https://tailwindcss.com/docs/configuration - """ - - content: NotRequired[list[str]] - important: NotRequired[str | bool] - prefix: NotRequired[str] - separator: NotRequired[str] - presets: NotRequired[list[str]] - darkMode: NotRequired[Literal["media", "class", "selector"]] - theme: NotRequired[dict[str, Any]] - corePlugins: NotRequired[list[str] | dict[str, bool]] - plugins: NotRequired[list[TailwindPluginConfig]] - - -def tailwind_config_js_template( - *, default_content: list[str], **kwargs: Unpack[TailwindConfig] -): - """Generate a Tailwind CSS configuration file in JavaScript format. - - Args: - default_content: The default content to use if none is provided. - **kwargs: The template variables. - - Returns: - The Tailwind config template. - """ - import json - - # Extract parameters - plugins = kwargs.get("plugins", []) - presets = kwargs.get("presets", []) - content = kwargs.get("content") - theme = kwargs.get("theme") - dark_mode = kwargs.get("darkMode") - core_plugins = kwargs.get("corePlugins") - important = kwargs.get("important") - prefix = kwargs.get("prefix") - separator = kwargs.get("separator") - - # Extract destructured imports from plugin dicts only - imports = [ - plugin["import"] - for plugin in plugins - if isinstance(plugin, Mapping) and "import" in plugin - ] - - # Generate import statements for destructured imports - import_lines = "\n".join([ - f"import {{ {imp['name']} }} from {json.dumps(imp['from'])};" for imp in imports - ]) - - # Generate plugin imports - plugin_imports = [] - for i, plugin in enumerate(plugins, 1): - if isinstance(plugin, Mapping) and "call" not in plugin: - plugin_imports.append( - f"import plugin{i} from {json.dumps(plugin['name'])};" - ) - elif not isinstance(plugin, Mapping): - plugin_imports.append(f"import plugin{i} from {json.dumps(plugin)};") - - plugin_imports_lines = "\n".join(plugin_imports) - - presets_imports_lines = "\n".join([ - f"import preset{i} from {json.dumps(preset)};" - for i, preset in enumerate(presets, 1) - ]) - - # Generate plugin array - plugin_list = [] - for i, plugin in enumerate(plugins, 1): - if isinstance(plugin, Mapping) and "call" in plugin: - args_part = "" - if "args" in plugin: - args_part = json.dumps(plugin["args"]) - plugin_list.append(f"{plugin['call']}({args_part})") - else: - plugin_list.append(f"plugin{i}") - - plugin_use_str = ",".join(plugin_list) - - return rf""" -{import_lines} - -{plugin_imports_lines} - -{presets_imports_lines} - -export default {{ - content: {json.dumps(content or default_content)}, - theme: {json.dumps(theme or {})}, - {f"darkMode: {json.dumps(dark_mode)}," if dark_mode is not None else ""} - {f"corePlugins: {json.dumps(core_plugins)}," if core_plugins is not None else ""} - {f"importants: {json.dumps(important)}," if important is not None else ""} - {f"prefix: {json.dumps(prefix)}," if prefix is not None else ""} - {f"separator: {json.dumps(separator)}," if separator is not None else ""} - {f"presets: [{', '.join(f'preset{i}' for i in range(1, len(presets) + 1))}]," if presets else ""} - plugins: [{plugin_use_str}] -}}; -""" - - -@dataclasses.dataclass -class TailwindPlugin(PluginBase): - """Plugin for Tailwind CSS.""" - - config: TailwindConfig = dataclasses.field( - default_factory=lambda: TailwindConfig( - plugins=[ - "@tailwindcss/typography@0.5.19", - ], - ) - ) - - def get_frontend_development_dependencies(self, **context) -> list[str]: - """Get the packages required by the plugin. - - Args: - **context: The context for the plugin. - - Returns: - A list of packages required by the plugin. - """ - config = self.get_config() - - return [ - plugin if isinstance(plugin, str) else plugin.get("name") - for plugin in config.get("plugins", []) - ] + config.get("presets", []) - - def get_config(self) -> TailwindConfig: - """Get the Tailwind CSS configuration. - - Returns: - The Tailwind CSS configuration. - """ - from reflex.config import get_config - - rxconfig_config = getattr(get_config(), "tailwind", None) - - if rxconfig_config is not None and rxconfig_config != self.config: - from reflex.utils import console - - console.warn( - "It seems you have provided a tailwind configuration in your call to `rx.Config`." - f" You should provide the configuration as an argument to `rx.plugins.{self.__class__.__name__}()` instead." - ) - return rxconfig_config - - return self.config - - def get_unversioned_config(self) -> TailwindConfig: - """Get the Tailwind CSS configuration without version-specific adjustments. - - Returns: - The Tailwind CSS configuration without version-specific adjustments. - """ - from reflex.utils.format import format_library_name - - config = deepcopy(self.get_config()) - if presets := config.get("presets"): - # Somehow, having an empty list of presets breaks Tailwind. - # So we only set the presets if there are any. - config["presets"] = [format_library_name(preset) for preset in presets] - config["plugins"] = [ - remove_version_from_plugin(plugin) for plugin in config.get("plugins", []) - ] - return config +from reflex_core.plugins.shared_tailwind import * diff --git a/reflex/plugins/sitemap.py b/reflex/plugins/sitemap.py index e2ba3257435..085a5ba7879 100644 --- a/reflex/plugins/sitemap.py +++ b/reflex/plugins/sitemap.py @@ -1,210 +1,3 @@ -"""Sitemap plugin for Reflex.""" +"""Re-export from reflex_core.plugins.sitemap.""" -import datetime -from collections.abc import Sequence -from pathlib import Path -from types import SimpleNamespace -from typing import TYPE_CHECKING, Literal, TypedDict -from xml.etree.ElementTree import Element, SubElement, indent, tostring - -from typing_extensions import NotRequired - -from reflex import constants - -from .base import Plugin as PluginBase - -if TYPE_CHECKING: - from reflex.app import UnevaluatedPage - -Location = str -LastModified = datetime.datetime -ChangeFrequency = Literal[ - "always", "hourly", "daily", "weekly", "monthly", "yearly", "never" -] -Priority = float - - -class SitemapLink(TypedDict): - """A link in the sitemap.""" - - loc: Location - lastmod: NotRequired[LastModified] - changefreq: NotRequired[ChangeFrequency] - priority: NotRequired[Priority] - - -class SitemapLinkConfiguration(TypedDict): - """Configuration for a sitemap link.""" - - loc: NotRequired[Location] - lastmod: NotRequired[LastModified] - changefreq: NotRequired[ChangeFrequency] - priority: NotRequired[Priority] - - -class Constants(SimpleNamespace): - """Sitemap constants.""" - - FILE_PATH: Path = Path(constants.Dirs.PUBLIC) / "sitemap.xml" - - -def configuration_with_loc( - *, config: SitemapLinkConfiguration, deploy_url: str | None, loc: Location -) -> SitemapLink: - """Set the 'loc' field of the configuration. - - Args: - config: The configuration dictionary. - deploy_url: The deployment URL, if any. - loc: The location to set. - - Returns: - A SitemapLink dictionary with the 'loc' field set. - """ - if deploy_url and not loc.startswith("http://") and not loc.startswith("https://"): - loc = f"{deploy_url.rstrip('/')}/{loc.lstrip('/')}" - link: SitemapLink = {"loc": loc} - if (lastmod := config.get("lastmod")) is not None: - link["lastmod"] = lastmod - if (changefreq := config.get("changefreq")) is not None: - link["changefreq"] = changefreq - if (priority := config.get("priority")) is not None: - link["priority"] = min(1.0, max(0.0, priority)) - return link - - -def generate_xml(links: Sequence[SitemapLink]) -> str: - """Generate an XML sitemap from a list of links. - - Args: - links: A sequence of SitemapLink dictionaries. - - Returns: - A pretty-printed XML string representing the sitemap. - """ - urlset = Element("urlset", xmlns="https://www.sitemaps.org/schemas/sitemap/0.9") - - for link in links: - url = SubElement(urlset, "url") - - loc_element = SubElement(url, "loc") - loc_element.text = link["loc"] - - if (changefreq := link.get("changefreq")) is not None: - changefreq_element = SubElement(url, "changefreq") - changefreq_element.text = changefreq - - if (lastmod := link.get("lastmod")) is not None: - lastmod_element = SubElement(url, "lastmod") - if isinstance(lastmod, datetime.datetime): - lastmod = lastmod.isoformat() - lastmod_element.text = lastmod - - if (priority := link.get("priority")) is not None: - priority_element = SubElement(url, "priority") - priority_element.text = str(priority) - indent(urlset, " ") - return tostring(urlset, encoding="utf-8", xml_declaration=True).decode("utf-8") - - -def is_route_dynamic(route: str) -> bool: - """Check if a route is dynamic. - - Args: - route: The route to check. - - Returns: - True if the route is dynamic, False otherwise. - """ - return "[" in route and "]" in route - - -def generate_links_for_sitemap( - unevaluated_pages: Sequence["UnevaluatedPage"], -) -> list[SitemapLink]: - """Generate sitemap links from unevaluated pages. - - Args: - unevaluated_pages: Sequence of unevaluated pages. - - Returns: - A list of SitemapLink dictionaries. - """ - from reflex.config import get_config - from reflex.utils import console - - deploy_url = get_config().deploy_url - - links: list[SitemapLink] = [] - - for page in unevaluated_pages: - sitemap_config: SitemapLinkConfiguration | None = page.context.get( - "sitemap", {} - ) - if sitemap_config is None: - continue - - if is_route_dynamic(page.route) or page.route == "404": - if not sitemap_config: - continue - - if (loc := sitemap_config.get("loc")) is None: - route_message = ( - "Dynamic route" if is_route_dynamic(page.route) else "Route 404" - ) - console.warn( - route_message - + f" '{page.route}' does not have a 'loc' in sitemap configuration. Skipping." - ) - continue - - sitemap_link = configuration_with_loc( - config=sitemap_config, deploy_url=deploy_url, loc=loc - ) - - elif (loc := sitemap_config.get("loc")) is not None: - sitemap_link = configuration_with_loc( - config=sitemap_config, deploy_url=deploy_url, loc=loc - ) - - else: - loc = page.route if page.route != "index" else "/" - if not loc.startswith("/"): - loc = "/" + loc - sitemap_link = configuration_with_loc( - config=sitemap_config, deploy_url=deploy_url, loc=loc - ) - - links.append(sitemap_link) - return links - - -def sitemap_task(unevaluated_pages: Sequence["UnevaluatedPage"]) -> tuple[str, str]: - """Task to generate the sitemap XML file. - - Args: - unevaluated_pages: Sequence of unevaluated pages. - - Returns: - A tuple containing the file path and the generated XML content. - """ - return ( - str(Constants.FILE_PATH), - generate_xml(generate_links_for_sitemap(unevaluated_pages)), - ) - - -class SitemapPlugin(PluginBase): - """Sitemap plugin for Reflex.""" - - def pre_compile(self, **context): - """Generate the sitemap XML file before compilation. - - Args: - context: The context for the plugin. - """ - unevaluated_pages = context.get("unevaluated_pages", []) - context["add_save_task"](sitemap_task, unevaluated_pages) - - -Plugin = SitemapPlugin +from reflex_core.plugins.sitemap import * diff --git a/reflex/plugins/tailwind_v3.py b/reflex/plugins/tailwind_v3.py index 04e25904018..2e7cad305a6 100644 --- a/reflex/plugins/tailwind_v3.py +++ b/reflex/plugins/tailwind_v3.py @@ -1,170 +1,3 @@ -"""Base class for all plugins.""" +"""Re-export from reflex_core.plugins.tailwind_v3.""" -import dataclasses -from pathlib import Path -from types import SimpleNamespace - -from reflex.constants.base import Dirs -from reflex.constants.compiler import Ext, PageNames -from reflex.plugins.shared_tailwind import ( - TailwindConfig, - TailwindPlugin, - tailwind_config_js_template, -) - - -class Constants(SimpleNamespace): - """Tailwind constants.""" - - # The Tailwindcss version - VERSION = "tailwindcss@3.4.19" - # The Tailwind config. - CONFIG = "tailwind.config.js" - # Default Tailwind content paths - CONTENT = [f"./{Dirs.PAGES}/**/*.{{js,ts,jsx,tsx}}", "./utils/**/*.{js,ts,jsx,tsx}"] - # Relative tailwind style path to root stylesheet in Dirs.STYLES. - ROOT_STYLE_PATH = "./tailwind.css" - - # Content of the style content. - ROOT_STYLE_CONTENT = """ -@import "tailwindcss/base"; - -@import url('{radix_url}'); - -@tailwind components; -@tailwind utilities; -""" - - # The default tailwind css. - TAILWIND_CSS = "@import url('./tailwind.css');" - - -def compile_config(config: TailwindConfig): - """Compile the Tailwind config. - - Args: - config: The Tailwind config. - - Returns: - The compiled Tailwind config. - """ - return Constants.CONFIG, tailwind_config_js_template( - **config, - default_content=Constants.CONTENT, - ) - - -def compile_root_style(): - """Compile the Tailwind root style. - - Returns: - The compiled Tailwind root style. - """ - from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET - - return str( - Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH - ), Constants.ROOT_STYLE_CONTENT.format( - radix_url=RADIX_THEMES_STYLESHEET, - ) - - -def _index_of_element_that_has(haystack: list[str], needle: str) -> int | None: - return next( - (i for i, line in enumerate(haystack) if needle in line), - None, - ) - - -def add_tailwind_to_postcss_config(postcss_file_content: str) -> str: - """Add tailwind to the postcss config. - - Args: - postcss_file_content: The content of the postcss config file. - - Returns: - The modified postcss config file content. - """ - from reflex.constants import Dirs - - postcss_file_lines = postcss_file_content.splitlines() - - if _index_of_element_that_has(postcss_file_lines, "tailwindcss") is not None: - return postcss_file_content - - line_with_postcss_plugins = _index_of_element_that_has( - postcss_file_lines, "plugins" - ) - if not line_with_postcss_plugins: - print( # noqa: T201 - f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. " - "Please make sure the file exists and is valid." - ) - return postcss_file_content - - postcss_import_line = _index_of_element_that_has( - postcss_file_lines, '"postcss-import"' - ) - postcss_file_lines.insert( - (postcss_import_line or line_with_postcss_plugins) + 1, "tailwindcss: {}," - ) - - return "\n".join(postcss_file_lines) - - -def add_tailwind_to_css_file(css_file_content: str) -> str: - """Add tailwind to the css file. - - Args: - css_file_content: The content of the css file. - - Returns: - The modified css file content. - """ - from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET - - if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content: - return css_file_content - if RADIX_THEMES_STYLESHEET not in css_file_content: - print( # noqa: T201 - f"Could not find line with '{RADIX_THEMES_STYLESHEET}' in {Dirs.STYLES}. " - "Please make sure the file exists and is valid." - ) - return css_file_content - return css_file_content.replace( - f"@import url('{RADIX_THEMES_STYLESHEET}');", - Constants.TAILWIND_CSS, - ) - - -@dataclasses.dataclass -class TailwindV3Plugin(TailwindPlugin): - """Plugin for Tailwind CSS.""" - - def get_frontend_development_dependencies(self, **context) -> list[str]: - """Get the packages required by the plugin. - - Args: - **context: The context for the plugin. - - Returns: - A list of packages required by the plugin. - """ - return [ - *super().get_frontend_development_dependencies(**context), - Constants.VERSION, - ] - - def pre_compile(self, **context): - """Pre-compile the plugin. - - Args: - context: The context for the plugin. - """ - context["add_save_task"](compile_config, self.get_unversioned_config()) - context["add_save_task"](compile_root_style) - context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config) - context["add_modify_task"]( - str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)), - add_tailwind_to_css_file, - ) +from reflex_core.plugins.tailwind_v3 import * diff --git a/reflex/plugins/tailwind_v4.py b/reflex/plugins/tailwind_v4.py index 072ca088a7a..f78999b1fc4 100644 --- a/reflex/plugins/tailwind_v4.py +++ b/reflex/plugins/tailwind_v4.py @@ -1,174 +1,3 @@ -"""Base class for all plugins.""" +"""Re-export from reflex_core.plugins.tailwind_v4.""" -import dataclasses -from pathlib import Path -from types import SimpleNamespace - -from reflex.constants.base import Dirs -from reflex.constants.compiler import Ext, PageNames -from reflex.plugins.shared_tailwind import ( - TailwindConfig, - TailwindPlugin, - tailwind_config_js_template, -) - - -class Constants(SimpleNamespace): - """Tailwind constants.""" - - # The Tailwindcss version - VERSION = "tailwindcss@4.2.2" - # The Tailwind config. - CONFIG = "tailwind.config.js" - # Default Tailwind content paths - CONTENT = [f"./{Dirs.PAGES}/**/*.{{js,ts,jsx,tsx}}", "./utils/**/*.{js,ts,jsx,tsx}"] - # Relative tailwind style path to root stylesheet in Dirs.STYLES. - ROOT_STYLE_PATH = "./tailwind.css" - - # Content of the style content. - ROOT_STYLE_CONTENT = """@layer theme, base, components, utilities; -@import "tailwindcss/theme.css" layer(theme); -@import "tailwindcss/preflight.css" layer(base); -@import "{radix_url}" layer(components); -@import "tailwindcss/utilities.css" layer(utilities); -@config "../tailwind.config.js"; -""" - - # The default tailwind css. - TAILWIND_CSS = "@import url('./tailwind.css');" - - -def compile_config(config: TailwindConfig): - """Compile the Tailwind config. - - Args: - config: The Tailwind config. - - Returns: - The compiled Tailwind config. - """ - return Constants.CONFIG, tailwind_config_js_template( - **config, - default_content=Constants.CONTENT, - ) - - -def compile_root_style(): - """Compile the Tailwind root style. - - Returns: - The compiled Tailwind root style. - """ - from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET - - return str( - Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH - ), Constants.ROOT_STYLE_CONTENT.format( - radix_url=RADIX_THEMES_STYLESHEET, - ) - - -def _index_of_element_that_has(haystack: list[str], needle: str) -> int | None: - return next( - (i for i, line in enumerate(haystack) if needle in line), - None, - ) - - -def add_tailwind_to_postcss_config(postcss_file_content: str) -> str: - """Add tailwind to the postcss config. - - Args: - postcss_file_content: The content of the postcss config file. - - Returns: - The modified postcss config file content. - """ - from reflex.constants import Dirs - - postcss_file_lines = postcss_file_content.splitlines() - - line_with_postcss_plugins = _index_of_element_that_has( - postcss_file_lines, "plugins" - ) - if not line_with_postcss_plugins: - print( # noqa: T201 - f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. " - "Please make sure the file exists and is valid." - ) - return postcss_file_content - - plugins_to_remove = ['"postcss-import"', "tailwindcss", "autoprefixer"] - plugins_to_add = ['"@tailwindcss/postcss"'] - - for plugin in plugins_to_remove: - plugin_index = _index_of_element_that_has(postcss_file_lines, plugin) - if plugin_index is not None: - postcss_file_lines.pop(plugin_index) - - for plugin in plugins_to_add[::-1]: - if not _index_of_element_that_has(postcss_file_lines, plugin): - postcss_file_lines.insert( - line_with_postcss_plugins + 1, f" {plugin}: {{}}," - ) - - return "\n".join(postcss_file_lines) - - -def add_tailwind_to_css_file(css_file_content: str) -> str: - """Add tailwind to the css file. - - Args: - css_file_content: The content of the css file. - - Returns: - The modified css file content. - """ - from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET - - if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content: - return css_file_content - if RADIX_THEMES_STYLESHEET not in css_file_content: - print( # noqa: T201 - f"Could not find line with '{RADIX_THEMES_STYLESHEET}' in {Dirs.STYLES}. " - "Please make sure the file exists and is valid." - ) - return css_file_content - return css_file_content.replace( - f"@import url('{RADIX_THEMES_STYLESHEET}');", - Constants.TAILWIND_CSS, - ) - - -@dataclasses.dataclass -class TailwindV4Plugin(TailwindPlugin): - """Plugin for Tailwind CSS.""" - - def get_frontend_development_dependencies(self, **context) -> list[str]: - """Get the packages required by the plugin. - - Args: - **context: The context for the plugin. - - Returns: - A list of packages required by the plugin. - """ - return [ - *super().get_frontend_development_dependencies(**context), - Constants.VERSION, - "@tailwindcss/postcss@4.2.2", - ] - - def pre_compile(self, **context): - """Pre-compile the plugin. - - Args: - context: The context for the plugin. - """ - context["add_save_task"](compile_config, self.get_unversioned_config()) - context["add_save_task"](compile_root_style) - context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config) - context["add_modify_task"]( - str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)), - add_tailwind_to_css_file, - ) +from reflex_core.plugins.tailwind_v4 import * diff --git a/reflex/reflex.py b/reflex/reflex.py index 7c3f98465b2..b605400e1bf 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -8,17 +8,16 @@ import click from reflex_cli.v2.deployments import hosting_cli +from reflex_core import constants +from reflex_core.config import get_config +from reflex_core.environment import environment +from reflex_core.utils import console -from reflex import constants -from reflex.config import get_config from reflex.custom_components.custom_components import custom_components_cli -from reflex.environment import environment -from reflex.utils import console if TYPE_CHECKING: from reflex_cli.constants.base import LogLevel as HostingLogLevel - - from reflex.constants.base import LITERAL_ENV + from reflex_core.constants.base import LITERAL_ENV def set_loglevel(ctx: click.Context, self: click.Parameter, value: str | None): @@ -633,9 +632,9 @@ def status(): return # Run alembic check command and display output - import reflex.config + import reflex_core.config - config = reflex.config.get_config() + config = reflex_core.config.get_config() console.print(f"[bold]\\[{config.db_url}][/bold]") # Get migration history using Model method diff --git a/reflex/route.py b/reflex/route.py index 761ec8f974d..930b457ac63 100644 --- a/reflex/route.py +++ b/reflex/route.py @@ -5,8 +5,8 @@ import re from collections.abc import Callable -from reflex import constants -from reflex.config import get_config +from reflex_core import constants +from reflex_core.config import get_config def verify_route_validity(route: str) -> None: diff --git a/reflex/state.py b/reflex/state.py index a105f9039d4..d420702c5b4 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -32,14 +32,10 @@ get_type_hints, ) -from rich.markup import escape -from typing_extensions import Self - -import reflex.istate.dynamic -from reflex import constants, event -from reflex.constants.state import FIELD_MARKER -from reflex.environment import PerformanceMode, environment -from reflex.event import ( +from reflex_core import constants +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.environment import PerformanceMode, environment +from reflex_core.event import ( BACKGROUND_TASK_MARKER, EVENT_ACTIONS_MARKER, Event, @@ -48,14 +44,7 @@ call_script, fix_events, ) -from reflex.istate import HANDLED_PICKLE_ERRORS, debug_failed_pickles -from reflex.istate.data import RouterData -from reflex.istate.proxy import ImmutableMutableProxy as ImmutableMutableProxy -from reflex.istate.proxy import MutableProxy, StateProxy, is_mutable_type -from reflex.istate.storage import ClientStorageBase -from reflex.model import Model -from reflex.utils import console, format, prerequisites, types -from reflex.utils.exceptions import ( +from reflex_core.utils.exceptions import ( ComputedVarShadowsBaseVarsError, ComputedVarShadowsStateVarError, DynamicComponentInvalidSignatureError, @@ -69,11 +58,10 @@ StateTooLargeError, UnretrievableVarValueError, ) -from reflex.utils.exceptions import ImmutableStateError as ImmutableStateError -from reflex.utils.exec import is_testing_env -from reflex.utils.types import _isinstance, is_union, value_inside_optional -from reflex.vars import Field, VarData, field -from reflex.vars.base import ( +from reflex_core.utils.exceptions import ImmutableStateError as ImmutableStateError +from reflex_core.utils.types import _isinstance, is_union, value_inside_optional +from reflex_core.vars import Field, VarData, field +from reflex_core.vars.base import ( ComputedVar, DynamicRouteVar, EvenMoreBasicBaseState, @@ -82,9 +70,22 @@ dispatch, is_computed_var, ) +from rich.markup import escape +from typing_extensions import Self + +import reflex.istate.dynamic +from reflex import event +from reflex.istate import HANDLED_PICKLE_ERRORS, debug_failed_pickles +from reflex.istate.data import RouterData +from reflex.istate.proxy import ImmutableMutableProxy as ImmutableMutableProxy +from reflex.istate.proxy import MutableProxy, StateProxy, is_mutable_type +from reflex.istate.storage import ClientStorageBase +from reflex.model import Model +from reflex.utils import console, format, prerequisites, types +from reflex.utils.exec import is_testing_env if TYPE_CHECKING: - from reflex.components.component import Component + from reflex_core.components.component import Component Delta = dict[str, Any] @@ -230,8 +231,8 @@ def __call__(self, *args: Any) -> EventSpec: EventHandlerValueError: If the given Var name is not a str NotImplementedError: If the setter for the given Var is async """ - from reflex.config import get_config - from reflex.utils.exceptions import EventHandlerValueError + from reflex_core.config import get_config + from reflex_core.utils.exceptions import EventHandlerValueError config = get_config() if config.state_auto_setters is None: @@ -443,7 +444,7 @@ def __init__( Raises: ReflexRuntimeError: If the state is instantiated directly by end user. """ - from reflex.utils.exceptions import ReflexRuntimeError + from reflex_core.utils.exceptions import ReflexRuntimeError if not _reflex_internal_init and not is_testing_env(): msg = ( @@ -517,7 +518,7 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): Raises: StateValueError: If a substate class shadows another. """ - from reflex.utils.exceptions import StateValueError + from reflex_core.utils.exceptions import StateValueError super().__init_subclass__(**kwargs) @@ -722,7 +723,7 @@ def _evaluate(cls, f: Callable[[Self], Any], of_type: type | None = None) -> Var console.warn( "The _evaluate method is experimental and may be removed in future versions." ) - from reflex.components.component import Component + from reflex_core.components.component import Component of_type = of_type or Component @@ -1082,8 +1083,8 @@ def _init_var(cls, name: str, prop: Var): Raises: VarTypeError: if the variable has an incorrect type """ - from reflex.config import get_config - from reflex.utils.exceptions import VarTypeError + from reflex_core.config import get_config + from reflex_core.utils.exceptions import VarTypeError if not types.is_valid_var_type(prop._var_type): msg = ( @@ -1186,7 +1187,7 @@ def _create_setter(cls, name: str, prop: Var): name: The name of the var. prop: The var to create a setter for. """ - from reflex.config import get_config + from reflex_core.config import get_config config = get_config() create_event_handler_kwargs = {} @@ -1922,12 +1923,9 @@ async def _process_event( elif dataclasses.is_dataclass(hinted_args): payload[arg] = hinted_args(**value) elif find_spec("pydantic"): - from pydantic import BaseModel as BaseModelV2 - from pydantic.v1 import BaseModel as BaseModelV1 + from pydantic import BaseModel - if issubclass(hinted_args, BaseModelV1): - payload[arg] = hinted_args.parse_obj(value) - elif issubclass(hinted_args, BaseModelV2): + if issubclass(hinted_args, BaseModel): payload[arg] = hinted_args.model_validate(value) elif isinstance(value, list) and (hinted_args is set or hinted_args is set): payload[arg] = set(value) @@ -2176,7 +2174,7 @@ def get_value(self, key: str) -> Any: """ if isinstance(key, MutableProxy): # Legacy behavior from v0.7.14: handle non-string keys with deprecation warning - from reflex.utils import console + from reflex_core.utils import console console.deprecate( feature_name="Non-string keys in get_value", @@ -2554,7 +2552,7 @@ def dynamic(func: Callable[[T], Component]): state_class: type[T] = values[0] def wrapper() -> Component: - from reflex.components.base.fragment import fragment + from reflex_components_core.base.fragment import fragment return fragment(state_class._evaluate(lambda state: func(state))) diff --git a/reflex/style.py b/reflex/style.py index 26cc4955f85..3caac782ebb 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -1,416 +1,3 @@ -"""Handle styling.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any, Literal - -from reflex import constants -from reflex.components.core.breakpoints import Breakpoints, breakpoints_values -from reflex.event import EventChain, EventHandler, EventSpec, run_script -from reflex.utils import format -from reflex.utils.exceptions import ReflexError -from reflex.utils.imports import ImportVar -from reflex.utils.types import typehint_issubclass -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var -from reflex.vars.function import FunctionVar -from reflex.vars.object import ObjectVar - -SYSTEM_COLOR_MODE: str = "system" -LIGHT_COLOR_MODE: str = "light" -DARK_COLOR_MODE: str = "dark" -LiteralColorMode = Literal["system", "light", "dark"] - -# Reference the global ColorModeContext -color_mode_imports = { - f"$/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="ColorModeContext")], - "react": [ImportVar(tag="useContext")], -} - - -def _color_mode_var(_js_expr: str, _var_type: type = str) -> Var: - """Create a Var that destructs the _js_expr from ColorModeContext. - - Args: - _js_expr: The name of the variable to get from ColorModeContext. - _var_type: The type of the Var. - - Returns: - The Var that resolves to the color mode. - """ - return Var( - _js_expr=_js_expr, - _var_type=_var_type, - _var_data=VarData( - imports=color_mode_imports, - hooks={f"const {{ {_js_expr} }} = useContext(ColorModeContext)": None}, - ), - ).guess_type() - - -def set_color_mode( - new_color_mode: LiteralColorMode | Var[LiteralColorMode], -) -> EventSpec: - """Create an EventSpec Var that sets the color mode to a specific value. - - Note: `set_color_mode` is not a real event and cannot be triggered from a - backend event handler. - - Args: - new_color_mode: The color mode to set. - - Returns: - The EventSpec Var that can be passed to an event trigger. - """ - base_setter = _color_mode_var( - _js_expr=constants.ColorMode.SET, - ).to(FunctionVar) - - return run_script( - base_setter.call(new_color_mode), - ) - - -# Var resolves to the current color mode for the app ("light", "dark" or "system") -color_mode = _color_mode_var(_js_expr=constants.ColorMode.NAME) -# Var resolves to the resolved color mode for the app ("light" or "dark") -resolved_color_mode = _color_mode_var(_js_expr=constants.ColorMode.RESOLVED_NAME) -# Var resolves to a function invocation that toggles the color mode -toggle_color_mode = _color_mode_var( - _js_expr=constants.ColorMode.TOGGLE, - _var_type=EventChain, -) - -STYLE_PROP_SHORTHAND_MAPPING = { - "paddingX": ("paddingInlineStart", "paddingInlineEnd"), - "paddingY": ("paddingTop", "paddingBottom"), - "marginX": ("marginInlineStart", "marginInlineEnd"), - "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"), -} - - -def media_query(breakpoint_expr: str): - """Create a media query selector. - - Args: - breakpoint_expr: The CSS expression representing the breakpoint. - - Returns: - The media query selector used as a key in emotion css dict. - """ - return f"@media screen and (min-width: {breakpoint_expr})" - - -def convert_item( - style_item: int | str | Var, -) -> tuple[str | Var, VarData | None]: - """Format a single value in a style dictionary. - - Args: - style_item: The style item to format. - - Returns: - The formatted style item and any associated VarData. - - Raises: - ReflexError: If an EventHandler is used as a style value - """ - from reflex.components.component import BaseComponent - - if isinstance(style_item, (EventHandler, BaseComponent)): - msg = ( - f"{type(style_item)} cannot be used as style values. " - "Please use a Var or a literal value." - ) - raise ReflexError(msg) - - if isinstance(style_item, Var): - return style_item, style_item._get_all_var_data() - - # Otherwise, convert to Var to collapse VarData encoded in f-string. - new_var = LiteralVar.create(style_item) - var_data = new_var._get_all_var_data() if new_var is not None else None - return new_var, var_data - - -def convert_list( - responsive_list: list[str | dict | Var], -) -> tuple[list[str | dict[str, Var | list | dict]], VarData | None]: - """Format a responsive value list. - - Args: - responsive_list: The raw responsive value list (one value per breakpoint). - - Returns: - The recursively converted responsive value list and any associated VarData. - """ - converted_value = [] - item_var_datas = [] - for responsive_item in responsive_list: - if isinstance(responsive_item, dict): - # Recursively format nested style dictionaries. - item, item_var_data = convert(responsive_item) - else: - item, item_var_data = convert_item(responsive_item) - converted_value.append(item) - item_var_datas.append(item_var_data) - return converted_value, VarData.merge(*item_var_datas) - - -def convert( - style_dict: dict[str, Var | dict | list | str], -) -> tuple[dict[str, str | list | dict], VarData | None]: - """Format a style dictionary. - - Args: - style_dict: The style dictionary to format. - - Returns: - The formatted style dictionary. - """ - var_data = None # Track import/hook data from any Vars in the style dict. - out = {} - - def update_out_dict( - return_value: Var | dict | list | str, keys_to_update: tuple[str, ...] - ): - for k in keys_to_update: - out[k] = return_value - - for key, value in style_dict.items(): - keys = ( - format_style_key(key) - if not isinstance(value, (dict, ObjectVar, list)) - or ( - isinstance(value, Breakpoints) - and all(not isinstance(v, dict) for v in value.values()) - ) - or (isinstance(value, list) and all(not isinstance(v, dict) for v in value)) - or ( - isinstance(value, ObjectVar) - and not typehint_issubclass(value._var_type, Mapping) - ) - else (key,) - ) - - if isinstance(value, Var): - return_val = value - new_var_data = value._get_all_var_data() - update_out_dict(return_val, keys) - elif isinstance(value, dict): - # Recursively format nested style dictionaries. - return_val, new_var_data = convert(value) - update_out_dict(return_val, keys) - elif isinstance(value, list): - # Responsive value is a list of dict or value - return_val, new_var_data = convert_list(value) - update_out_dict(return_val, keys) - else: - return_val, new_var_data = convert_item(value) - update_out_dict(return_val, keys) - # Combine all the collected VarData instances. - var_data = VarData.merge(var_data, new_var_data) - - if isinstance(style_dict, Breakpoints): - out = Breakpoints(out).factorize() - - return out, var_data - - -def format_style_key(key: str) -> tuple[str, ...]: - """Convert style keys to camel case and convert shorthand - styles names to their corresponding css names. - - Args: - key: The style key to convert. - - Returns: - Tuple of css style names corresponding to the key provided. - """ - if key.startswith("--"): - return (key,) - key = format.to_camel_case(key) - return STYLE_PROP_SHORTHAND_MAPPING.get(key, (key,)) - - -EMPTY_VAR_DATA = VarData() - - -class Style(dict[str, Any]): - """A style dictionary.""" - - def __init__(self, style_dict: dict[str, Any] | None = None, **kwargs): - """Initialize the style. - - Args: - style_dict: The style dictionary. - kwargs: Other key value pairs to apply to the dict update. - """ - if style_dict: - style_dict.update(kwargs) - else: - style_dict = kwargs - if style_dict: - style_dict, self._var_data = convert(style_dict) - else: - self._var_data = EMPTY_VAR_DATA - super().__init__(style_dict) - - def update(self, style_dict: dict | None, **kwargs): - """Update the style. - - Args: - style_dict: The style dictionary. - kwargs: Other key value pairs to apply to the dict update. - """ - if not isinstance(style_dict, Style): - converted_dict = type(self)(style_dict) - else: - converted_dict = style_dict - if kwargs: - if converted_dict is None: - converted_dict = type(self)(kwargs) - else: - converted_dict.update(kwargs) - # Combine our VarData with that of any Vars in the style_dict that was passed. - self._var_data = VarData.merge(self._var_data, converted_dict._var_data) - super().update(converted_dict) - - def __setitem__(self, key: str, value: Any): - """Set an item in the style. - - Args: - key: The key to set. - value: The value to set. - """ - # Create a Var to collapse VarData encoded in f-string. - var = LiteralVar.create(value) - if var is not None: - # Carry the imports/hooks when setting a Var as a value. - self._var_data = VarData.merge( - getattr(self, "_var_data", None), var._get_all_var_data() - ) - super().__setitem__(key, value) - - def __or__(self, other: Style | dict) -> Style: - """Combine two styles. - - Args: - other: The other style to combine. - - Returns: - The combined style. - """ - other_var_data = None - if not isinstance(other, Style): - other_dict, other_var_data = convert(other) - else: - other_dict, other_var_data = other, other._var_data - - new_style = Style(super().__or__(other_dict)) - if self._var_data or other_var_data: - new_style._var_data = VarData.merge(self._var_data, other_var_data) - return new_style - - -def _format_emotion_style_pseudo_selector(key: str) -> str: - """Format a pseudo selector for emotion CSS-in-JS. - - Args: - key: Underscore-prefixed or colon-prefixed pseudo selector key (_hover/:hover). - - Returns: - A self-referential pseudo selector key (&:hover). - """ - prefix = None - if key.startswith("_"): - prefix = "&:" - key = key[1:] - if key.startswith(":"): - # Handle pseudo selectors and elements in native format. - prefix = "&" - if prefix is not None: - return prefix + format.to_kebab_case(key) - return key - - -def format_as_emotion(style_dict: dict[str, Any]) -> Style | None: - """Convert the style to an emotion-compatible CSS-in-JS dict. - - Args: - style_dict: The style dict to convert. - - Returns: - The emotion style dict. - """ - var_data = style_dict._var_data if isinstance(style_dict, Style) else None - - emotion_style = Style() - - for orig_key, value in style_dict.items(): - key = _format_emotion_style_pseudo_selector(orig_key) - if isinstance(value, (Breakpoints, list)): - if isinstance(value, Breakpoints): - mbps = { - media_query(bp): ( - bp_value if isinstance(bp_value, dict) else {key: bp_value} - ) - for bp, bp_value in value.items() - } - else: - # Apply media queries from responsive value list. - mbps = { - media_query([0, *breakpoints_values][bp]): ( - bp_value if isinstance(bp_value, dict) else {key: bp_value} - ) - for bp, bp_value in enumerate(value) - } - if key.startswith("&:"): - emotion_style[key] = mbps - else: - for mq, style_sub_dict in mbps.items(): - emotion_style.setdefault(mq, {}).update(style_sub_dict) - elif isinstance(value, dict): - # Recursively format nested style dictionaries. - emotion_style[key] = format_as_emotion(value) - else: - emotion_style[key] = value - if emotion_style: - if var_data is not None: - emotion_style._var_data = VarData.merge(emotion_style._var_data, var_data) - return emotion_style - return None - - -def convert_dict_to_style_and_format_emotion( - raw_dict: dict[str, Any], -) -> dict[str, Any] | None: - """Convert a dict to a style dict and then format as emotion. - - Args: - raw_dict: The dict to convert. - - Returns: - The emotion dict. - - """ - return format_as_emotion(Style(raw_dict)) - - -STACK_CHILDREN_FULL_WIDTH = { - "& :where(.rx-Stack)": { - "width": "100%", - }, - "& :where(.rx-Stack) > :where( " - "div:not(.rt-Box, .rx-Upload, .rx-Html)," - "input, select, textarea, table" - ")": { - "width": "100%", - "flex_shrink": "1", - }, -} +from reflex_core.style import * diff --git a/reflex/testing.py b/reflex/testing.py index 66e1f11b485..c8d94b0bc2b 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -26,6 +26,10 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar import uvicorn +from reflex_core.components.component import CUSTOM_COMPONENTS, CustomComponent +from reflex_core.config import get_config +from reflex_core.environment import environment +from reflex_core.utils.types import ASGIApp from typing_extensions import Self import reflex @@ -34,9 +38,6 @@ import reflex.utils.format import reflex.utils.prerequisites import reflex.utils.processes -from reflex.components.component import CUSTOM_COMPONENTS, CustomComponent -from reflex.config import get_config -from reflex.environment import environment from reflex.experimental.memo import EXPERIMENTAL_MEMOS from reflex.istate.manager.disk import StateManagerDisk from reflex.istate.manager.memory import StateManagerMemory @@ -50,7 +51,6 @@ from reflex.utils import console, js_runtimes from reflex.utils.export import export from reflex.utils.token_manager import TokenManager -from reflex.utils.types import ASGIApp try: from selenium import webdriver @@ -274,7 +274,7 @@ def _initialize_app(self): reflex.utils.prerequisites.initialize_frontend_dependencies() with chdir(self.app_path): # ensure config and app are reloaded when testing different app - config = reflex.config.get_config(reload=True) + config = get_config(reload=True) # Ensure the AppHarness test does not skip State assignment due to running via pytest os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None) os.environ[reflex.constants.APP_HARNESS_FLAG] = "true" @@ -405,7 +405,7 @@ async def _reset_backend_state_manager(self): def _start_frontend(self): # Set up the frontend. with chdir(self.app_path): - config = reflex.config.get_config() + config = get_config() print("Polling for servers...") # for pytest diagnosis #noqa: T201 config.api_url = "http://{}:{}".format( *self._poll_for_servers(timeout=30).getsockname(), @@ -439,7 +439,7 @@ def _wait_frontend(self): m = re.search(reflex.constants.ReactRouter.FRONTEND_LISTENING_REGEX, line) if m is not None: self.frontend_url = m.group(1) - config = reflex.config.get_config() + config = get_config() config.deploy_url = self.frontend_url break if self.frontend_url is None: @@ -1075,7 +1075,7 @@ def _run_frontend(self): def _start_frontend(self): # Set up the frontend. with chdir(self.app_path): - config = reflex.config.get_config() + config = get_config() print("Polling for servers...") # for pytest diagnosis #noqa: T201 config.api_url = "http://{}:{}".format( *self._poll_for_servers(timeout=30).getsockname(), diff --git a/reflex/utils/build.py b/reflex/utils/build.py index 7b7408f8b3b..605d933908a 100644 --- a/reflex/utils/build.py +++ b/reflex/utils/build.py @@ -6,10 +6,10 @@ import zipfile from pathlib import Path, PosixPath +from reflex_core import constants +from reflex_core.config import get_config from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn -from reflex import constants -from reflex.config import get_config from reflex.utils import console, js_runtimes, path_ops, prerequisites, processes from reflex.utils.exec import is_in_app_harness diff --git a/reflex/utils/codespaces.py b/reflex/utils/codespaces.py index 03954911952..aeb53925d7e 100644 --- a/reflex/utils/codespaces.py +++ b/reflex/utils/codespaces.py @@ -4,16 +4,15 @@ import os +from reflex_components_core.base.script import Script +from reflex_components_core.core.banner import has_connection_errors +from reflex_components_core.core.cond import cond +from reflex_core.components.component import Component +from reflex_core.constants import Endpoint +from reflex_core.utils.decorator import once from starlette.requests import Request from starlette.responses import HTMLResponse -from reflex.components.base.script import Script -from reflex.components.component import Component -from reflex.components.core.banner import has_connection_errors -from reflex.components.core.cond import cond -from reflex.constants import Endpoint -from reflex.utils.decorator import once - @once def redirect_script() -> str: diff --git a/reflex/utils/compat.py b/reflex/utils/compat.py index 7a466aefc4d..6a3570f0c0a 100644 --- a/reflex/utils/compat.py +++ b/reflex/utils/compat.py @@ -1,95 +1,3 @@ -"""Compatibility hacks and helpers.""" +"""Re-export from reflex_core.""" -import sys -from collections.abc import Mapping -from importlib.util import find_spec -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from pydantic.fields import FieldInfo - - -async def windows_hot_reload_lifespan_hack(): - """[REF-3164] A hack to fix hot reload on Windows. - - Uvicorn has an issue stopping itself on Windows after detecting changes in - the filesystem. - - This workaround repeatedly prints and flushes null characters to stderr, - which seems to allow the uvicorn server to exit when the CTRL-C signal is - sent from the reloader process. - - Don't ask me why this works, I discovered it by accident - masenf. - """ - import asyncio - import sys - - try: - while True: - sys.stderr.write("\0") - sys.stderr.flush() - await asyncio.sleep(0.5) - except asyncio.CancelledError: - pass - - -def annotations_from_namespace(namespace: Mapping[str, Any]) -> dict[str, Any]: - """Get the annotations from a class namespace. - - Args: - namespace: The class namespace. - - Returns: - The (forward-ref) annotations from the class namespace. - """ - if sys.version_info >= (3, 14) and "__annotations__" not in namespace: - from annotationlib import ( - Format, - call_annotate_function, - get_annotate_from_class_namespace, - ) - - if annotate := get_annotate_from_class_namespace(namespace): - return call_annotate_function(annotate, format=Format.FORWARDREF) - return namespace.get("__annotations__", {}) - - -if find_spec("pydantic") and find_spec("pydantic.v1"): - from pydantic.v1.main import ModelMetaclass - - class ModelMetaclassLazyAnnotations(ModelMetaclass): - """Compatibility metaclass to resolve python3.14 style lazy annotations.""" - - def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs): - """Resolve python3.14 style lazy annotations before passing off to pydantic v1. - - Args: - name: The class name. - bases: The base classes. - namespace: The class namespace. - **kwargs: Additional keyword arguments. - - Returns: - The created class. - """ - namespace["__annotations__"] = annotations_from_namespace(namespace) - return super().__new__(mcs, name, bases, namespace, **kwargs) - -else: - ModelMetaclassLazyAnnotations = type # type: ignore[assignment] - - -def sqlmodel_field_has_primary_key(field_info: "FieldInfo") -> bool: - """Determines if a field is a primary. - - Args: - field_info: a rx.model field - - Returns: - If field_info is a primary key (Bool) - """ - if getattr(field_info, "primary_key", None) is True: - return True - if getattr(field_info, "sa_column", None) is None: - return False - return bool(getattr(field_info.sa_column, "primary_key", None)) # pyright: ignore[reportAttributeAccessIssue] +from reflex_core.utils.compat import * diff --git a/reflex/utils/console.py b/reflex/utils/console.py index 0fdaed5cc9e..dc03bd72c23 100644 --- a/reflex/utils/console.py +++ b/reflex/utils/console.py @@ -1,473 +1,3 @@ -"""Functions to communicate to the user via console.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import contextlib -import datetime -import inspect -import os -import shutil -import sys -import time -from pathlib import Path -from types import FrameType - -from rich.console import Console -from rich.progress import MofNCompleteColumn, Progress, TaskID, TimeElapsedColumn -from rich.prompt import Prompt - -from reflex.constants import LogLevel -from reflex.constants.base import Reflex -from reflex.utils.decorator import once - -# Console for pretty printing. -_console = Console(highlight=False) -_console_stderr = Console(stderr=True, highlight=False) - -# The current log level. -_LOG_LEVEL = LogLevel.INFO - -# Deprecated features who's warning has been printed. -_EMITTED_DEPRECATION_WARNINGS = set() - -# Info messages which have been printed. -_EMITTED_INFO = set() - -# Warnings which have been printed. -_EMITTED_WARNINGS = set() - -# Errors which have been printed. -_EMITTED_ERRORS = set() - -# Success messages which have been printed. -_EMITTED_SUCCESS = set() - -# Debug messages which have been printed. -_EMITTED_DEBUG = set() - -# Logs which have been printed. -_EMITTED_LOGS = set() - -# Prints which have been printed. -_EMITTED_PRINTS = set() - - -def set_log_level(log_level: LogLevel | None): - """Set the log level. - - Args: - log_level: The log level to set. - - Raises: - TypeError: If the log level is a string. - """ - if log_level is None: - return - if not isinstance(log_level, LogLevel): - msg = f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead." - raise TypeError(msg) - global _LOG_LEVEL - if log_level != _LOG_LEVEL: - # Set the loglevel persistenly for subprocesses. - os.environ["REFLEX_LOGLEVEL"] = log_level.value - _LOG_LEVEL = log_level - - -def is_debug() -> bool: - """Check if the log level is debug. - - Returns: - True if the log level is debug. - """ - return _LOG_LEVEL <= LogLevel.DEBUG - - -def print(msg: str, *, dedupe: bool = False, **kwargs): - """Print a message. - - Args: - msg: The message to print. - dedupe: If True, suppress multiple console logs of print message. - kwargs: Keyword arguments to pass to the print function. - """ - if dedupe: - if msg in _EMITTED_PRINTS: - return - _EMITTED_PRINTS.add(msg) - _console.print(msg, **kwargs) - - -def _print_stderr(msg: str, *, dedupe: bool = False, **kwargs): - """Print a message to stderr. - - Args: - msg: The message to print. - dedupe: If True, suppress multiple console logs of print message. - kwargs: Keyword arguments to pass to the print function. - """ - if dedupe: - if msg in _EMITTED_PRINTS: - return - _EMITTED_PRINTS.add(msg) - _console_stderr.print(msg, **kwargs) - - -@once -def log_file_console(): - """Create a console that logs to a file. - - Returns: - A Console object that logs to a file. - """ - from reflex.environment import environment - - if not (env_log_file := environment.REFLEX_LOG_FILE.get()): - subseconds = int((time.time() % 1) * 1000) - timestamp = time.strftime("%Y-%m-%d_%H-%M-%S") + f"_{subseconds:03d}" - log_file = Reflex.DIR / "logs" / (timestamp + ".log") - log_file.parent.mkdir(parents=True, exist_ok=True) - else: - log_file = env_log_file - if log_file.exists(): - log_file.unlink() - log_file.touch() - return Console(file=log_file.open("a", encoding="utf-8")) - - -@once -def should_use_log_file_console() -> bool: - """Check if the log file console should be used. - - Returns: - True if the log file console should be used, False otherwise. - """ - from reflex.environment import environment - - return environment.REFLEX_ENABLE_FULL_LOGGING.get() - - -def print_to_log_file(msg: str, *, dedupe: bool = False, **kwargs): - """Print a message to the log file. - - Args: - msg: The message to print. - dedupe: If True, suppress multiple console logs of print message. - kwargs: Keyword arguments to pass to the print function. - """ - log_file_console().print(f"[{datetime.datetime.now()}] {msg}", **kwargs) - - -def debug(msg: str, *, dedupe: bool = False, **kwargs): - """Print a debug message. - - Args: - msg: The debug message. - dedupe: If True, suppress multiple console logs of debug message. - kwargs: Keyword arguments to pass to the print function. - """ - if is_debug(): - msg_ = f"[purple]Debug: {msg}[/purple]" - if dedupe: - if msg_ in _EMITTED_DEBUG: - return - _EMITTED_DEBUG.add(msg_) - if progress := kwargs.pop("progress", None): - progress.console.print(msg_, **kwargs) - else: - print(msg_, **kwargs) - if should_use_log_file_console() and kwargs.pop("progress", None) is None: - print_to_log_file(f"[purple]Debug: {msg}[/purple]", **kwargs) - - -def info(msg: str, *, dedupe: bool = False, **kwargs): - """Print an info message. - - Args: - msg: The info message. - dedupe: If True, suppress multiple console logs of info message. - kwargs: Keyword arguments to pass to the print function. - """ - if _LOG_LEVEL <= LogLevel.INFO: - if dedupe: - if msg in _EMITTED_INFO: - return - _EMITTED_INFO.add(msg) - print(f"[cyan]Info: {msg}[/cyan]", **kwargs) - if should_use_log_file_console(): - print_to_log_file(f"[cyan]Info: {msg}[/cyan]", **kwargs) - - -def success(msg: str, *, dedupe: bool = False, **kwargs): - """Print a success message. - - Args: - msg: The success message. - dedupe: If True, suppress multiple console logs of success message. - kwargs: Keyword arguments to pass to the print function. - """ - if _LOG_LEVEL <= LogLevel.INFO: - if dedupe: - if msg in _EMITTED_SUCCESS: - return - _EMITTED_SUCCESS.add(msg) - print(f"[green]Success: {msg}[/green]", **kwargs) - if should_use_log_file_console(): - print_to_log_file(f"[green]Success: {msg}[/green]", **kwargs) - - -def log(msg: str, *, dedupe: bool = False, **kwargs): - """Takes a string and logs it to the console. - - Args: - msg: The message to log. - dedupe: If True, suppress multiple console logs of log message. - kwargs: Keyword arguments to pass to the print function. - """ - if _LOG_LEVEL <= LogLevel.INFO: - if dedupe: - if msg in _EMITTED_LOGS: - return - _EMITTED_LOGS.add(msg) - _console.log(msg, **kwargs) - if should_use_log_file_console(): - print_to_log_file(msg, **kwargs) - - -def rule(title: str, **kwargs): - """Prints a horizontal rule with a title. - - Args: - title: The title of the rule. - kwargs: Keyword arguments to pass to the print function. - """ - _console.rule(title, **kwargs) - - -def warn(msg: str, *, dedupe: bool = False, **kwargs): - """Print a warning message. - - Args: - msg: The warning message. - dedupe: If True, suppress multiple console logs of warning message. - kwargs: Keyword arguments to pass to the print function. - """ - if _LOG_LEVEL <= LogLevel.WARNING: - if dedupe: - if msg in _EMITTED_WARNINGS: - return - _EMITTED_WARNINGS.add(msg) - print(f"[orange1]Warning: {msg}[/orange1]", **kwargs) - if should_use_log_file_console(): - print_to_log_file(f"[orange1]Warning: {msg}[/orange1]", **kwargs) - - -@once -def _exclude_paths_from_frame_info() -> list[Path]: - import importlib.util - - import click - import granian - import socketio - import typing_extensions - - import reflex as rx - - # Exclude utility modules that should never be the source of deprecated reflex usage. - exclude_modules = [click, rx, typing_extensions, socketio, granian] - modules_paths = [file for m in exclude_modules if (file := m.__file__)] + [ - spec.origin - for m in [*sys.builtin_module_names, *sys.stdlib_module_names] - if (spec := importlib.util.find_spec(m)) and spec.origin - ] - exclude_roots = [ - p.parent.resolve() if (p := Path(file)).name == "__init__.py" else p.resolve() - for file in modules_paths - ] - # Specifically exclude the reflex cli module. - if reflex_bin := shutil.which(b"reflex"): - exclude_roots.append(Path(reflex_bin.decode())) - - return exclude_roots - - -def _get_first_non_framework_frame() -> FrameType | None: - exclude_roots = _exclude_paths_from_frame_info() - - frame = inspect.currentframe() - while frame := frame and frame.f_back: - frame_path = Path(inspect.getfile(frame)).resolve() - if not any(frame_path.is_relative_to(root) for root in exclude_roots): - break - return frame - - -def deprecate( - *, - feature_name: str, - reason: str, - deprecation_version: str, - removal_version: str, - dedupe: bool = True, - **kwargs, -): - """Print a deprecation warning. - - Args: - feature_name: The feature to deprecate. - reason: The reason for deprecation. - deprecation_version: The version the feature was deprecated - removal_version: The version the deprecated feature will be removed - dedupe: If True, suppress multiple console logs of deprecation message. - kwargs: Keyword arguments to pass to the print function. - """ - dedupe_key = feature_name - loc = "" - - # See if we can find where the deprecation exists in "user code" - origin_frame = _get_first_non_framework_frame() - if origin_frame is not None: - filename = Path(origin_frame.f_code.co_filename) - if filename.is_relative_to(Path.cwd()): - filename = filename.relative_to(Path.cwd()) - loc = f" ({filename}:{origin_frame.f_lineno})" - dedupe_key = f"{dedupe_key} {loc}" - - if dedupe_key not in _EMITTED_DEPRECATION_WARNINGS: - msg = ( - f"{feature_name} has been deprecated in version {deprecation_version}. {reason.rstrip('.').lstrip('. ')}. It will be completely " - f"removed in {removal_version}.{loc}" - ) - if _LOG_LEVEL <= LogLevel.WARNING: - print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs) - if should_use_log_file_console(): - print_to_log_file(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs) - if dedupe: - _EMITTED_DEPRECATION_WARNINGS.add(dedupe_key) - - -def error(msg: str, *, dedupe: bool = False, **kwargs): - """Print an error message. - - Args: - msg: The error message. - dedupe: If True, suppress multiple console logs of error message. - kwargs: Keyword arguments to pass to the print function. - """ - if _LOG_LEVEL <= LogLevel.ERROR: - if dedupe: - if msg in _EMITTED_ERRORS: - return - _EMITTED_ERRORS.add(msg) - _print_stderr(f"[red]{msg}[/red]", **kwargs) - if should_use_log_file_console(): - print_to_log_file(f"[red]{msg}[/red]", **kwargs) - - -def ask( - question: str, - choices: list[str] | None = None, - default: str | None = None, - show_choices: bool = True, -) -> str | None: - """Takes a prompt question and optionally a list of choices - and returns the user input. - - Args: - question: The question to ask the user. - choices: A list of choices to select from. - default: The default option selected. - show_choices: Whether to show the choices. - - Returns: - A string with the user input. - """ - return Prompt.ask( - question, choices=choices, default=default, show_choices=show_choices - ) - - -def progress(): - """Create a new progress bar. - - Returns: - A new progress bar. - """ - return Progress( - *Progress.get_default_columns()[:-1], - MofNCompleteColumn(), - TimeElapsedColumn(), - ) - - -def status(*args, **kwargs): - """Create a status with a spinner. - - Args: - *args: Args to pass to the status. - **kwargs: Kwargs to pass to the status. - - Returns: - A new status. - """ - return _console.status(*args, **kwargs) - - -@contextlib.contextmanager -def timing(msg: str): - """Create a context manager to time a block of code. - - Args: - msg: The message to display. - - Yields: - None. - """ - start = time.time() - try: - yield - finally: - debug(f"[white]\\[timing] {msg}: {time.time() - start:.2f}s[/white]") - - -class PoorProgress: - """A poor man's progress bar.""" - - def __init__(self): - """Initialize the progress bar.""" - super().__init__() - self.tasks = {} - self.progress = 0 - self.total = 0 - - def add_task(self, task: str, total: int): - """Add a task to the progress bar. - - Args: - task: The task name. - total: The total number of steps for the task. - - Returns: - The task ID. - """ - self.total += total - task_id = TaskID(len(self.tasks)) - self.tasks[task_id] = {"total": total, "current": 0} - return task_id - - def advance(self, task: TaskID, advance: int = 1): - """Advance the progress of a task. - - Args: - task: The task ID. - advance: The number of steps to advance. - """ - if task in self.tasks: - self.tasks[task]["current"] += advance - self.progress += advance - _console.print(f"Progress: {self.progress}/{self.total}") - - def start(self): - """Start the progress bar.""" - - def stop(self): - """Stop the progress bar.""" +from reflex_core.utils.console import * diff --git a/reflex/utils/decorator.py b/reflex/utils/decorator.py index bdec72807c4..908ddbc7d25 100644 --- a/reflex/utils/decorator.py +++ b/reflex/utils/decorator.py @@ -1,148 +1,3 @@ -"""Decorator utilities.""" +"""Re-export from reflex_core.""" -import functools -from collections.abc import Callable -from pathlib import Path -from typing import ParamSpec, TypeVar, cast - -T = TypeVar("T") - - -def once(f: Callable[[], T]) -> Callable[[], T]: - """A decorator that calls the function once and caches the result. - - Args: - f: The function to call. - - Returns: - A function that calls the function once and caches the result. - """ - unset = object() - value: object | T = unset - - @functools.wraps(f) - def wrapper() -> T: - nonlocal value - value = f() if value is unset else value - return value # pyright: ignore[reportReturnType] - - return wrapper - - -def once_unless_none(f: Callable[[], T | None]) -> Callable[[], T | None]: - """A decorator that calls the function once and caches the result unless it is None. - - Args: - f: The function to call. - - Returns: - A function that calls the function once and caches the result unless it is None. - """ - value: T | None = None - - @functools.wraps(f) - def wrapper() -> T | None: - nonlocal value - value = f() if value is None else value - return value - - return wrapper - - -P = ParamSpec("P") - - -def debug(f: Callable[P, T]) -> Callable[P, T]: - """A decorator that prints the function name, arguments, and result. - - Args: - f: The function to call. - - Returns: - A function that prints the function name, arguments, and result. - """ - - @functools.wraps(f) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - result = f(*args, **kwargs) - print( # noqa: T201 - f"Calling {f.__name__} with args: {args} and kwargs: {kwargs}, result: {result}" - ) - return result - - return wrapper - - -def _write_cached_procedure_file(payload: str, cache_file: Path, value: object): - import pickle - - cache_file.parent.mkdir(parents=True, exist_ok=True) - cache_file.write_bytes(pickle.dumps((payload, value))) - - -def _read_cached_procedure_file(cache_file: Path) -> tuple[str | None, object]: - import pickle - - if cache_file.exists(): - with cache_file.open("rb") as f: - return pickle.loads(f.read()) - - return None, None - - -P = ParamSpec("P") -Picklable = TypeVar("Picklable") - - -def cached_procedure( - cache_file_path: Callable[[], Path], - payload_fn: Callable[P, str], -) -> Callable[[Callable[P, Picklable]], Callable[P, Picklable]]: - """Decorator to cache the result of a function based on its arguments. - - Args: - cache_file_path: Function that computes the cache file path. - payload_fn: Function that computes cache payload from function args. - - Returns: - The decorated function. - """ - - def _inner_decorator(func: Callable[P, Picklable]) -> Callable[P, Picklable]: - def _inner(*args: P.args, **kwargs: P.kwargs) -> Picklable: - cache_file = cache_file_path() - - payload, value = _read_cached_procedure_file(cache_file) - new_payload = payload_fn(*args, **kwargs) - - if payload != new_payload: - new_value = func(*args, **kwargs) - _write_cached_procedure_file(new_payload, cache_file, new_value) - return new_value - - from reflex.utils import console - - console.debug( - f"Using cached value for {func.__name__} with payload: {new_payload}" - ) - return cast("Picklable", value) - - return _inner - - return _inner_decorator - - -def cache_result_in_disk( - cache_file_path: Callable[[], Path], -) -> Callable[[Callable[[], Picklable]], Callable[[], Picklable]]: - """Decorator to cache the result of a function on disk. - - Args: - cache_file_path: Function that computes the cache file path. - - Returns: - The decorated function. - """ - return cached_procedure( - cache_file_path=cache_file_path, payload_fn=lambda: "constant" - ) +from reflex_core.utils.decorator import * diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index 4c4e2ad35c5..d0cc24d089e 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -1,286 +1,3 @@ -"""Custom Exceptions.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from reflex.vars import Var - - -class ReflexError(Exception): - """Base exception for all Reflex exceptions.""" - - -class ConfigError(ReflexError): - """Custom exception for config related errors.""" - - -class InvalidStateManagerModeError(ReflexError, ValueError): - """Raised when an invalid state manager mode is provided.""" - - -class ReflexRuntimeError(ReflexError, RuntimeError): - """Custom RuntimeError for Reflex.""" - - -class UploadTypeError(ReflexError, TypeError): - """Custom TypeError for upload related errors.""" - - -class EnvVarValueError(ReflexError, ValueError): - """Custom ValueError raised when unable to convert env var to expected type.""" - - -class ComponentTypeError(ReflexError, TypeError): - """Custom TypeError for component related errors.""" - - -class ChildrenTypeError(ComponentTypeError): - """Raised when the children prop of a component is not a valid type.""" - - def __init__(self, component: str, child: Any): - """Initialize the exception. - - Args: - component: The name of the component. - child: The child that caused the error. - """ - super().__init__( - f"Component {component} received child {child} of type {type(child)}. " - "Accepted types are other components, state vars, or primitive Python types (dict excluded)." - ) - - -class EventHandlerTypeError(ReflexError, TypeError): - """Custom TypeError for event handler related errors.""" - - -class EventHandlerValueError(ReflexError, ValueError): - """Custom ValueError for event handler related errors.""" - - -class StateValueError(ReflexError, ValueError): - """Custom ValueError for state related errors.""" - - -class VarNameError(ReflexError, NameError): - """Custom NameError for when a state var has been shadowed by a substate var.""" - - -class VarTypeError(ReflexError, TypeError): - """Custom TypeError for var related errors.""" - - -class VarValueError(ReflexError, ValueError): - """Custom ValueError for var related errors.""" - - -class VarAttributeError(ReflexError, AttributeError): - """Custom AttributeError for var related errors.""" - - -class UntypedVarError(ReflexError, TypeError): - """Custom TypeError for untyped var errors.""" - - def __init__(self, var: Var, action: str, doc_link: str = ""): - """Create an UntypedVarError from a var. - - Args: - var: The var. - action: The action that caused the error. - doc_link: The link to the documentation. - """ - var_data = var._get_all_var_data() - is_state_var = ( - var_data - and var_data.state - and var_data.field_name - and var_data.state + "." + var_data.field_name == str(var) - ) - super().__init__( - f"Cannot {action} on untyped var '{var!s}' of type '{var._var_type!s}'." - + ( - " Please add a type annotation to the var in the state class." - if is_state_var - else " You can call the var's .to(desired_type) method to convert it to the desired type." - ) - + (f" See {doc_link}" if doc_link else "") - ) - - -class UntypedComputedVarError(ReflexError, TypeError): - """Custom TypeError for untyped computed var errors.""" - - def __init__(self, var_name: str): - """Initialize the UntypedComputedVarError. - - Args: - var_name: The name of the computed var. - """ - super().__init__(f"Computed var '{var_name}' must have a type annotation.") - - -class ComputedVarSignatureError(ReflexError, TypeError): - """Custom TypeError for computed var signature errors.""" - - def __init__(self, var_name: str, signature: str): - """Initialize the ComputedVarSignatureError. - - Args: - var_name: The name of the var. - signature: The invalid signature. - """ - super().__init__(f"Computed var `{var_name}{signature}` cannot take arguments.") - - -class MissingAnnotationError(ReflexError, TypeError): - """Custom TypeError for missing annotations.""" - - def __init__(self, var_name: str): - """Initialize the MissingAnnotationError. - - Args: - var_name: The name of the var. - """ - super().__init__(f"Var '{var_name}' must have a type annotation.") - - -class UploadValueError(ReflexError, ValueError): - """Custom ValueError for upload related errors.""" - - -class PageValueError(ReflexError, ValueError): - """Custom ValueError for page related errors.""" - - -class RouteValueError(ReflexError, ValueError): - """Custom ValueError for route related errors.""" - - -class VarOperationTypeError(ReflexError, TypeError): - """Custom TypeError for when unsupported operations are performed on vars.""" - - -class VarDependencyError(ReflexError, ValueError): - """Custom ValueError for when a var depends on a non-existent var.""" - - -class InvalidStylePropError(ReflexError, TypeError): - """Custom Type Error when style props have invalid values.""" - - -class ImmutableStateError(ReflexError): - """Raised when a background task attempts to modify state outside of context.""" - - -class LockExpiredError(ReflexError): - """Raised when the state lock expires while an event is being processed.""" - - -class MatchTypeError(ReflexError, TypeError): - """Raised when the return types of match cases are different.""" - - -class EventHandlerArgTypeMismatchError(ReflexError, TypeError): - """Raised when the annotations of args accepted by an EventHandler differs from the spec of the event trigger.""" - - -class EventFnArgMismatchError(ReflexError, TypeError): - """Raised when the number of args required by an event handler is more than provided by the event trigger.""" - - -class DynamicRouteArgShadowsStateVarError(ReflexError, NameError): - """Raised when a dynamic route arg shadows a state var.""" - - -class ComputedVarShadowsStateVarError(ReflexError, NameError): - """Raised when a computed var shadows a state var.""" - - -class ComputedVarShadowsBaseVarsError(ReflexError, NameError): - """Raised when a computed var shadows a base var.""" - - -class EventHandlerShadowsBuiltInStateMethodError(ReflexError, NameError): - """Raised when an event handler shadows a built-in state method.""" - - -class GeneratedCodeHasNoFunctionDefsError(ReflexError): - """Raised when refactored code generated with flexgen has no functions defined.""" - - -class PrimitiveUnserializableToJSONError(ReflexError, ValueError): - """Raised when a primitive type is unserializable to JSON. Usually with NaN and Infinity.""" - - -class InvalidLifespanTaskTypeError(ReflexError, TypeError): - """Raised when an invalid task type is registered as a lifespan task.""" - - -class DynamicComponentMissingLibraryError(ReflexError, ValueError): - """Raised when a dynamic component is missing a library.""" - - -class SetUndefinedStateVarError(ReflexError, AttributeError): - """Raised when setting the value of a var without first declaring it.""" - - -class StateSchemaMismatchError(ReflexError, TypeError): - """Raised when the serialized schema of a state class does not match the current schema.""" - - -class EnvironmentVarValueError(ReflexError, ValueError): - """Raised when an environment variable is set to an invalid value.""" - - -class DynamicComponentInvalidSignatureError(ReflexError, TypeError): - """Raised when a dynamic component has an invalid signature.""" - - -class InvalidPropValueError(ReflexError): - """Raised when a prop value is invalid.""" - - -class StateTooLargeError(ReflexError): - """Raised when the state is too large to be serialized.""" - - -class StateSerializationError(ReflexError): - """Raised when the state cannot be serialized.""" - - -class StateMismatchError(ReflexError, ValueError): - """Raised when the state retrieved does not match the expected state.""" - - -class SystemPackageMissingError(ReflexError): - """Raised when a system package is missing.""" - - def __init__(self, package: str): - """Initialize the SystemPackageMissingError. - - Args: - package: The missing package. - """ - from reflex.constants import IS_MACOS - - extra = ( - f" You can do so by running 'brew install {package}'." if IS_MACOS else "" - ) - super().__init__( - f"System package '{package}' is missing." - f" Please install it through your system package manager.{extra}" - ) - - -class EventDeserializationError(ReflexError, ValueError): - """Raised when an event cannot be deserialized.""" - - -class InvalidLockWarningThresholdError(ReflexError): - """Raised when an invalid lock warning threshold is provided.""" - - -class UnretrievableVarValueError(ReflexError): - """Raised when the value of a var is not retrievable.""" +from reflex_core.utils.exceptions import * diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index c67976b23b2..e8e01d4bcba 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -15,12 +15,14 @@ from typing import Any, NamedTuple, TypedDict from urllib.parse import urljoin -from reflex import constants -from reflex.config import get_config -from reflex.constants.base import LogLevel -from reflex.environment import environment -from reflex.utils import console, path_ops -from reflex.utils.decorator import once +from reflex_core import constants +from reflex_core.config import get_config +from reflex_core.constants.base import LogLevel +from reflex_core.environment import environment +from reflex_core.utils import console +from reflex_core.utils.decorator import once + +from reflex.utils import path_ops from reflex.utils.misc import get_module_path from reflex.utils.prerequisites import get_web_dir @@ -539,8 +541,7 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel): from granian.constants import Interfaces from granian.log import LogLevels from granian.server import Server as Granian - - from reflex.environment import _load_dotenv_from_env + from reflex_core.environment import _load_dotenv_from_env granian_app = Granian( target=get_app_instance_from_file(), diff --git a/reflex/utils/export.py b/reflex/utils/export.py index cc58a659f25..cc6aa783fea 100644 --- a/reflex/utils/export.py +++ b/reflex/utils/export.py @@ -2,10 +2,12 @@ from pathlib import Path -from reflex import constants -from reflex.config import get_config -from reflex.environment import environment -from reflex.utils import build, console, exec, prerequisites, telemetry +from reflex_core import constants +from reflex_core.config import get_config +from reflex_core.environment import environment +from reflex_core.utils import console + +from reflex.utils import build, exec, prerequisites, telemetry def export( diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 4f847487649..b5bfc9d23b3 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -1,774 +1,3 @@ -"""Formatting operations.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import inspect -import json -import os -import re -from typing import TYPE_CHECKING, Any - -from reflex import constants -from reflex.constants.state import FRONTEND_EVENT_STATE -from reflex.utils import exceptions - -if TYPE_CHECKING: - from reflex.components.component import ComponentStyle - from reflex.event import ArgsSpec, EventChain, EventHandler, EventSpec, EventType - -WRAP_MAP = { - "{": "}", - "(": ")", - "[": "]", - "<": ">", - '"': '"', - "'": "'", - "`": "`", -} - - -def length_of_largest_common_substring(str1: str, str2: str) -> int: - """Find the length of the largest common substring between two strings. - - Args: - str1: The first string. - str2: The second string. - - Returns: - The length of the largest common substring. - """ - if not str1 or not str2: - return 0 - - # Create a matrix of size (len(str1) + 1) x (len(str2) + 1) - dp = [[0] * (len(str2) + 1) for _ in range(len(str1) + 1)] - - # Variables to keep track of maximum length and ending position - max_length = 0 - - # Fill the dp matrix - for i in range(1, len(str1) + 1): - for j in range(1, len(str2) + 1): - if str1[i - 1] == str2[j - 1]: - dp[i][j] = dp[i - 1][j - 1] + 1 - if dp[i][j] > max_length: - max_length = dp[i][j] - - return max_length - - -def get_close_char(open: str, close: str | None = None) -> str: - """Check if the given character is a valid brace. - - Args: - open: The open character. - close: The close character if provided. - - Returns: - The close character. - - Raises: - ValueError: If the open character is not a valid brace. - """ - if close is not None: - return close - if open not in WRAP_MAP: - msg = f"Invalid wrap open: {open}, must be one of {WRAP_MAP.keys()}" - raise ValueError(msg) - return WRAP_MAP[open] - - -def is_wrapped(text: str, open: str, close: str | None = None) -> bool: - """Check if the given text is wrapped in the given open and close characters. - - "(a) + (b)" --> False - "((abc))" --> True - "(abc)" --> True - - Args: - text: The text to check. - open: The open character. - close: The close character. - - Returns: - Whether the text is wrapped. - """ - close = get_close_char(open, close) - if not (text.startswith(open) and text.endswith(close)): - return False - - depth = 0 - for ch in text[:-1]: - if ch == open: - depth += 1 - if ch == close: - depth -= 1 - if depth == 0: # it shouldn't close before the end - return False - return True - - -def wrap( - text: str, - open: str, - close: str | None = None, - check_first: bool = True, - num: int = 1, -) -> str: - """Wrap the given text in the given open and close characters. - - Args: - text: The text to wrap. - open: The open character. - close: The close character. - check_first: Whether to check if the text is already wrapped. - num: The number of times to wrap the text. - - Returns: - The wrapped text. - """ - close = get_close_char(open, close) - - # If desired, check if the text is already wrapped in braces. - if check_first and is_wrapped(text=text, open=open, close=close): - return text - - # Wrap the text in braces. - return f"{open * num}{text}{close * num}" - - -def indent(text: str, indent_level: int = 2) -> str: - """Indent the given text by the given indent level. - - Args: - text: The text to indent. - indent_level: The indent level. - - Returns: - The indented text. - """ - lines = text.splitlines() - if len(lines) < 2: - return text - return os.linesep.join(f"{' ' * indent_level}{line}" for line in lines) + os.linesep - - -def to_snake_case(text: str) -> str: - """Convert a string to snake case. - - The words in the text are converted to lowercase and - separated by underscores. - - Args: - text: The string to convert. - - Returns: - The snake case string. - """ - s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", text) - return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower().replace("-", "_") - - -def to_camel_case(text: str, treat_hyphens_as_underscores: bool = True) -> str: - """Convert a string to camel case. - - The first word in the text is converted to lowercase and - the rest of the words are converted to title case, removing underscores. - - Args: - text: The string to convert. - treat_hyphens_as_underscores: Whether to allow hyphens in the string. - - Returns: - The camel case string. - """ - if treat_hyphens_as_underscores: - text = text.replace("-", "_") - words = text.split("_") - # Capitalize the first letter of each word except the first one - if len(words) == 1: - return words[0] - return words[0] + "".join([w.capitalize() for w in words[1:]]) - - -def to_title_case(text: str, sep: str = "") -> str: - """Convert a string from snake case to title case. - - Args: - text: The string to convert. - sep: The separator to use to join the words. - - Returns: - The title case string. - """ - return sep.join(word.title() for word in text.split("_")) - - -def to_kebab_case(text: str) -> str: - """Convert a string to kebab case. - - The words in the text are converted to lowercase and - separated by hyphens. - - Args: - text: The string to convert. - - Returns: - The title case string. - """ - return to_snake_case(text).replace("_", "-") - - -def make_default_page_title(app_name: str, route: str) -> str: - """Make a default page title from a route. - - Args: - app_name: The name of the app owning the page. - route: The route to make the title from. - - Returns: - The default page title. - """ - route_parts = [ - part - for part in route.split("/") - if part and not (part.startswith("[") and part.endswith("]")) - ] - - title = constants.DefaultPage.TITLE.format( - app_name, route_parts[-1] if route_parts else constants.PageNames.INDEX_ROUTE - ) - return to_title_case(title) - - -def _escape_js_string(string: str) -> str: - """Escape the string for use as a JS string literal. - - Args: - string: The string to escape. - - Returns: - The escaped string. - """ - - # TODO: we may need to re-visit this logic after new Var API is implemented. - def escape_outside_segments(segment: str): - """Escape backticks in segments outside of `${}`. - - Args: - segment: The part of the string to escape. - - Returns: - The escaped or unescaped segment. - """ - if segment.startswith("${") and segment.endswith("}"): - # Return the `${}` segment unchanged - return segment - # Escape backticks in the segment - return segment.replace(r"\`", "`").replace("`", r"\`") - - # Split the string into parts, keeping the `${}` segments - parts = re.split(r"(\$\{.*?\})", string) - escaped_parts = [escape_outside_segments(part) for part in parts] - return "".join(escaped_parts) - - -def _wrap_js_string(string: str) -> str: - """Wrap string so it looks like {`string`}. - - Args: - string: The string to wrap. - - Returns: - The wrapped string. - """ - string = wrap(string, "`") - return wrap(string, "{") - - -def format_string(string: str) -> str: - """Format the given string as a JS string literal.. - - Args: - string: The string to format. - - Returns: - The formatted string. - """ - return _wrap_js_string(_escape_js_string(string)) - - -def format_var(var: Var) -> str: - """Format the given Var as a javascript value. - - Args: - var: The Var to format. - - Returns: - The formatted Var. - """ - return str(var) - - -def format_route(route: str) -> str: - """Format the given route. - - Args: - route: The route to format. - - Returns: - The formatted route. - """ - route = route.strip("/") - - # If the route is empty, return the index route. - if route == "": - return constants.PageNames.INDEX_ROUTE - - return route - - -def format_match( - cond: str | Var, - match_cases: list[tuple[list[Var], Var]], - default: Var, -) -> str: - """Format a match expression whose return type is a Var. - - Args: - cond: The condition. - match_cases: The list of cases to match. - default: The default case. - - Returns: - The formatted match expression - - """ - switch_code = f"(() => {{ switch (JSON.stringify({cond})) {{" - - for case in match_cases: - conditions, return_value = case - - case_conditions = " ".join([ - f"case JSON.stringify({condition!s}):" for condition in conditions - ]) - case_code = f"{case_conditions} return ({return_value!s}); break;" - switch_code += case_code - - switch_code += f"default: return ({default!s}); break;" - switch_code += "};})()" - - return switch_code - - -def format_prop( - prop: Var | EventChain | ComponentStyle | str, -) -> int | float | str: - """Format a prop. - - Args: - prop: The prop to format. - - Returns: - The formatted prop to display within a tag. - - Raises: - exceptions.InvalidStylePropError: If the style prop value is not a valid type. - TypeError: If the prop is not valid. - ValueError: If the prop is not a string. - """ - # import here to avoid circular import. - from reflex.event import EventChain - from reflex.utils import serializers - from reflex.vars import Var - - try: - # Handle var props. - if isinstance(prop, Var): - return str(prop) - - # Handle event props. - if isinstance(prop, EventChain): - return str(Var.create(prop)) - - # Handle other types. - if isinstance(prop, str): - if is_wrapped(prop, "{"): - return prop - return json_dumps(prop) - - # For dictionaries, convert any properties to strings. - if isinstance(prop, dict): - prop = serializers.serialize_dict(prop) # pyright: ignore [reportAttributeAccessIssue] - - else: - # Dump the prop as JSON. - prop = json_dumps(prop) - except exceptions.InvalidStylePropError: - raise - except TypeError as e: - msg = f"Could not format prop: {prop} of type {type(prop)}" - raise TypeError(msg) from e - - # Wrap the variable in braces. - if not isinstance(prop, str): - msg = f"Invalid prop: {prop}. Expected a string." - raise ValueError(msg) - return wrap(prop, "{", check_first=False) - - -def format_props(*single_props, **key_value_props) -> list[str]: - """Format the tag's props. - - Args: - single_props: Props that are not key-value pairs. - key_value_props: Props that are key-value pairs. - - Returns: - The formatted props list. - """ - # Format all the props. - from reflex.vars import LiteralStringVar, LiteralVar, Var - - return [ - (str(LiteralStringVar.create(name)) if "-" in name else name) - + ":" - + str(format_prop(prop if isinstance(prop, Var) else LiteralVar.create(prop))) - for name, prop in sorted(key_value_props.items()) - if prop is not None - ] + [(f"...{LiteralVar.create(prop)!s}") for prop in single_props] - - -def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: - """Get the state and function name of an event handler. - - Args: - handler: The event handler to get the parts of. - - Returns: - The state and function name. - """ - # Get the class that defines the event handler. - parts = handler.fn.__qualname__.split(".") - - # Get the state full name - state_full_name = handler.state_full_name - - # If there's no enclosing class, just return the function name. - if not state_full_name: - return ("", parts[-1]) - - # Get the function name - name = parts[-1] - - from reflex.state import State - - if state_full_name == FRONTEND_EVENT_STATE and name not in State.__dict__: - return ("", to_snake_case(handler.fn.__qualname__)) - - return (state_full_name, name) - - -def format_event_handler(handler: EventHandler) -> str: - """Format an event handler. - - Args: - handler: The event handler to format. - - Returns: - The formatted function. - """ - state, name = get_event_handler_parts(handler) - if state == "": - return name - return f"{state}.{name}" - - -def format_event(event_spec: EventSpec) -> str: - """Format an event. - - Args: - event_spec: The event to format. - - Returns: - The compiled event. - """ - args = ",".join([ - ":".join(( - name._js_expr, - ( - wrap( - json.dumps(val._js_expr).strip('"').replace("`", "\\`"), - "`", - ) - if val._var_is_string - else str(val) - ), - )) - for name, val in event_spec.args - ]) - event_args = [ - wrap(format_event_handler(event_spec.handler), '"'), - ] - event_args.append(wrap(args, "{")) - - if event_spec.client_handler_name: - event_args.append(wrap(event_spec.client_handler_name, '"')) - return f"ReflexEvent({', '.join(event_args)})" - - -if TYPE_CHECKING: - from reflex.vars import Var - - -def format_queue_events( - events: EventType[Any] | None = None, - args_spec: ArgsSpec | None = 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. - - Raises: - ValueError: If a lambda function is given which returns a Var. - """ - from reflex.event import ( - EventChain, - EventHandler, - EventSpec, - call_event_fn, - call_event_handler, - ) - from reflex.vars import FunctionVar, Var - - if not events: - return Var("(() => null)").to(FunctionVar, 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) - 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) # pyright: ignore [reportAssignmentType, reportArgumentType] - if isinstance(specs, Var): - msg = f"Invalid event spec: {specs}. Expected a list of EventSpecs." - raise ValueError(msg) - 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( - f"{arg_def} => {{queueEvents([{','.join(payloads)}], {constants.CompileVars.SOCKET}, false, navigate, params);" - f"processEvent({constants.CompileVars.SOCKET}, navigate, params);}}", - ).to(FunctionVar, EventChain) - - -def format_query_params(router_data: dict[str, Any]) -> dict[str, str]: - """Convert back query params name to python-friendly case. - - Args: - router_data: the router_data dict containing the query params - - Returns: - The reformatted query params - """ - params = router_data[constants.RouteVar.QUERY] - return {k.replace("-", "_"): v for k, v in params.items()} - - -def format_state_name(state_name: str) -> str: - """Format a state name, replacing dots with double underscore. - - This allows individual substates to be accessed independently as javascript vars - without using dot notation. - - Args: - state_name: The state name to format. - - Returns: - The formatted state name. - """ - return state_name.replace(".", "__") - - -def format_ref(ref: str) -> str: - """Format a ref. - - Args: - ref: The ref to format. - - Returns: - The formatted ref. - """ - # Replace all non-word characters with underscores. - clean_ref = re.sub(r"[^\w]+", "_", ref) - return f"ref_{clean_ref}" - - -def format_library_name(library_fullname: str | dict[str, Any]) -> str: - """Format the name of a library. - - Args: - library_fullname: The library reference, either as a string or a dictionary with a 'name' key. - - Returns: - The name without the @version if it was part of the name - - Raises: - KeyError: If library_fullname is a dictionary without a 'name' key. - TypeError: If library_fullname or its 'name' value is not a string. - """ - # If input is a dictionary, extract the 'name' key - if isinstance(library_fullname, dict): - if "name" not in library_fullname: - msg = "Dictionary input must contain a 'name' key" - raise KeyError(msg) - library_fullname = library_fullname["name"] - - # Process the library name as a string - if not isinstance(library_fullname, str): - msg = "Library name must be a string" - raise TypeError(msg) - - if library_fullname.startswith("https://"): - return library_fullname - - lib, at, version = library_fullname.rpartition("@") - if not lib: - lib = at + version - - return lib - - -def json_dumps(obj: Any, **kwargs) -> str: - """Takes an object and returns a jsonified string. - - Args: - obj: The object to be serialized. - kwargs: Additional keyword arguments to pass to json.dumps. - - Returns: - A string - """ - from reflex.utils import serializers - - kwargs.setdefault("ensure_ascii", False) - kwargs.setdefault("default", serializers.serialize) - - return json.dumps(obj, **kwargs) - - -def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]: - """Collapse keys with consecutive suffixes into a single list value. - - Separators dash and underscore are removed, unless this would overwrite an existing key. - - Args: - form_dict: The dict to collapse. - - Returns: - The collapsed dict. - """ - ending_digit_regex = re.compile(r"^(.*?)[_-]?(\d+)$") - collapsed = {} - for k in sorted(form_dict): - m = ending_digit_regex.match(k) - if m: - collapsed.setdefault(m.group(1), []).append(form_dict[k]) - # collapsing never overwrites valid data from the form_dict - collapsed.update(form_dict) - return collapsed - - -def format_array_ref(refs: str, idx: Var | None) -> str: - """Format a ref accessed by array. - - Args: - refs : The ref array to access. - idx : The index of the ref in the array. - - Returns: - The formatted ref. - """ - clean_ref = re.sub(r"[^\w]+", "_", refs) - if idx is not None: - return f"refs_{clean_ref}[{idx!s}]" - return f"refs_{clean_ref}" - - -def format_data_editor_column(col: str | dict): - """Format a given column into the proper format. - - Args: - col: The column. - - Returns: - The formatted column. - - Raises: - ValueError: invalid type provided for column. - """ - from reflex.vars import Var - - if isinstance(col, str): - return {"title": col, "id": col.lower(), "type": "str"} - - if isinstance(col, (dict,)): - if "id" not in col: - col["id"] = col["title"].lower() - if "type" not in col: - col["type"] = "str" - if "overlayIcon" not in col: - col["overlayIcon"] = None - return col - - if isinstance(col, Var): - return col - - msg = f"unexpected type ({(type(col).__name__)}: {col}) for column header in data_editor" - raise ValueError(msg) - - -def format_data_editor_cell(cell: Any): - """Format a given data into a renderable cell for data_editor. - - Args: - cell: The data to format. - - Returns: - The formatted cell. - """ - from reflex.vars.base import Var - - return { - "kind": Var(_js_expr="GridCellKind.Text"), - "data": cell, - } +from reflex_core.utils.format import * diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 37d66fac7db..5674845cc44 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -5,10 +5,11 @@ import re from pathlib import Path -from reflex import constants +from reflex_core import constants +from reflex_core.config import Config, get_config +from reflex_core.environment import environment + from reflex.compiler import templates -from reflex.config import Config, get_config -from reflex.environment import environment from reflex.utils import console, path_ops from reflex.utils.prerequisites import get_project_hash, get_web_dir from reflex.utils.registry import get_npm_registry diff --git a/reflex/utils/imports.py b/reflex/utils/imports.py index e4ec1935739..a40f3b6648b 100644 --- a/reflex/utils/imports.py +++ b/reflex/utils/imports.py @@ -1,150 +1,3 @@ -"""Import operations.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import dataclasses -from collections import defaultdict -from collections.abc import Mapping, Sequence - - -def merge_parsed_imports( - *imports: ImmutableParsedImportDict, -) -> ParsedImportDict: - """Merge multiple parsed import dicts together. - - Args: - *imports: The list of import dicts to merge. - - Returns: - The merged import dicts. - """ - all_imports: defaultdict[str, list[ImportVar]] = defaultdict(list) - for import_dict in imports: - for lib, fields in import_dict.items(): - all_imports[lib].extend(fields) - return all_imports - - -def merge_imports( - *imports: ImportDict | ParsedImportDict | ParsedImportTuple, -) -> ParsedImportDict: - """Merge multiple import dicts together. - - Args: - *imports: The list of import dicts to merge. - - Returns: - The merged import dicts. - """ - all_imports: defaultdict[str, list[ImportVar]] = defaultdict(list) - for import_dict in imports: - for lib, fields in ( - import_dict if isinstance(import_dict, tuple) else import_dict.items() - ): - # If the lib is an absolute path, we need to prefix it with a $ - lib = ( - "$" + lib - if lib.startswith(("/utils/", "/components/", "/styles/", "/public/")) - else lib - ) - if isinstance(fields, (list, tuple, set)): - all_imports[lib].extend( - ImportVar(field) if isinstance(field, str) else field - for field in fields - ) - else: - all_imports[lib].append( - ImportVar(fields) if isinstance(fields, str) else fields - ) - return all_imports - - -def parse_imports( - imports: ImmutableImportDict | ImmutableParsedImportDict, -) -> ParsedImportDict: - """Parse the import dict into a standard format. - - Args: - imports: The import dict to parse. - - Returns: - The parsed import dict. - """ - return { - package: [maybe_tags] - if isinstance(maybe_tags, ImportVar) - else [ImportVar(tag=maybe_tags)] - if isinstance(maybe_tags, str) - else [ImportVar(tag=tag) if isinstance(tag, str) else tag for tag in maybe_tags] - for package, maybe_tags in imports.items() - } - - -def collapse_imports( - imports: ParsedImportDict | ParsedImportTuple, -) -> ParsedImportDict: - """Remove all duplicate ImportVar within an ImportDict. - - Args: - imports: The import dict to collapse. - - Returns: - The collapsed import dict. - """ - return { - lib: ( - list(set(import_vars)) - if isinstance(import_vars, list) - else list(import_vars) - ) - for lib, import_vars in ( - imports if isinstance(imports, tuple) else imports.items() - ) - } - - -@dataclasses.dataclass(frozen=True) -class ImportVar: - """An import var.""" - - # The name of the import tag. - tag: str | None - - # whether the import is default or named. - is_default: bool | None = False - - # The tag alias. - alias: str | None = None - - # Whether this import need to install the associated lib - install: bool | None = True - - # whether this import should be rendered or not - render: bool | None = True - - # The path of the package to import from. - package_path: str = "/" - - @property - def name(self) -> str: - """The name of the import. - - Returns: - The name(tag name with alias) of tag. - """ - if self.alias: - return ( - self.alias - if self.is_default and self.tag != "*" - else (self.tag + " as " + self.alias if self.tag else self.alias) - ) - return self.tag or "" - - -ImportTypes = str | ImportVar | list[str | ImportVar] | list[ImportVar] -ImmutableImportTypes = str | ImportVar | Sequence[str | ImportVar] -ImportDict = dict[str, ImportTypes] -ImmutableImportDict = Mapping[str, ImmutableImportTypes] -ParsedImportDict = dict[str, list[ImportVar]] -ImmutableParsedImportDict = Mapping[str, Sequence[ImportVar]] -ParsedImportTuple = tuple[tuple[str, tuple[ImportVar, ...]], ...] +from reflex_core.utils.imports import * diff --git a/reflex/utils/js_runtimes.py b/reflex/utils/js_runtimes.py index 76eb9b12654..b031ac7d681 100644 --- a/reflex/utils/js_runtimes.py +++ b/reflex/utils/js_runtimes.py @@ -7,13 +7,13 @@ from pathlib import Path from packaging import version +from reflex_core import constants +from reflex_core.config import Config, get_config +from reflex_core.environment import environment +from reflex_core.utils.decorator import cached_procedure, once +from reflex_core.utils.exceptions import SystemPackageMissingError -from reflex import constants -from reflex.config import Config, get_config -from reflex.environment import environment from reflex.utils import console, net, path_ops, processes -from reflex.utils.decorator import cached_procedure, once -from reflex.utils.exceptions import SystemPackageMissingError from reflex.utils.prerequisites import get_web_dir, windows_check_onedrive_in_path diff --git a/reflex/utils/lazy_loader.py b/reflex/utils/lazy_loader.py index 3cc2f83061e..faf48f398b0 100644 --- a/reflex/utils/lazy_loader.py +++ b/reflex/utils/lazy_loader.py @@ -1,97 +1,3 @@ -"""Module to implement lazy loading in reflex. +"""Re-export from reflex_core.utils.lazy_loader.""" -BSD 3-Clause License - -Copyright (c) 2022--2023, Scientific Python project All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - - Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" - -from __future__ import annotations - -import copy -import importlib -import os -import sys - - -def attach( - package_name: str, - submodules: set[str] | None = None, - submod_attrs: dict[str, list[str]] | None = None, - **extra_mappings, -): - """Replaces a package's __getattr__, __dir__, and __all__ attributes using lazy.attach. - The lazy loader __getattr__ doesn't support tuples as list values. We needed to add - this functionality (tuples) in Reflex to support 'import as _' statements. This function - reformats the submod_attrs dictionary to flatten the module list before passing it to - lazy_loader. - - Args: - package_name: name of the package. - submodules : List of submodules to attach. - submod_attrs : Dictionary of submodule -> list of attributes / functions. - These attributes are imported as they are used. - extra_mappings: Additional mappings to resolve lazily. - - Returns: - __getattr__, __dir__, __all__ - """ - submod_attrs = copy.deepcopy(submod_attrs) - if submod_attrs: - for k, v in submod_attrs.items(): - # when flattening the list, only keep the alias in the tuple(mod[1]) - submod_attrs[k] = [ - mod if not isinstance(mod, tuple) else mod[1] for mod in v - ] - - if submod_attrs is None: - submod_attrs = {} - - submodules = set(submodules) if submodules is not None else set() - - attr_to_modules = { - attr: mod for mod, attrs in submod_attrs.items() for attr in attrs - } - - __all__ = sorted([*(submodules | attr_to_modules.keys()), *(extra_mappings or [])]) - - def __getattr__(name: str): # noqa: N807 - if name in extra_mappings: - submod_path, attr = extra_mappings[name].rsplit(".", 1) - submod = importlib.import_module(submod_path) - return getattr(submod, attr) - if name in submodules: - return importlib.import_module(f"{package_name}.{name}") - if name in attr_to_modules: - submod_path = f"{package_name}.{attr_to_modules[name]}" - submod = importlib.import_module(submod_path) - attr = getattr(submod, name) - - # If the attribute lives in a file (module) with the same - # name as the attribute, ensure that the attribute and *not* - # the module is accessible on the package. - if name == attr_to_modules[name]: - pkg = sys.modules[package_name] - pkg.__dict__[name] = attr - - return attr - msg = f"No {package_name} attribute {name}" - raise AttributeError(msg) - - def __dir__(): # noqa: N807 - return __all__ - - if os.environ.get("EAGER_IMPORT", ""): - for attr in set(attr_to_modules.keys()) | submodules: - __getattr__(attr) - - return __getattr__, __dir__, list(__all__) +from reflex_core.utils.lazy_loader import * diff --git a/reflex/utils/misc.py b/reflex/utils/misc.py index 0eb4d245771..67ae79a13ab 100644 --- a/reflex/utils/misc.py +++ b/reflex/utils/misc.py @@ -103,7 +103,7 @@ def preload_color_theme(): Returns: Script: A script component to add to App.head_components """ - from reflex.components.el.elements.scripts import Script + from reflex_components_core.el.elements.scripts import Script # Create direct inline script content (like next-themes dangerouslySetInnerHTML) script_content = """ diff --git a/reflex/utils/net.py b/reflex/utils/net.py index 38642d4da0a..17f7bd5de22 100644 --- a/reflex/utils/net.py +++ b/reflex/utils/net.py @@ -5,8 +5,8 @@ from collections.abc import Callable from typing import ParamSpec, TypeVar -from reflex.utils.decorator import once -from reflex.utils.types import Unset +from reflex_core.utils.decorator import once +from reflex_core.utils.types import Unset from . import console @@ -17,7 +17,7 @@ def _httpx_verify_kwarg() -> bool: Returns: True if SSL verification is enabled, False otherwise """ - from reflex.environment import environment + from reflex_core.environment import environment return not environment.SSL_NO_VERIFY.get() @@ -135,7 +135,7 @@ def _httpx_local_address_kwarg() -> str: Returns: The local address to bind to """ - from reflex.environment import environment + from reflex_core.environment import environment return environment.REFLEX_HTTP_CLIENT_BIND_ADDRESS.get() or ( "::" if _should_use_ipv6() else "0.0.0.0" diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index 152fac596fa..f076da1b9a7 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -9,8 +9,8 @@ import stat from pathlib import Path -from reflex.config import get_config -from reflex.environment import environment +from reflex_core.config import get_config +from reflex_core.environment import environment # Shorthand for join. join = os.linesep.join diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 6e9353d9633..c91c62c8f91 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -18,12 +18,13 @@ from typing import NamedTuple from packaging import version +from reflex_core import constants +from reflex_core.config import Config, get_config +from reflex_core.environment import environment +from reflex_core.utils.decorator import once -from reflex import constants, model -from reflex.config import Config, get_config -from reflex.environment import environment +from reflex import model from reflex.utils import console, net, path_ops -from reflex.utils.decorator import once from reflex.utils.misc import get_module_path if typing.TYPE_CHECKING: diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index a8f893fa661..a89d53869b0 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -16,11 +16,11 @@ from typing import Any, Literal, overload import rich.markup +from reflex_core import constants +from reflex_core.config import get_config +from reflex_core.environment import environment from rich.progress import Progress -from reflex import constants -from reflex.config import get_config -from reflex.environment import environment from reflex.utils import console, path_ops, prerequisites from reflex.utils.registry import get_npm_registry diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index 93a7bae6b18..9a58d31e5ca 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -1,1570 +1,3 @@ -"""The pyi generator module.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import ast -import contextlib -import importlib -import inspect -import json -import logging -import multiprocessing -import re -import subprocess -import sys -import typing -from collections.abc import Callable, Iterable, Mapping, Sequence -from concurrent.futures import ProcessPoolExecutor -from functools import cache -from hashlib import md5 -from inspect import getfullargspec -from itertools import chain -from pathlib import Path -from types import MappingProxyType, ModuleType, SimpleNamespace, UnionType -from typing import Any, get_args, get_origin - -from reflex.components.component import Component -from reflex.utils import types as rx_types -from reflex.vars.base import Var - -logger = logging.getLogger("pyi_generator") - -PWD = Path.cwd() - -PYI_HASHES = "pyi_hashes.json" - -EXCLUDED_FILES = [ - "app.py", - "component.py", - "bare.py", - "foreach.py", - "cond.py", - "match.py", - "multiselect.py", - "literals.py", -] - -# These props exist on the base component, but should not be exposed in create methods. -EXCLUDED_PROPS = [ - "alias", - "children", - "event_triggers", - "library", - "lib_dependencies", - "tag", - "is_default", - "special_props", - "_is_tag_in_global_scope", - "_invalid_children", - "_memoization_mode", - "_rename_props", - "_valid_children", - "_valid_parents", - "State", -] - -OVERWRITE_TYPES = { - "style": "Sequence[Mapping[str, Any]] | Mapping[str, Any] | Var[Mapping[str, Any]] | Breakpoints | None", -} - -DEFAULT_TYPING_IMPORTS = { - "Any", - "Callable", - "Dict", - # "List", - "Sequence", - "Mapping", - "Literal", - "Optional", - "Union", - "Annotated", -} - -# TODO: fix import ordering and unused imports with ruff later -DEFAULT_IMPORTS = { - "typing": sorted(DEFAULT_TYPING_IMPORTS), - "reflex.components.core.breakpoints": ["Breakpoints"], - "reflex.event": [ - "EventChain", - "EventHandler", - "EventSpec", - "EventType", - "KeyInputInfo", - "PointerEventInfo", - ], - "reflex.style": ["Style"], - "reflex.vars.base": ["Var"], -} - - -def _walk_files(path: str | Path): - """Walk all files in a path. - This can be replaced with Path.walk() in python3.12. - - Args: - path: The path to walk. - - Yields: - The next file in the path. - """ - for p in Path(path).iterdir(): - if p.is_dir(): - yield from _walk_files(p) - continue - yield p.resolve() - - -def _relative_to_pwd(path: Path) -> Path: - """Get the relative path of a path to the current working directory. - - Args: - path: The path to get the relative path for. - - Returns: - The relative path. - """ - if path.is_absolute(): - return path.relative_to(PWD) - return path - - -def _get_type_hint( - value: Any, type_hint_globals: dict, is_optional: bool = True -) -> str: - """Resolve the type hint for value. - - Args: - value: The type annotation as a str or actual types/aliases. - type_hint_globals: The globals to use to resolving a type hint str. - is_optional: Whether the type hint should be wrapped in Optional. - - Returns: - The resolved type hint as a str. - - Raises: - TypeError: If the value name is not visible in the type hint globals. - """ - res = "" - args = get_args(value) - - if value is type(None) or value is None: - return "None" - - if rx_types.is_union(value): - if type(None) in value.__args__: - res_args = [ - _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg)) - for arg in value.__args__ - if arg is not type(None) - ] - res_args.sort() - if len(res_args) == 1: - return f"{res_args[0]} | None" - res = f"{' | '.join(res_args)}" - return f"{res} | None" - - res_args = [ - _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg)) - for arg in value.__args__ - ] - res_args.sort() - return f"{' | '.join(res_args)}" - - if args: - inner_container_type_args = ( - sorted(repr(arg) for arg in args) - if rx_types.is_literal(value) - else [ - _get_type_hint(arg, type_hint_globals, is_optional=False) - for arg in args - if arg is not type(None) - ] - ) - - if ( - value.__module__ not in ["builtins", "__builtins__"] - and value.__name__ not in type_hint_globals - ): - msg = ( - f"{value.__module__ + '.' + value.__name__} is not a default import, " - "add it to DEFAULT_IMPORTS in pyi_generator.py" - ) - raise TypeError(msg) - - res = f"{value.__name__}[{', '.join(inner_container_type_args)}]" - - if value.__name__ == "Var": - args = list( - chain.from_iterable([ - get_args(arg) if rx_types.is_union(arg) else [arg] for arg in args - ]) - ) - - # For Var types, Union with the inner args so they can be passed directly. - types = [res] + [ - _get_type_hint(arg, type_hint_globals, is_optional=False) - for arg in args - if arg is not type(None) - ] - if len(types) > 1: - res = " | ".join(sorted(types)) - - elif isinstance(value, str): - ev = eval(value, type_hint_globals) - if rx_types.is_optional(ev): - return _get_type_hint(ev, type_hint_globals, is_optional=False) - - if rx_types.is_union(ev): - res = [ - _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg)) - for arg in ev.__args__ - ] - return f"{' | '.join(res)}" - res = ( - _get_type_hint(ev, type_hint_globals, is_optional=False) - if ev.__name__ == "Var" - else value - ) - elif isinstance(value, list): - res = [ - _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg)) - for arg in value - ] - return f"[{', '.join(res)}]" - else: - res = value.__name__ - if is_optional and not res.startswith("Optional") and not res.endswith("| None"): - res = f"{res} | None" - return res - - -@cache -def _get_source(obj: Any) -> str: - """Get and cache the source for a Python object. - - Args: - obj: The object whose source should be retrieved. - - Returns: - The source code for the object. - """ - return inspect.getsource(obj) - - -@cache -def _get_class_prop_comments(clz: type[Component]) -> Mapping[str, tuple[str, ...]]: - """Parse and cache prop comments for a component class. - - Args: - clz: The class to extract prop comments from. - - Returns: - An immutable mapping of prop name to comment lines. - """ - props_comments: dict[str, tuple[str, ...]] = {} - comments = [] - for line in _get_source(clz).splitlines(): - reached_functions = re.search(r"def ", line) - if reached_functions: - # We've reached the functions, so stop. - break - - if line == "": - # We hit a blank line, so clear comments to avoid commented out prop appearing in next prop docs. - comments.clear() - continue - - # Get comments for prop - if line.strip().startswith("#"): - # Remove noqa from the comments. - line = line.partition(" # noqa")[0] - comments.append(line) - continue - - # Check if this line has a prop. - match = re.search(r"\w+:", line) - if match is None: - # This line doesn't have a var, so continue. - continue - - # Get the prop. - prop = match.group(0).strip(":") - if comments: - props_comments[prop] = tuple( - comment.strip().strip("#") for comment in comments - ) - comments.clear() - - return MappingProxyType(props_comments) - - -@cache -def _get_full_argspec(func: Callable) -> inspect.FullArgSpec: - """Get and cache the full argspec for a callable. - - Args: - func: The callable to inspect. - - Returns: - The full argument specification. - """ - return getfullargspec(func) - - -@cache -def _get_signature_return_annotation(func: Callable) -> Any: - """Get and cache a callable's return annotation. - - Args: - func: The callable to inspect. - - Returns: - The callable's return annotation. - """ - return inspect.signature(func).return_annotation - - -@cache -def _get_module_star_imports(module_name: str) -> Mapping[str, Any]: - """Resolve names imported by `from module import *`. - - Args: - module_name: The module to inspect. - - Returns: - An immutable mapping of imported names to values. - """ - module = importlib.import_module(module_name) - exported_names = getattr(module, "__all__", None) - if exported_names is not None: - return MappingProxyType({ - name: getattr(module, name) for name in exported_names - }) - return MappingProxyType({ - name: value for name, value in vars(module).items() if not name.startswith("_") - }) - - -@cache -def _get_module_selected_imports( - module_name: str, imported_names: tuple[str, ...] -) -> Mapping[str, Any]: - """Resolve a set of imported names from a module. - - Args: - module_name: The module to import from. - imported_names: The names to resolve. - - Returns: - An immutable mapping of imported names to values. - """ - module = importlib.import_module(module_name) - return MappingProxyType({name: getattr(module, name) for name in imported_names}) - - -@cache -def _get_class_annotation_globals(target_class: type) -> Mapping[str, Any]: - """Get globals needed to resolve class annotations. - - Args: - target_class: The class whose annotation globals should be resolved. - - Returns: - An immutable mapping of globals for the class MRO. - """ - available_vars: dict[str, Any] = {} - for module_name in {cls.__module__ for cls in target_class.__mro__}: - available_vars.update(sys.modules[module_name].__dict__) - return MappingProxyType(available_vars) - - -@cache -def _get_class_event_triggers(target_class: type) -> frozenset[str]: - """Get and cache event trigger names for a class. - - Args: - target_class: The class to inspect. - - Returns: - The event trigger names defined on the class. - """ - return frozenset(target_class.get_event_triggers()) - - -def _generate_imports( - typing_imports: Iterable[str], -) -> list[ast.ImportFrom | ast.Import]: - """Generate the import statements for the stub file. - - Args: - typing_imports: The typing imports to include. - - Returns: - The list of import statements. - """ - return [ - *[ - ast.ImportFrom(module=name, names=[ast.alias(name=val) for val in values]) # pyright: ignore [reportCallIssue] - for name, values in DEFAULT_IMPORTS.items() - ], - ast.Import([ast.alias("reflex")]), - ] - - -def _generate_docstrings(clzs: list[type[Component]], props: list[str]) -> str: - """Generate the docstrings for the create method. - - Args: - clzs: The classes to generate docstrings for. - props: The props to generate docstrings for. - - Returns: - The docstring for the create method. - """ - props_comments = {} - for clz in clzs: - for prop, comment_lines in _get_class_prop_comments(clz).items(): - if prop in props: - props_comments[prop] = list(comment_lines) - clz = clzs[0] - new_docstring = [] - for line in (clz.create.__doc__ or "").splitlines(): - if "**" in line: - indent = line.split("**")[0] - new_docstring.extend([ - f"{indent}{n}:{' '.join(c)}" for n, c in props_comments.items() - ]) - new_docstring.append(line) - return "\n".join(new_docstring) - - -def _extract_func_kwargs_as_ast_nodes( - func: Callable, - type_hint_globals: dict[str, Any], -) -> list[tuple[ast.arg, ast.Constant | None]]: - """Get the kwargs already defined on the function. - - Args: - func: The function to extract kwargs from. - type_hint_globals: The globals to use to resolving a type hint str. - - Returns: - The list of kwargs as ast arg nodes. - """ - spec = _get_full_argspec(func) - kwargs = [] - - for kwarg in spec.kwonlyargs: - arg = ast.arg(arg=kwarg) - if kwarg in spec.annotations: - arg.annotation = ast.Name( - id=_get_type_hint(spec.annotations[kwarg], type_hint_globals) - ) - default = None - if spec.kwonlydefaults is not None and kwarg in spec.kwonlydefaults: - default = ast.Constant(value=spec.kwonlydefaults[kwarg]) - kwargs.append((arg, default)) - return kwargs - - -def _extract_class_props_as_ast_nodes( - func: Callable, - clzs: list[type], - type_hint_globals: dict[str, Any], - extract_real_default: bool = False, -) -> list[tuple[ast.arg, ast.Constant | None]]: - """Get the props defined on the class and all parents. - - Args: - func: The function that kwargs will be added to. - clzs: The classes to extract props from. - type_hint_globals: The globals to use to resolving a type hint str. - extract_real_default: Whether to extract the real default value from the - pydantic field definition. - - Returns: - The list of props as ast arg nodes - """ - spec = _get_full_argspec(func) - func_kwonlyargs = set(spec.kwonlyargs) - all_props: set[str] = set() - kwargs = [] - for target_class in clzs: - event_triggers = _get_class_event_triggers(target_class) - # Import from the target class to ensure type hints are resolvable. - type_hint_globals.update(_get_module_star_imports(target_class.__module__)) - annotation_globals = { - **type_hint_globals, - **_get_class_annotation_globals(target_class), - } - for name, value in target_class.__annotations__.items(): - if ( - name in func_kwonlyargs - or name in EXCLUDED_PROPS - or name in all_props - or name in event_triggers - or (isinstance(value, str) and "ClassVar" in value) - ): - continue - all_props.add(name) - - default = None - if extract_real_default: - # TODO: This is not currently working since the default is not type compatible - # with the annotation in some cases. - with contextlib.suppress(AttributeError, KeyError): - # Try to get default from pydantic field definition. - default = target_class.__fields__[name].default - if isinstance(default, Var): - default = default._decode() - - kwargs.append(( - ast.arg( - arg=name, - annotation=ast.Name( - id=OVERWRITE_TYPES.get( - name, - _get_type_hint( - value, - annotation_globals, - ), - ) - ), - ), - ast.Constant(value=default), # pyright: ignore [reportArgumentType] - )) - return kwargs - - -def _get_visible_type_name( - typ: Any, type_hint_globals: Mapping[str, Any] | None -) -> str | None: - """Get a visible identifier for a type in the current module. - - Args: - typ: The type annotation to resolve. - type_hint_globals: The globals visible in the current module. - - Returns: - The visible identifier if one exists, otherwise None. - """ - if type_hint_globals is None: - return None - - type_name = getattr(typ, "__name__", None) - if ( - type_name is not None - and type_name in type_hint_globals - and type_hint_globals[type_name] is typ - ): - return type_name - - for name, value in type_hint_globals.items(): - if name.isidentifier() and value is typ: - return name - - return None - - -def type_to_ast( - typ: Any, - cls: type, - type_hint_globals: Mapping[str, Any] | None = None, -) -> ast.expr: - """Converts any type annotation into its AST representation. - Handles nested generic types, unions, etc. - - Args: - typ: The type annotation to convert. - cls: The class where the type annotation is used. - type_hint_globals: The globals visible where the annotation is used. - - Returns: - The AST representation of the type annotation. - """ - if typ is type(None) or typ is None: - return ast.Name(id="None") - - origin = get_origin(typ) - if origin is typing.Literal: - return ast.Subscript( - value=ast.Name(id="Literal"), - slice=ast.Tuple( - elts=[ast.Constant(value=val) for val in get_args(typ)], ctx=ast.Load() - ), - ctx=ast.Load(), - ) - if origin is UnionType: - origin = typing.Union - - # Handle plain types (int, str, custom classes, etc.) - if origin is None: - if hasattr(typ, "__name__"): - if typ.__module__.startswith("reflex."): - typ_parts = typ.__module__.split(".") - cls_parts = cls.__module__.split(".") - - zipped = list(zip(typ_parts, cls_parts, strict=False)) - - if all(a == b for a, b in zipped) and len(typ_parts) == len(cls_parts): - return ast.Name(id=typ.__name__) - if visible_name := _get_visible_type_name(typ, type_hint_globals): - return ast.Name(id=visible_name) - if ( - typ.__module__ in DEFAULT_IMPORTS - and typ.__name__ in DEFAULT_IMPORTS[typ.__module__] - ): - return ast.Name(id=typ.__name__) - return ast.Name(id=typ.__module__ + "." + typ.__name__) - return ast.Name(id=typ.__name__) - if hasattr(typ, "_name"): - return ast.Name(id=typ._name) - return ast.Name(id=str(typ)) - - # Get the base type name (List, Dict, Optional, etc.) - base_name = getattr(origin, "_name", origin.__name__) - - # Get type arguments - args = get_args(typ) - - # Handle empty type arguments - if not args: - return ast.Name(id=base_name) - - # Convert all type arguments recursively - arg_nodes = [type_to_ast(arg, cls, type_hint_globals) for arg in args] - - # Special case for single-argument types (like list[T] or Optional[T]) - if len(arg_nodes) == 1: - slice_value = arg_nodes[0] - else: - slice_value = ast.Tuple(elts=arg_nodes, ctx=ast.Load()) - - return ast.Subscript( - value=ast.Name(id=base_name), - slice=slice_value, - ctx=ast.Load(), - ) - - -@cache -def _get_parent_imports(func: Callable) -> Mapping[str, tuple[str, ...]]: - """Get parent imports needed to resolve forwarded type hints. - - Args: - func: The callable whose annotations are being analyzed. - - Returns: - An immutable mapping of module names to imported symbol names. - """ - imports_: dict[str, set[str]] = {"reflex.vars": {"Var"}} - module_dir = set(dir(importlib.import_module(func.__module__))) - for type_hint in inspect.get_annotations(func).values(): - try: - match = re.match(r"\w+\[([\w\d]+)\]", type_hint) - except TypeError: - continue - if match: - type_hint = match.group(1) - if type_hint in module_dir: - imports_.setdefault(func.__module__, set()).add(type_hint) - return MappingProxyType({ - module_name: tuple(sorted(imported_names)) - for module_name, imported_names in imports_.items() - }) - - -def _generate_component_create_functiondef( - clz: type[Component], - type_hint_globals: dict[str, Any], - lineno: int, - decorator_list: Sequence[ast.expr] = (ast.Name(id="classmethod"),), -) -> ast.FunctionDef: - """Generate the create function definition for a Component. - - Args: - clz: The Component class to generate the create functiondef for. - type_hint_globals: The globals to use to resolving a type hint str. - lineno: The line number to use for the ast nodes. - decorator_list: The list of decorators to apply to the create functiondef. - - Returns: - The create functiondef node for the ast. - - Raises: - TypeError: If clz is not a subclass of Component. - """ - if not issubclass(clz, Component): - msg = f"clz must be a subclass of Component, not {clz!r}" - raise TypeError(msg) - - # add the imports needed by get_type_hint later - type_hint_globals.update({ - name: getattr(typing, name) for name in DEFAULT_TYPING_IMPORTS - }) - - if clz.__module__ != clz.create.__module__: - imports_ = _get_parent_imports(clz.create) - for name, values in imports_.items(): - type_hint_globals.update(_get_module_selected_imports(name, values)) - - kwargs = _extract_func_kwargs_as_ast_nodes(clz.create, type_hint_globals) - - # kwargs associated with props defined in the class and its parents - all_classes = [c for c in clz.__mro__ if issubclass(c, Component)] - prop_kwargs = _extract_class_props_as_ast_nodes( - clz.create, all_classes, type_hint_globals - ) - all_props = [arg[0].arg for arg in prop_kwargs] - kwargs.extend(prop_kwargs) - - def figure_out_return_type(annotation: Any): - if isinstance(annotation, type) and issubclass(annotation, inspect._empty): - return ast.Name(id="EventType[Any]") - - if not isinstance(annotation, str) and get_origin(annotation) is tuple: - arguments = get_args(annotation) - - arguments_without_var = [ - get_args(argument)[0] if get_origin(argument) == Var else argument - for argument in arguments - ] - - # Convert each argument type to its AST representation - type_args = [ - type_to_ast(arg, cls=clz, type_hint_globals=type_hint_globals) - for arg in arguments_without_var - ] - - # Get all prefixes of the type arguments - all_count_args_type = [ - ast.Name( - f"EventType[{', '.join([ast.unparse(arg) for arg in type_args[:i]])}]" - ) - if i > 0 - else ast.Name("EventType[()]") - for i in range(len(type_args) + 1) - ] - - # Create EventType using the joined string - return ast.Name(id=f"{' | '.join(map(ast.unparse, all_count_args_type))}") - - if isinstance(annotation, str) and annotation.lower().startswith("tuple["): - inside_of_tuple = ( - annotation - .removeprefix("tuple[") - .removeprefix("Tuple[") - .removesuffix("]") - ) - - if inside_of_tuple == "()": - return ast.Name(id="EventType[()]") - - arguments = [""] - - bracket_count = 0 - - for char in inside_of_tuple: - if char == "[": - bracket_count += 1 - elif char == "]": - bracket_count -= 1 - - if char == "," and bracket_count == 0: - arguments.append("") - else: - arguments[-1] += char - - arguments = [argument.strip() for argument in arguments] - - arguments_without_var = [ - argument.removeprefix("Var[").removesuffix("]") - if argument.startswith("Var[") - else argument - for argument in arguments - ] - - all_count_args_type = [ - ast.Name(f"EventType[{', '.join(arguments_without_var[:i])}]") - if i > 0 - else ast.Name("EventType[()]") - for i in range(len(arguments) + 1) - ] - - return ast.Name(id=f"{' | '.join(map(ast.unparse, all_count_args_type))}") - return ast.Name(id="EventType[Any]") - - event_triggers = clz.get_event_triggers() - - # event handler kwargs - kwargs.extend( - ( - ast.arg( - arg=trigger, - annotation=ast.Subscript( - ast.Name("Optional"), - ast.Name( - id=ast.unparse( - figure_out_return_type( - _get_signature_return_annotation(event_specs) - ) - if not isinstance( - event_specs := event_triggers[trigger], Sequence - ) - else ast.Subscript( - ast.Name("Union"), - ast.Tuple([ - figure_out_return_type( - _get_signature_return_annotation(event_spec) - ) - for event_spec in event_specs - ]), - ) - ) - ), - ), - ), - ast.Constant(value=None), - ) - for trigger in sorted(event_triggers) - ) - - logger.debug(f"Generated {clz.__name__}.create method with {len(kwargs)} kwargs") - create_args = ast.arguments( - args=[ast.arg(arg="cls")], - posonlyargs=[], - vararg=ast.arg(arg="children"), - kwonlyargs=[arg[0] for arg in kwargs], - kw_defaults=[arg[1] for arg in kwargs], - kwarg=ast.arg(arg="props"), - defaults=[], - ) - - return ast.FunctionDef( # pyright: ignore [reportCallIssue] - name="create", - args=create_args, - body=[ - ast.Expr( - value=ast.Constant( - value=_generate_docstrings( - all_classes, [*all_props, *event_triggers] - ) - ), - ), - ast.Expr( - value=ast.Constant(value=Ellipsis), - ), - ], - decorator_list=list(decorator_list), - lineno=lineno, - returns=ast.Constant(value=clz.__name__), - ) - - -def _generate_staticmethod_call_functiondef( - node: ast.ClassDef, - clz: type[Component] | type[SimpleNamespace], - type_hint_globals: dict[str, Any], -) -> ast.FunctionDef | None: - fullspec = _get_full_argspec(clz.__call__) - - call_args = ast.arguments( - args=[ - ast.arg( - name, - annotation=ast.Name( - id=_get_type_hint( - anno := fullspec.annotations[name], - type_hint_globals, - is_optional=rx_types.is_optional(anno), - ) - ), - ) - for name in fullspec.args - ], - posonlyargs=[], - kwonlyargs=[], - kw_defaults=[], - kwarg=ast.arg(arg="props"), - defaults=( - [ast.Constant(value=default) for default in fullspec.defaults] - if fullspec.defaults - else [] - ), - ) - return ast.FunctionDef( # pyright: ignore [reportCallIssue] - name="__call__", - args=call_args, - body=[ - ast.Expr(value=ast.Constant(value=clz.__call__.__doc__)), - ast.Expr( - value=ast.Constant(...), - ), - ], - decorator_list=[ast.Name(id="staticmethod")], - lineno=node.lineno, - returns=ast.Constant( - value=_get_type_hint( - typing.get_type_hints(clz.__call__).get("return", None), - type_hint_globals, - is_optional=False, - ) - ), - ) - - -def _generate_namespace_call_functiondef( - node: ast.ClassDef, - clz_name: str, - classes: dict[str, type[Component] | type[SimpleNamespace]], - type_hint_globals: dict[str, Any], -) -> ast.FunctionDef | None: - """Generate the __call__ function definition for a SimpleNamespace. - - Args: - node: The existing __call__ classdef parent node from the ast - clz_name: The name of the SimpleNamespace class to generate the __call__ functiondef for. - classes: Map name to actual class definition. - type_hint_globals: The globals to use to resolving a type hint str. - - Returns: - The create functiondef node for the ast. - """ - # add the imports needed by get_type_hint later - type_hint_globals.update({ - name: getattr(typing, name) for name in DEFAULT_TYPING_IMPORTS - }) - - clz = classes[clz_name] - - if not hasattr(clz.__call__, "__self__"): - return _generate_staticmethod_call_functiondef(node, clz, type_hint_globals) - - # Determine which class is wrapped by the namespace __call__ method - component_clz = clz.__call__.__self__ - - if clz.__call__.__func__.__name__ != "create": # pyright: ignore [reportFunctionMemberAccess] - return None - - if not issubclass(component_clz, Component): - return None - - definition = _generate_component_create_functiondef( - clz=component_clz, - type_hint_globals=type_hint_globals, - lineno=node.lineno, - decorator_list=[], - ) - definition.name = "__call__" - - # Turn the definition into a staticmethod - del definition.args.args[0] # remove `cls` arg - definition.decorator_list = [ast.Name(id="staticmethod")] - - return definition - - -class StubGenerator(ast.NodeTransformer): - """A node transformer that will generate the stubs for a given module.""" - - def __init__( - self, - module: ModuleType, - classes: dict[str, type[Component | SimpleNamespace]], - ): - """Initialize the stub generator. - - Args: - module: The actual module object module to generate stubs for. - classes: The actual Component class objects to generate stubs for. - """ - super().__init__() - # Dict mapping class name to actual class object. - self.classes = classes - # Track the last class node that was visited. - self.current_class = None - # These imports will be included in the AST of stub files. - self.typing_imports = DEFAULT_TYPING_IMPORTS.copy() - # Whether those typing imports have been inserted yet. - self.inserted_imports = False - # This dict is used when evaluating type hints. - self.type_hint_globals = module.__dict__.copy() - - @staticmethod - def _remove_docstring( - node: ast.Module | ast.ClassDef | ast.FunctionDef, - ) -> ast.Module | ast.ClassDef | ast.FunctionDef: - """Removes any docstring in place. - - Args: - node: The node to remove the docstring from. - - Returns: - The modified node. - """ - if ( - node.body - and isinstance(node.body[0], ast.Expr) - and isinstance(node.body[0].value, ast.Constant) - ): - node.body.pop(0) - return node - - def _current_class_is_component(self) -> type[Component] | None: - """Check if the current class is a Component. - - Returns: - Whether the current class is a Component. - """ - if ( - self.current_class is not None - and self.current_class in self.classes - and issubclass((clz := self.classes[self.current_class]), Component) - ): - return clz - return None - - def visit_Module(self, node: ast.Module) -> ast.Module: - """Visit a Module node and remove docstring from body. - - Args: - node: The Module node to visit. - - Returns: - The modified Module node. - """ - self.generic_visit(node) - return self._remove_docstring(node) # pyright: ignore [reportReturnType] - - def visit_Import( - self, node: ast.Import | ast.ImportFrom - ) -> ast.Import | ast.ImportFrom | list[ast.Import | ast.ImportFrom]: - """Collect import statements from the module. - - If this is the first import statement, insert the typing imports before it. - - Args: - node: The import node to visit. - - Returns: - The modified import node(s). - """ - if not self.inserted_imports: - self.inserted_imports = True - default_imports = _generate_imports(self.typing_imports) - return [*default_imports, node] - return node - - def visit_ImportFrom( - self, node: ast.ImportFrom - ) -> ast.Import | ast.ImportFrom | list[ast.Import | ast.ImportFrom] | None: - """Visit an ImportFrom node. - - Remove any `from __future__ import *` statements, and hand off to visit_Import. - - Args: - node: The ImportFrom node to visit. - - Returns: - The modified ImportFrom node. - """ - if node.module == "__future__": - return None # ignore __future__ imports: https://docs.astral.sh/ruff/rules/future-annotations-in-stub/ - return self.visit_Import(node) - - def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: - """Visit a ClassDef node. - - Remove all assignments in the class body, and add a create functiondef - if one does not exist. - - Args: - node: The ClassDef node to visit. - - Returns: - The modified ClassDef node. - """ - self.current_class = node.name - self._remove_docstring(node) - - # Define `__call__` as a real function so the docstring appears in the stub. - call_definition = None - for child in node.body[:]: - found_call = False - if ( - isinstance(child, ast.AnnAssign) - and isinstance(child.target, ast.Name) - and child.target.id.startswith("_") - ): - node.body.remove(child) - if isinstance(child, ast.Assign): - for target in child.targets[:]: - if isinstance(target, ast.Name) and target.id == "__call__": - child.targets.remove(target) - found_call = True - if not found_call: - continue - if not child.targets[:]: - node.body.remove(child) - call_definition = _generate_namespace_call_functiondef( - node, - self.current_class, - self.classes, - type_hint_globals=self.type_hint_globals, - ) - break - - self.generic_visit(node) # Visit child nodes. - - if ( - not any( - isinstance(child, ast.FunctionDef) and child.name == "create" - for child in node.body - ) - and (clz := self._current_class_is_component()) is not None - ): - # Add a new .create FunctionDef since one does not exist. - node.body.append( - _generate_component_create_functiondef( - clz=clz, - type_hint_globals=self.type_hint_globals, - lineno=node.lineno, - ) - ) - if call_definition is not None: - node.body.append(call_definition) - if not node.body: - # We should never return an empty body. - node.body.append(ast.Expr(value=ast.Constant(value=Ellipsis))) - self.current_class = None - return node - - def visit_FunctionDef(self, node: ast.FunctionDef) -> Any: - """Visit a FunctionDef node. - - Special handling for `.create` functions to add type hints for all props - defined on the component class. - - Remove all private functions and blank out the function body of the - remaining public functions. - - Args: - node: The FunctionDef node to visit. - - Returns: - The modified FunctionDef node (or None). - """ - if ( - node.name == "create" - and self.current_class in self.classes - and issubclass((clz := self.classes[self.current_class]), Component) - ): - node = _generate_component_create_functiondef( - clz=clz, - type_hint_globals=self.type_hint_globals, - lineno=node.lineno, - decorator_list=node.decorator_list, - ) - else: - if node.name.startswith("_") and node.name != "__call__": - return None # remove private methods - - if node.body[-1] != ast.Expr(value=ast.Constant(value=Ellipsis)): - # Blank out the function body for public functions. - node.body = [ast.Expr(value=ast.Constant(value=Ellipsis))] - return node - - def visit_Assign(self, node: ast.Assign) -> ast.Assign | None: - """Remove non-annotated assignment statements. - - Args: - node: The Assign node to visit. - - Returns: - The modified Assign node (or None). - """ - # Special case for assignments to `typing.Any` as fallback. - if ( - node.value is not None - and isinstance(node.value, ast.Name) - and node.value.id == "Any" - ): - return node - - if self._current_class_is_component(): - # Remove annotated assignments in Component classes (props) - return None - - # remove dunder method assignments for lazy_loader.attach - for target in node.targets: - if isinstance(target, ast.Tuple): - for name in target.elts: - if isinstance(name, ast.Name) and name.id.startswith("_"): - return None - - return node - - def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AnnAssign | None: - """Visit an AnnAssign node (Annotated assignment). - - Remove private target and remove the assignment value in the stub. - - Args: - node: The AnnAssign node to visit. - - Returns: - The modified AnnAssign node (or None). - """ - # skip ClassVars - if ( - isinstance(node.annotation, ast.Subscript) - and isinstance(node.annotation.value, ast.Name) - and node.annotation.value.id == "ClassVar" - ): - return node - if isinstance(node.target, ast.Name) and node.target.id.startswith("_"): - return None - if self._current_class_is_component(): - # Remove annotated assignments in Component classes (props) - return None - # Blank out assignments in type stubs. - node.value = None - return node - - -class InitStubGenerator(StubGenerator): - """A node transformer that will generate the stubs for a given init file.""" - - def visit_Import( - self, node: ast.Import | ast.ImportFrom - ) -> ast.Import | ast.ImportFrom | list[ast.Import | ast.ImportFrom]: - """Collect import statements from the init module. - - Args: - node: The import node to visit. - - Returns: - The modified import node(s). - """ - return [node] - - -def _path_to_module_name(path: Path) -> str: - """Convert a file path to a dotted module name. - - Args: - path: The file path to convert. - - Returns: - The dotted module name. - """ - return _relative_to_pwd(path).with_suffix("").as_posix().replace("/", ".") - - -def _write_pyi_file(module_path: Path, source: str) -> str: - relpath = str(_relative_to_pwd(module_path)).replace("\\", "/") - pyi_content = ( - "\n".join([ - f'"""Stub file for {relpath}"""', - "# ------------------- DO NOT EDIT ----------------------", - "# This file was generated by `reflex/utils/pyi_generator.py`!", - "# ------------------------------------------------------", - "", - ]) - + source - ) - - pyi_path = module_path.with_suffix(".pyi") - pyi_path.write_text(pyi_content) - logger.info(f"Wrote {relpath}") - return md5(pyi_content.encode()).hexdigest() - - -def _get_init_lazy_imports(mod: tuple | ModuleType, new_tree: ast.AST): - # retrieve the _SUBMODULES and _SUBMOD_ATTRS from an init file if present. - sub_mods: set[str] | None = getattr(mod, "_SUBMODULES", None) - sub_mod_attrs: dict[str, list[str | tuple[str, str]]] | None = getattr( - mod, "_SUBMOD_ATTRS", None - ) - extra_mappings: dict[str, str] | None = getattr(mod, "_EXTRA_MAPPINGS", None) - - if not sub_mods and not sub_mod_attrs and not extra_mappings: - return None - sub_mods_imports = [] - sub_mod_attrs_imports = [] - extra_mappings_imports = [] - - if sub_mods: - sub_mods_imports = [f"from . import {mod}" for mod in sorted(sub_mods)] - sub_mods_imports.append("") - - if sub_mod_attrs: - flattened_sub_mod_attrs = { - imported: module - for module, attrs in sub_mod_attrs.items() - for imported in attrs - } - # construct the import statement and handle special cases for aliases - sub_mod_attrs_imports = [ - f"from .{module} import " - + ( - ( - (imported[0] + " as " + imported[1]) - if imported[0] != imported[1] - else imported[0] - ) - if isinstance(imported, tuple) - else imported - ) - for imported, module in flattened_sub_mod_attrs.items() - ] - sub_mod_attrs_imports.append("") - - if extra_mappings: - for alias, import_path in extra_mappings.items(): - module_name, import_name = import_path.rsplit(".", 1) - extra_mappings_imports.append( - f"from {module_name} import {import_name} as {alias}" - ) - - text = ( - "\n" - + "\n".join([ - *sub_mods_imports, - *sub_mod_attrs_imports, - *extra_mappings_imports, - ]) - + "\n" - ) - text += ast.unparse(new_tree) + "\n\n" - text += f"__all__ = {getattr(mod, '__all__', [])!r}\n" - return text - - -def _scan_file(module_path: Path) -> tuple[str, str] | None: - """Process a single Python file and generate its .pyi stub. - - Args: - module_path: Path to the Python source file. - - Returns: - Tuple of (pyi_path, content_hash) or None if no stub needed. - """ - module_import = _path_to_module_name(module_path) - module = importlib.import_module(module_import) - logger.debug(f"Read {module_path}") - class_names = { - name: obj - for name, obj in vars(module).items() - if isinstance(obj, type) - and ( - rx_types.safe_issubclass(obj, Component) - or rx_types.safe_issubclass(obj, SimpleNamespace) - ) - and obj != Component - and inspect.getmodule(obj) == module - } - is_init_file = _relative_to_pwd(module_path).name == "__init__.py" - if not class_names and not is_init_file: - return None - - if is_init_file: - new_tree = InitStubGenerator(module, class_names).visit( - ast.parse(_get_source(module)) - ) - init_imports = _get_init_lazy_imports(module, new_tree) - if not init_imports: - return None - content_hash = _write_pyi_file(module_path, init_imports) - else: - new_tree = StubGenerator(module, class_names).visit( - ast.parse(_get_source(module)) - ) - content_hash = _write_pyi_file(module_path, ast.unparse(new_tree)) - return str(module_path.with_suffix(".pyi").resolve()), content_hash - - -class PyiGenerator: - """A .pyi file generator that will scan all defined Component in Reflex and - generate the appropriate stub. - """ - - modules: list = [] - root: str = "" - current_module: Any = {} - written_files: list[tuple[str, str]] = [] - - def _scan_files(self, files: list[Path]): - max_workers = min(multiprocessing.cpu_count() or 1, len(files), 8) - use_parallel = ( - max_workers > 1 and "fork" in multiprocessing.get_all_start_methods() - ) - - if not use_parallel: - # Serial fallback: _scan_file handles its own imports. - for file in files: - result = _scan_file(file) - if result is not None: - self.written_files.append(result) - return - - # Pre-import all modules sequentially to populate sys.modules - # so forked workers inherit the cache and skip redundant imports. - importable_files: list[Path] = [] - for file in files: - module_import = _path_to_module_name(file) - try: - importlib.import_module(module_import) - importable_files.append(file) - except Exception: - logger.exception(f"Failed to import {module_import}") - - # Generate stubs in parallel using forked worker processes. - ctx = multiprocessing.get_context("fork") - with ProcessPoolExecutor(max_workers=max_workers, mp_context=ctx) as executor: - self.written_files.extend( - r for r in executor.map(_scan_file, importable_files) if r is not None - ) - - def scan_all( - self, - targets: list, - changed_files: list[Path] | None = None, - use_json: bool = False, - ): - """Scan all targets for class inheriting Component and generate the .pyi files. - - Args: - targets: the list of file/folders to scan. - changed_files (optional): the list of changed files since the last run. - use_json: whether to use json to store the hashes. - """ - file_targets = [] - for target in targets: - target_path = Path(target) - if ( - target_path.is_file() - and target_path.suffix == ".py" - and target_path.name not in EXCLUDED_FILES - ): - file_targets.append(target_path) - continue - if not target_path.is_dir(): - continue - for file_path in _walk_files(target_path): - relative = _relative_to_pwd(file_path) - if relative.name in EXCLUDED_FILES or file_path.suffix != ".py": - continue - if ( - changed_files is not None - and _relative_to_pwd(file_path) not in changed_files - ): - continue - file_targets.append(file_path) - - # check if pyi changed but not the source - if changed_files is not None: - for changed_file in changed_files: - if changed_file.suffix != ".pyi": - continue - py_file_path = changed_file.with_suffix(".py") - if not py_file_path.exists() and changed_file.exists(): - changed_file.unlink() - if py_file_path in file_targets: - continue - subprocess.run(["git", "checkout", changed_file]) - - self._scan_files(file_targets) - - file_paths, hashes = ( - [f[0] for f in self.written_files], - [f[1] for f in self.written_files], - ) - - # Fix generated pyi files with ruff. - if file_paths: - subprocess.run(["ruff", "check", "--fix", *file_paths]) - subprocess.run(["ruff", "format", *file_paths]) - - if use_json: - if file_paths and changed_files is None: - file_paths = list(map(Path, file_paths)) - top_dir = file_paths[0].parent - for file_path in file_paths: - file_parent = file_path.parent - while len(file_parent.parts) > len(top_dir.parts): - file_parent = file_parent.parent - while len(top_dir.parts) > len(file_parent.parts): - top_dir = top_dir.parent - while not file_parent.samefile(top_dir): - file_parent = file_parent.parent - top_dir = top_dir.parent - - while ( - not top_dir.samefile(top_dir.parent) - and not (top_dir / PYI_HASHES).exists() - ): - top_dir = top_dir.parent - - pyi_hashes_file = top_dir / PYI_HASHES - - if pyi_hashes_file.exists(): - pyi_hashes_file.write_text( - json.dumps( - dict( - zip( - [ - f.relative_to(pyi_hashes_file.parent).as_posix() - for f in file_paths - ], - hashes, - strict=True, - ) - ), - indent=2, - sort_keys=True, - ) - + "\n", - ) - elif file_paths: - file_paths = list(map(Path, file_paths)) - pyi_hashes_parent = file_paths[0].parent - while ( - not pyi_hashes_parent.samefile(pyi_hashes_parent.parent) - and not (pyi_hashes_parent / PYI_HASHES).exists() - ): - pyi_hashes_parent = pyi_hashes_parent.parent - - pyi_hashes_file = pyi_hashes_parent / PYI_HASHES - if pyi_hashes_file.exists(): - pyi_hashes = json.loads(pyi_hashes_file.read_text()) - for file_path, hashed_content in zip( - file_paths, hashes, strict=False - ): - formatted_path = file_path.relative_to( - pyi_hashes_parent - ).as_posix() - pyi_hashes[formatted_path] = hashed_content - - pyi_hashes_file.write_text( - json.dumps(pyi_hashes, indent=2, sort_keys=True) + "\n" - ) - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="Generate .pyi stub files") - parser.add_argument( - "targets", - nargs="*", - default=["reflex/components", "reflex/experimental", "reflex/__init__.py"], - help="Target directories/files to process", - ) - args = parser.parse_args() - - logging.basicConfig(level=logging.INFO) - logging.getLogger("blib2to3.pgen2.driver").setLevel(logging.INFO) - - gen = PyiGenerator() - gen.scan_all(args.targets, None, use_json=True) +from reflex_core.utils.pyi_generator import * diff --git a/reflex/utils/redir.py b/reflex/utils/redir.py index 0193ac1d223..a12b7efb078 100644 --- a/reflex/utils/redir.py +++ b/reflex/utils/redir.py @@ -14,7 +14,7 @@ def open_browser(target_url: "SplitResult") -> None: """ import webbrowser - from reflex.utils import console + from reflex_core.utils import console if not webbrowser.open(target_url.geturl()): console.warn( @@ -29,7 +29,7 @@ def reflex_build_redirect() -> None: """Open the browser window to reflex.build.""" from urllib.parse import urlsplit - from reflex import constants + from reflex_core import constants open_browser(urlsplit(constants.Templates.REFLEX_BUILD_FRONTEND_WITH_REFERRER)) @@ -38,6 +38,6 @@ def reflex_templates(): """Open the browser window to reflex.build/templates.""" from urllib.parse import urlsplit - from reflex import constants + from reflex_core import constants open_browser(urlsplit(constants.Templates.REFLEX_TEMPLATES_URL)) diff --git a/reflex/utils/registry.py b/reflex/utils/registry.py index 64f10ba1ee9..80e3af95b64 100644 --- a/reflex/utils/registry.py +++ b/reflex/utils/registry.py @@ -2,9 +2,10 @@ from pathlib import Path -from reflex.environment import environment +from reflex_core.environment import environment +from reflex_core.utils.decorator import cache_result_in_disk, once + from reflex.utils import console, net -from reflex.utils.decorator import cache_result_in_disk, once def latency(registry: str) -> int: diff --git a/reflex/utils/rename.py b/reflex/utils/rename.py index a51e6c990b1..52421a9e852 100644 --- a/reflex/utils/rename.py +++ b/reflex/utils/rename.py @@ -4,9 +4,10 @@ import sys from pathlib import Path -from reflex import constants -from reflex.config import get_config -from reflex.utils import console +from reflex_core import constants +from reflex_core.config import get_config +from reflex_core.utils import console + from reflex.utils.misc import get_module_path diff --git a/reflex/utils/serializers.py b/reflex/utils/serializers.py index 9693601ec5d..4ddf05bb29d 100644 --- a/reflex/utils/serializers.py +++ b/reflex/utils/serializers.py @@ -1,531 +1,3 @@ -"""Serializers used to convert Var types to JSON strings.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import contextlib -import dataclasses -import decimal -import functools -import inspect -import json -import warnings -from collections.abc import Callable, Mapping, Sequence -from datetime import date, datetime, time, timedelta -from enum import Enum -from importlib.util import find_spec -from pathlib import Path -from typing import Any, Literal, TypeVar, get_type_hints, overload -from uuid import UUID - -from reflex.base import Base -from reflex.constants.colors import Color -from reflex.utils import console, types - -# Mapping from type to a serializer. -# The serializer should convert the type to a JSON object. -SerializedType = str | bool | int | float | list | dict | None - - -Serializer = Callable[[Any], SerializedType] - - -SERIALIZERS: dict[type, Serializer] = {} -SERIALIZER_TYPES: dict[type, type] = {} - -SERIALIZED_FUNCTION = TypeVar("SERIALIZED_FUNCTION", bound=Serializer) - - -@overload -def serializer( - fn: None = None, - to: type[SerializedType] | None = None, - overwrite: bool | None = None, -) -> Callable[[SERIALIZED_FUNCTION], SERIALIZED_FUNCTION]: ... - - -@overload -def serializer( - fn: SERIALIZED_FUNCTION, - to: type[SerializedType] | None = None, - overwrite: bool | None = None, -) -> SERIALIZED_FUNCTION: ... - - -def serializer( - fn: SERIALIZED_FUNCTION | None = None, - to: Any = None, - overwrite: bool | None = None, -) -> SERIALIZED_FUNCTION | Callable[[SERIALIZED_FUNCTION], SERIALIZED_FUNCTION]: - """Decorator to add a serializer for a given type. - - Args: - fn: The function to decorate. - to: The type returned by the serializer. If this is `str`, then any Var created from this type will be treated as a string. - overwrite: Whether to overwrite the existing serializer. - - Returns: - The decorated function. - """ - - def wrapper(fn: SERIALIZED_FUNCTION) -> SERIALIZED_FUNCTION: - # Check the type hints to get the type of the argument. - type_hints = get_type_hints(fn) - args = [arg for arg in type_hints if arg != "return"] - - # Make sure the function takes a single argument. - if len(args) != 1: - msg = "Serializer must take a single argument." - raise ValueError(msg) - - # Get the type of the argument. - type_ = type_hints[args[0]] - - # Make sure the type is not already registered. - registered_fn = SERIALIZERS.get(type_) - if registered_fn is not None and registered_fn != fn and overwrite is not True: - message = f"Overwriting serializer for type {type_} from {registered_fn.__module__}:{registered_fn.__qualname__} to {fn.__module__}:{fn.__qualname__}." - if overwrite is False: - raise ValueError(message) - caller_frame = next( - filter( - lambda frame: frame.filename != __file__, - inspect.getouterframes(inspect.currentframe()), - ), - None, - ) - file_info = ( - f"(at {caller_frame.filename}:{caller_frame.lineno})" - if caller_frame - else "" - ) - console.warn( - f"{message} Call rx.serializer with `overwrite=True` if this is intentional. {file_info}" - ) - - to_type = to or type_hints.get("return") - - # Apply type transformation if requested - if to_type: - SERIALIZER_TYPES[type_] = to_type - get_serializer_type.cache_clear() - - # Register the serializer. - SERIALIZERS[type_] = fn - get_serializer.cache_clear() - - # Return the function. - return fn - - if fn is not None: - return wrapper(fn) - return wrapper - - -@overload -def serialize( - value: Any, get_type: Literal[True] -) -> tuple[SerializedType | None, types.GenericType | None]: ... - - -@overload -def serialize(value: Any, get_type: Literal[False]) -> SerializedType | None: ... - - -@overload -def serialize(value: Any) -> SerializedType | None: ... - - -def serialize( - value: Any, get_type: bool = False -) -> SerializedType | tuple[SerializedType | None, types.GenericType | None] | None: - """Serialize the value to a JSON string. - - Args: - value: The value to serialize. - get_type: Whether to return the type of the serialized value. - - Returns: - The serialized value, or None if a serializer is not found. - """ - # Get the serializer for the type. - serializer = get_serializer(type(value)) - - # If there is no serializer, return None. - if serializer is None: - if dataclasses.is_dataclass(value) and not isinstance(value, type): - return {k.name: getattr(value, k.name) for k in dataclasses.fields(value)} - - if get_type: - return None, None - return None - - # Serialize the value. - serialized = serializer(value) - - # Return the serialized value and the type. - if get_type: - return serialized, get_serializer_type(type(value)) - return serialized - - -@functools.lru_cache -def get_serializer(type_: type) -> Serializer | None: - """Get the serializer for the type. - - Args: - type_: The type to get the serializer for. - - Returns: - The serializer for the type, or None if there is no serializer. - """ - # First, check if the type is registered. - serializer = SERIALIZERS.get(type_) - if serializer is not None: - return serializer - - # If the type is not registered, check if it is a subclass of a registered type. - for registered_type, serializer in reversed(SERIALIZERS.items()): - if issubclass(type_, registered_type): - return serializer - - # If there is no serializer, return None. - return None - - -@functools.lru_cache -def get_serializer_type(type_: type) -> type | None: - """Get the converted type for the type after serializing. - - Args: - type_: The type to get the serializer type for. - - Returns: - The serialized type for the type, or None if there is no type conversion registered. - """ - # First, check if the type is registered. - serializer = SERIALIZER_TYPES.get(type_) - if serializer is not None: - return serializer - - # If the type is not registered, check if it is a subclass of a registered type. - for registered_type, serializer in reversed(SERIALIZER_TYPES.items()): - if issubclass(type_, registered_type): - return serializer - - # If there is no serializer, return None. - return None - - -def has_serializer(type_: type, into_type: type | None = None) -> bool: - """Check if there is a serializer for the type. - - Args: - type_: The type to check. - into_type: The type to serialize into. - - Returns: - Whether there is a serializer for the type. - """ - serializer_for_type = get_serializer(type_) - return serializer_for_type is not None and ( - into_type is None or get_serializer_type(type_) == into_type - ) - - -def can_serialize(type_: type, into_type: type | None = None) -> bool: - """Check if there is a serializer for the type. - - Args: - type_: The type to check. - into_type: The type to serialize into. - - Returns: - Whether there is a serializer for the type. - """ - return ( - isinstance(type_, type) - and dataclasses.is_dataclass(type_) - and (into_type is None or into_type is dict) - ) or has_serializer(type_, into_type) - - -@serializer(to=str) -def serialize_type(value: type) -> str: - """Serialize a python type. - - Args: - value: the type to serialize. - - Returns: - The serialized type. - """ - return value.__name__ - - -@serializer(to=dict) -def serialize_base(value: Base) -> dict: - """Serialize a Base instance. - - Args: - value : The Base to serialize. - - Returns: - The serialized Base. - """ - from reflex.vars.base import Var - - return { - k: v for k, v in value.dict().items() if isinstance(v, Var) or not callable(v) - } - - -if find_spec("pydantic"): - from pydantic import BaseModel as BaseModelV2 - from pydantic.v1 import BaseModel as BaseModelV1 - - @serializer(to=dict) - def serialize_base_model_v1(model: BaseModelV1) -> dict: - """Serialize a pydantic v1 BaseModel instance. - - Args: - model: The BaseModel to serialize. - - Returns: - The serialized BaseModel. - """ - return model.dict() - - if BaseModelV1 is not BaseModelV2: - - @serializer(to=dict) - def serialize_base_model_v2(model: BaseModelV2) -> dict: - """Serialize a pydantic v2 BaseModel instance. - - Args: - model: The BaseModel to serialize. - - Returns: - The serialized BaseModel. - """ - return model.model_dump() - - -@serializer -def serialize_set(value: set) -> list: - """Serialize a set to a JSON serializable list. - - Args: - value: The set to serialize. - - Returns: - The serialized list. - """ - return list(value) - - -@serializer -def serialize_sequence(value: Sequence) -> list: - """Serialize a sequence to a JSON serializable list. - - Args: - value: The sequence to serialize. - - Returns: - The serialized list. - """ - return list(value) - - -@serializer(to=dict) -def serialize_mapping(value: Mapping) -> dict: - """Serialize a mapping type to a dictionary. - - Args: - value: The mapping instance to serialize. - - Returns: - A new dictionary containing the same key-value pairs as the input mapping. - """ - return {**value} - - -@serializer(to=str) -def serialize_datetime(dt: date | datetime | time | timedelta) -> str: - """Serialize a datetime to a JSON string. - - Args: - dt: The datetime to serialize. - - Returns: - The serialized datetime. - """ - return str(dt) - - -@serializer(to=str) -def serialize_path(path: Path) -> str: - """Serialize a pathlib.Path to a JSON string. - - Args: - path: The path to serialize. - - Returns: - The serialized path. - """ - return str(path.as_posix()) - - -@serializer -def serialize_enum(en: Enum) -> str: - """Serialize a enum to a JSON string. - - Args: - en: The enum to serialize. - - Returns: - The serialized enum. - """ - return en.value - - -@serializer(to=str) -def serialize_uuid(uuid: UUID) -> str: - """Serialize a UUID to a JSON string. - - Args: - uuid: The UUID to serialize. - - Returns: - The serialized UUID. - """ - return str(uuid) - - -@serializer(to=float) -def serialize_decimal(value: decimal.Decimal) -> float: - """Serialize a Decimal to a float. - - Args: - value: The Decimal to serialize. - - Returns: - The serialized Decimal as a float. - """ - return float(value) - - -@serializer(to=str) -def serialize_color(color: Color) -> str: - """Serialize a color. - - Args: - color: The color to serialize. - - Returns: - The serialized color. - """ - return color.__format__("") - - -with contextlib.suppress(ImportError): - from pandas import DataFrame - - def format_dataframe_values(df: DataFrame) -> list[list[Any]]: - """Format dataframe values to a list of lists. - - Args: - df: The dataframe to format. - - Returns: - The dataframe as a list of lists. - """ - return [ - [str(d) if isinstance(d, (list, tuple)) else d for d in data] - for data in list(df.to_numpy().tolist()) - ] - - @serializer - def serialize_dataframe(df: DataFrame) -> dict: - """Serialize a pandas dataframe. - - Args: - df: The dataframe to serialize. - - Returns: - The serialized dataframe. - """ - return { - "columns": df.columns.tolist(), - "data": format_dataframe_values(df), - } - - -with contextlib.suppress(ImportError): - from plotly.graph_objects import Figure, layout - from plotly.io import to_json - - @serializer - def serialize_figure(figure: Figure) -> dict: - """Serialize a plotly figure. - - Args: - figure: The figure to serialize. - - Returns: - The serialized figure. - """ - return json.loads(str(to_json(figure))) - - @serializer - def serialize_template(template: layout.Template) -> dict: - """Serialize a plotly template. - - Args: - template: The template to serialize. - - Returns: - The serialized template. - """ - return { - "data": json.loads(str(to_json(template.data))), - "layout": json.loads(str(to_json(template.layout))), - } - - -with contextlib.suppress(ImportError): - import base64 - import io - - from PIL.Image import MIME - from PIL.Image import Image as Img - - @serializer - def serialize_image(image: Img) -> str: - """Serialize a plotly figure. - - Args: - image: The image to serialize. - - Returns: - The serialized image. - """ - buff = io.BytesIO() - image_format = getattr(image, "format", None) or "PNG" - image.save(buff, format=image_format) - image_bytes = buff.getvalue() - base64_image = base64.b64encode(image_bytes).decode("utf-8") - try: - # Newer method to get the mime type, but does not always work. - mime_type = image.get_format_mimetype() # pyright: ignore [reportAttributeAccessIssue] - except AttributeError: - try: - # Fallback method - mime_type = MIME[image_format] - except KeyError: - # Unknown mime_type: warn and return image/png and hope the browser can sort it out. - warnings.warn( # noqa: B028 - f"Unknown mime type for {image} {image_format}. Defaulting to image/png" - ) - mime_type = "image/png" - - return f"data:{mime_type};base64,{base64_image}" +from reflex_core.utils.serializers import * diff --git a/reflex/utils/tasks.py b/reflex/utils/tasks.py index ec51d94af22..6bb47fe60ae 100644 --- a/reflex/utils/tasks.py +++ b/reflex/utils/tasks.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Coroutine from typing import Any -from reflex.utils import console +from reflex_core.utils import console async def _run_forever( diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index 87202215268..e5ec1dacf02 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -11,11 +11,12 @@ from datetime import datetime, timezone from typing import TypedDict -from reflex import constants -from reflex.environment import environment +from reflex_core import constants +from reflex_core.environment import environment +from reflex_core.utils.decorator import once, once_unless_none +from reflex_core.utils.exceptions import ReflexError + from reflex.utils import console, processes -from reflex.utils.decorator import once, once_unless_none -from reflex.utils.exceptions import ReflexError from reflex.utils.js_runtimes import get_bun_version, get_node_version from reflex.utils.prerequisites import ensure_reflex_installation_id, get_project_hash @@ -316,7 +317,7 @@ def _send_event(event_data: _Event) -> bool: def _send(event: str, telemetry_enabled: bool | None, **kwargs) -> bool: - from reflex.config import get_config + from reflex_core.config import get_config # Get the telemetry_enabled from the config if it is not specified. if telemetry_enabled is None: diff --git a/reflex/utils/templates.py b/reflex/utils/templates.py index 6bd12253dfe..4fbc76a92e4 100644 --- a/reflex/utils/templates.py +++ b/reflex/utils/templates.py @@ -7,8 +7,9 @@ from pathlib import Path from urllib.parse import urlparse -from reflex import constants -from reflex.config import get_config +from reflex_core import constants +from reflex_core.config import get_config + from reflex.utils import console, net, path_ops, redir diff --git a/reflex/utils/token_manager.py b/reflex/utils/token_manager.py index 514d641cea6..95c73fda086 100644 --- a/reflex/utils/token_manager.py +++ b/reflex/utils/token_manager.py @@ -193,7 +193,7 @@ def __init__(self, redis: Redis): self.redis = redis # Get token expiration from config (default 1 hour) - from reflex.config import get_config + from reflex_core.config import get_config config = get_config() self.token_expiration = config.redis_token_expiration diff --git a/reflex/utils/types.py b/reflex/utils/types.py index b375157d556..9345271874c 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -1,1286 +1,3 @@ -"""Contains custom types and methods to check types.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import dataclasses -import sys -import types -from collections.abc import Callable, Iterable, Mapping, Sequence -from enum import Enum -from functools import cached_property, lru_cache -from importlib.util import find_spec -from types import GenericAlias -from typing import ( # noqa: UP035 - TYPE_CHECKING, - Any, - Awaitable, - ClassVar, - Dict, - ForwardRef, - List, - Literal, - MutableMapping, - NoReturn, - Protocol, - Tuple, - TypeVar, - Union, - _eval_type, # pyright: ignore [reportAttributeAccessIssue] - _GenericAlias, # pyright: ignore [reportAttributeAccessIssue] - _SpecialGenericAlias, # pyright: ignore [reportAttributeAccessIssue] - get_args, - is_typeddict, -) -from typing import get_origin as get_origin_og -from typing import get_type_hints as get_type_hints_og - -from typing_extensions import Self as Self -from typing_extensions import override as override - -import reflex -from reflex import constants -from reflex.base import Base -from reflex.components.core.breakpoints import Breakpoints -from reflex.utils import console - -# Potential GenericAlias types for isinstance checks. -GenericAliasTypes = (_GenericAlias, GenericAlias, _SpecialGenericAlias) - -# Potential Union types for isinstance checks. -UnionTypes = (Union, types.UnionType) - -# Union of generic types. -GenericType = type | _GenericAlias - -# Valid state var types. -PrimitiveTypes = (int, float, bool, str, list, dict, set, tuple) -StateVarTypes = (*PrimitiveTypes, Base, type(None)) - -if TYPE_CHECKING: - from reflex.state import BaseState - from reflex.vars.base import Var - -VAR1 = TypeVar("VAR1", bound="Var") -VAR2 = TypeVar("VAR2", bound="Var") -VAR3 = TypeVar("VAR3", bound="Var") -VAR4 = TypeVar("VAR4", bound="Var") -VAR5 = TypeVar("VAR5", bound="Var") -VAR6 = TypeVar("VAR6", bound="Var") -VAR7 = TypeVar("VAR7", bound="Var") - - -class _ArgsSpec0(Protocol): - def __call__(self) -> Sequence[Var]: ... - - -class _ArgsSpec1(Protocol): - def __call__(self, var1: VAR1, /) -> Sequence[Var]: ... # pyright: ignore [reportInvalidTypeVarUse] - - -class _ArgsSpec2(Protocol): - def __call__(self, var1: VAR1, var2: VAR2, /) -> Sequence[Var]: ... # pyright: ignore [reportInvalidTypeVarUse] - - -class _ArgsSpec3(Protocol): - def __call__(self, var1: VAR1, var2: VAR2, var3: VAR3, /) -> Sequence[Var]: ... # pyright: ignore [reportInvalidTypeVarUse] - - -class _ArgsSpec4(Protocol): - def __call__( - self, - var1: VAR1, # pyright: ignore [reportInvalidTypeVarUse] - var2: VAR2, # pyright: ignore [reportInvalidTypeVarUse] - var3: VAR3, # pyright: ignore [reportInvalidTypeVarUse] - var4: VAR4, # pyright: ignore [reportInvalidTypeVarUse] - /, - ) -> Sequence[Var]: ... - - -class _ArgsSpec5(Protocol): - def __call__( - self, - var1: VAR1, # pyright: ignore [reportInvalidTypeVarUse] - var2: VAR2, # pyright: ignore [reportInvalidTypeVarUse] - var3: VAR3, # pyright: ignore [reportInvalidTypeVarUse] - var4: VAR4, # pyright: ignore [reportInvalidTypeVarUse] - var5: VAR5, # pyright: ignore [reportInvalidTypeVarUse] - /, - ) -> Sequence[Var]: ... - - -class _ArgsSpec6(Protocol): - def __call__( - self, - var1: VAR1, # pyright: ignore [reportInvalidTypeVarUse] - var2: VAR2, # pyright: ignore [reportInvalidTypeVarUse] - var3: VAR3, # pyright: ignore [reportInvalidTypeVarUse] - var4: VAR4, # pyright: ignore [reportInvalidTypeVarUse] - var5: VAR5, # pyright: ignore [reportInvalidTypeVarUse] - var6: VAR6, # pyright: ignore [reportInvalidTypeVarUse] - /, - ) -> Sequence[Var]: ... - - -class _ArgsSpec7(Protocol): - def __call__( - self, - var1: VAR1, # pyright: ignore [reportInvalidTypeVarUse] - var2: VAR2, # pyright: ignore [reportInvalidTypeVarUse] - var3: VAR3, # pyright: ignore [reportInvalidTypeVarUse] - var4: VAR4, # pyright: ignore [reportInvalidTypeVarUse] - var5: VAR5, # pyright: ignore [reportInvalidTypeVarUse] - var6: VAR6, # pyright: ignore [reportInvalidTypeVarUse] - var7: VAR7, # pyright: ignore [reportInvalidTypeVarUse] - /, - ) -> Sequence[Var]: ... - - -ArgsSpec = ( - _ArgsSpec0 - | _ArgsSpec1 - | _ArgsSpec2 - | _ArgsSpec3 - | _ArgsSpec4 - | _ArgsSpec5 - | _ArgsSpec6 - | _ArgsSpec7 -) - -Scope = MutableMapping[str, Any] -Message = MutableMapping[str, Any] - -Receive = Callable[[], Awaitable[Message]] -Send = Callable[[Message], Awaitable[None]] - -ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]] - -PrimitiveToAnnotation = { - list: List, # noqa: UP006 - tuple: Tuple, # noqa: UP006 - dict: Dict, # noqa: UP006 -} - -RESERVED_BACKEND_VAR_NAMES = {"_abc_impl", "_backend_vars", "_was_touched", "_mixin"} - - -class Unset: - """A class to represent an unset value. - - This is used to differentiate between a value that is not set and a value that is set to None. - """ - - def __repr__(self) -> str: - """Return the string representation of the class. - - Returns: - The string representation of the class. - """ - return "Unset" - - def __bool__(self) -> bool: - """Return False when the class is used in a boolean context. - - Returns: - False - """ - return False - - -@lru_cache -def _get_origin_cached(tp: Any): - return get_origin_og(tp) - - -def get_origin(tp: Any): - """Get the origin of a class. - - Args: - tp: The class to get the origin of. - - Returns: - The origin of the class. - """ - return ( - origin - if (origin := getattr(tp, "__origin__", None)) is not None - else _get_origin_cached(tp) - ) - - -@lru_cache -def is_generic_alias(cls: GenericType) -> bool: - """Check whether the class is a generic alias. - - Args: - cls: The class to check. - - Returns: - Whether the class is a generic alias. - """ - return isinstance(cls, GenericAliasTypes) - - -@lru_cache -def get_type_hints(obj: Any) -> dict[str, Any]: - """Get the type hints of a class. - - Args: - obj: The class to get the type hints of. - - Returns: - The type hints of the class. - """ - return get_type_hints_og(obj) - - -def _unionize(args: list[GenericType]) -> GenericType: - if not args: - return Any # pyright: ignore [reportReturnType] - if len(args) == 1: - return args[0] - return Union[tuple(args)] # noqa: UP007 - - -def unionize(*args: GenericType) -> type: - """Unionize the types. - - Args: - args: The types to unionize. - - Returns: - The unionized types. - """ - return _unionize([arg for arg in args if arg is not NoReturn]) - - -def is_none(cls: GenericType) -> bool: - """Check if a class is None. - - Args: - cls: The class to check. - - Returns: - Whether the class is None. - """ - return cls is type(None) or cls is None - - -def is_union(cls: GenericType) -> bool: - """Check if a class is a Union. - - Args: - cls: The class to check. - - Returns: - Whether the class is a Union. - """ - origin = getattr(cls, "__origin__", None) - if origin is Union: - return True - return origin is None and isinstance(cls, types.UnionType) - - -def is_literal(cls: GenericType) -> bool: - """Check if a class is a Literal. - - Args: - cls: The class to check. - - Returns: - Whether the class is a literal. - """ - return getattr(cls, "__origin__", None) is Literal - - -@lru_cache -def has_args(cls: type) -> bool: - """Check if the class has generic parameters. - - Args: - cls: The class to check. - - Returns: - Whether the class has generic - """ - if get_args(cls): - return True - - # Check if the class inherits from a generic class (using __orig_bases__) - if hasattr(cls, "__orig_bases__"): - for base in cls.__orig_bases__: - if get_args(base): - return True - - return False - - -def is_optional(cls: GenericType) -> bool: - """Check if a class is an Optional. - - Args: - cls: The class to check. - - Returns: - Whether the class is an Optional. - """ - return ( - cls is None - or cls is type(None) - or (is_union(cls) and type(None) in get_args(cls)) - ) - - -def is_classvar(a_type: Any) -> bool: - """Check if a type is a ClassVar. - - Args: - a_type: The type to check. - - Returns: - Whether the type is a ClassVar. - """ - return ( - a_type is ClassVar - or (type(a_type) is _GenericAlias and a_type.__origin__ is ClassVar) - or ( - type(a_type) is ForwardRef and a_type.__forward_arg__.startswith("ClassVar") - ) - ) - - -def value_inside_optional(cls: GenericType) -> GenericType: - """Get the value inside an Optional type or the original type. - - Args: - cls: The class to check. - - Returns: - The value inside the Optional type or the original type. - """ - if is_union(cls) and len(args := get_args(cls)) >= 2 and type(None) in args: - if len(args) == 2: - return args[0] if args[1] is type(None) else args[1] - return unionize(*[arg for arg in args if arg is not type(None)]) - return cls - - -def get_field_type(cls: GenericType, field_name: str) -> GenericType | None: - """Get the type of a field in a class. - - Args: - cls: The class to check. - field_name: The name of the field to check. - - Returns: - The type of the field, if it exists, else None. - """ - if (fields := getattr(cls, "_fields", None)) is not None and field_name in fields: - return fields[field_name].annotated_type - if ( - hasattr(cls, "__fields__") - and field_name in cls.__fields__ - and hasattr(cls.__fields__[field_name], "annotation") - and not isinstance(cls.__fields__[field_name].annotation, (str, ForwardRef)) - ): - return cls.__fields__[field_name].annotation - type_hints = get_type_hints(cls) - return type_hints.get(field_name, None) - - -PROPERTY_CLASSES = (property,) -if find_spec("sqlalchemy") and find_spec("sqlalchemy.ext"): - from sqlalchemy.ext.hybrid import hybrid_property - - PROPERTY_CLASSES += (hybrid_property,) - - -def get_property_hint(attr: Any | None) -> GenericType | None: - """Check if an attribute is a property and return its type hint. - - Args: - attr: The descriptor to check. - - Returns: - The type hint of the property, if it is a property, else None. - """ - if not isinstance(attr, PROPERTY_CLASSES): - return None - hints = get_type_hints(attr.fget) - return hints.get("return", None) - - -def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None: - """Check if an attribute can be accessed on the cls and return its type. - - Supports pydantic models, unions, and annotated attributes on rx.Model. - - Args: - cls: The class to check. - name: The name of the attribute to check. - - Returns: - The type of the attribute, if accessible, or None - """ - try: - attr = getattr(cls, name, None) - except NotImplementedError: - attr = None - - if hint := get_property_hint(attr): - return hint - - if hasattr(cls, "__fields__") and name in cls.__fields__: - # pydantic models - return get_field_type(cls, name) - if find_spec("sqlalchemy") and find_spec("sqlalchemy.orm"): - import sqlalchemy - from sqlalchemy.ext.associationproxy import AssociationProxyInstance - from sqlalchemy.orm import ( - DeclarativeBase, - Mapped, - QueryableAttribute, - Relationship, - ) - - from reflex.model import Model - - if find_spec("sqlmodel"): - from sqlmodel import SQLModel - - sqlmodel_types = (Model, SQLModel) - else: - sqlmodel_types = (Model,) - - if isinstance(cls, type) and issubclass(cls, DeclarativeBase): - insp = sqlalchemy.inspect(cls) - if name in insp.columns: - # check for list types - column = insp.columns[name] - column_type = column.type - try: - type_ = insp.columns[name].type.python_type - except NotImplementedError: - type_ = None - if type_ is not None: - if hasattr(column_type, "item_type"): - try: - item_type = column_type.item_type.python_type # pyright: ignore [reportAttributeAccessIssue] - except NotImplementedError: - item_type = None - if item_type is not None: - if type_ in PrimitiveToAnnotation: - type_ = PrimitiveToAnnotation[type_] - type_ = type_[item_type] # pyright: ignore [reportIndexIssue] - if hasattr(column, "nullable") and column.nullable: - type_ = type_ | None - return type_ - if name in insp.all_orm_descriptors: - descriptor = insp.all_orm_descriptors[name] - if hint := get_property_hint(descriptor): - return hint - if isinstance(descriptor, QueryableAttribute): - prop = descriptor.property - if isinstance(prop, Relationship): - type_ = prop.mapper.class_ - # TODO: check for nullable? - return list[type_] if prop.uselist else type_ | None - if isinstance(attr, AssociationProxyInstance): - return list[ - get_attribute_access_type( - attr.target_class, - attr.remote_attr.key, # pyright: ignore [reportAttributeAccessIssue] - ) - ] - elif ( - isinstance(cls, type) - and not is_generic_alias(cls) - and issubclass(cls, sqlmodel_types) - ): - # Check in the annotations directly (for sqlmodel.Relationship) - hints = get_type_hints(cls) # pyright: ignore [reportArgumentType] - if name in hints: - type_ = hints[name] - type_origin = get_origin(type_) - if isinstance(type_origin, type) and issubclass(type_origin, Mapped): - return get_args(type_)[0] # SQLAlchemy v2 - if find_spec("pydantic"): - from pydantic.v1.fields import ModelField - - if isinstance(type_, ModelField): - return type_.type_ # SQLAlchemy v1.4 - return type_ - if is_union(cls): - # Check in each arg of the annotation. - return unionize( - *(get_attribute_access_type(arg, name) for arg in get_args(cls)) - ) - if isinstance(cls, type): - # Bare class - exceptions = NameError - try: - hints = get_type_hints(cls) # pyright: ignore [reportArgumentType] - if name in hints: - return hints[name] - except exceptions as e: - console.warn(f"Failed to resolve ForwardRefs for {cls}.{name} due to {e}") - return None # Attribute is not accessible. - - -@lru_cache -def get_base_class(cls: GenericType) -> type: - """Get the base class of a class. - - Args: - cls: The class. - - Returns: - The base class of the class. - - Raises: - TypeError: If a literal has multiple types. - """ - if is_literal(cls): - # only literals of the same type are supported. - arg_type = type(get_args(cls)[0]) - if not all(type(arg) is arg_type for arg in get_args(cls)): - msg = "only literals of the same type are supported" - raise TypeError(msg) - return type(get_args(cls)[0]) - - if is_union(cls): - return tuple(get_base_class(arg) for arg in get_args(cls)) # pyright: ignore [reportReturnType] - - return get_base_class(cls.__origin__) if is_generic_alias(cls) else cls - - -def _breakpoints_satisfies_typing(cls_check: GenericType, instance: Any) -> bool: - """Check if the breakpoints instance satisfies the typing. - - Args: - cls_check: The class to check against. - instance: The instance to check. - - Returns: - Whether the breakpoints instance satisfies the typing. - """ - cls_check_base = get_base_class(cls_check) - - if cls_check_base == Breakpoints: - _, expected_type = get_args(cls_check) - if is_literal(expected_type): - for value in instance.values(): - if not isinstance(value, str) or value not in get_args(expected_type): - return False - return True - if isinstance(cls_check_base, tuple): - # union type, so check all types - return any( - _breakpoints_satisfies_typing(type_to_check, instance) - for type_to_check in get_args(cls_check) - ) - if cls_check_base == reflex.vars.Var and "__args__" in cls_check.__dict__: - return _breakpoints_satisfies_typing(get_args(cls_check)[0], instance) - - return False - - -def _issubclass(cls: GenericType, cls_check: GenericType, instance: Any = None) -> bool: - """Check if a class is a subclass of another class. - - Args: - cls: The class to check. - cls_check: The class to check against. - instance: An instance of cls to aid in checking generics. - - Returns: - Whether the class is a subclass of the other class. - - Raises: - TypeError: If the base class is not valid for issubclass. - """ - # Special check for Any. - if cls_check == Any: - return True - if cls in [Any, Callable, None]: - return False - - # Get the base classes. - cls_base = get_base_class(cls) - cls_check_base = get_base_class(cls_check) - - # The class we're checking should not be a union. - if isinstance(cls_base, tuple): - return False - - # Check that fields of breakpoints match the expected values. - if isinstance(instance, Breakpoints): - return _breakpoints_satisfies_typing(cls_check, instance) - - if isinstance(cls_check_base, tuple): - cls_check_base = tuple( - cls_check_one if not is_typeddict(cls_check_one) else dict - for cls_check_one in cls_check_base - ) - if is_typeddict(cls_check_base): - cls_check_base = dict - - # Check if the types match. - try: - return cls_check_base == Any or issubclass(cls_base, cls_check_base) - except TypeError as te: - # These errors typically arise from bad annotations and are hard to - # debug without knowing the type that we tried to compare. - msg = f"Invalid type for issubclass: {cls_base}" - raise TypeError(msg) from te - - -def does_obj_satisfy_typed_dict( - obj: Any, - cls: GenericType, - *, - nested: int = 0, - treat_var_as_type: bool = True, - treat_mutable_obj_as_immutable: bool = False, -) -> bool: - """Check if an object satisfies a typed dict. - - Args: - obj: The object to check. - cls: The typed dict to check against. - nested: How many levels deep to check. - treat_var_as_type: Whether to treat Var as the type it represents, i.e. _var_type. - treat_mutable_obj_as_immutable: Whether to treat mutable objects as immutable. Useful if a component declares a mutable object as a prop, but the value is not expected to change. - - Returns: - Whether the object satisfies the typed dict. - """ - if not isinstance(obj, Mapping): - return False - - key_names_to_values = get_type_hints(cls) - required_keys: frozenset[str] = getattr(cls, "__required_keys__", frozenset()) - is_closed = getattr(cls, "__closed__", False) - extra_items_type = getattr(cls, "__extra_items__", Any) - - for key, value in obj.items(): - if is_closed and key not in key_names_to_values: - return False - if nested: - if key in key_names_to_values: - expected_type = key_names_to_values[key] - if not _isinstance( - value, - expected_type, - nested=nested - 1, - treat_var_as_type=treat_var_as_type, - treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable, - ): - return False - else: - if not _isinstance( - value, - extra_items_type, - nested=nested - 1, - treat_var_as_type=treat_var_as_type, - treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable, - ): - return False - - # required keys are all present - return required_keys.issubset(frozenset(obj)) - - -def _isinstance( - obj: Any, - cls: GenericType, - *, - nested: int = 0, - treat_var_as_type: bool = True, - treat_mutable_obj_as_immutable: bool = False, -) -> bool: - """Check if an object is an instance of a class. - - Args: - obj: The object to check. - cls: The class to check against. - nested: How many levels deep to check. - treat_var_as_type: Whether to treat Var as the type it represents, i.e. _var_type. - treat_mutable_obj_as_immutable: Whether to treat mutable objects as immutable. Useful if a component declares a mutable object as a prop, but the value is not expected to change. - - Returns: - Whether the object is an instance of the class. - """ - if cls is Any: - return True - - from reflex.vars import LiteralVar, Var - - if cls is Var: - return isinstance(obj, Var) - if isinstance(obj, LiteralVar): - return treat_var_as_type and _isinstance( - obj._var_value, cls, nested=nested, treat_var_as_type=True - ) - if isinstance(obj, Var): - return treat_var_as_type and typehint_issubclass( - obj._var_type, - cls, - treat_mutable_superclasss_as_immutable=treat_mutable_obj_as_immutable, - treat_literals_as_union_of_types=True, - treat_any_as_subtype_of_everything=True, - ) - - if cls is None or cls is type(None): - return obj is None - - if cls is not None and is_union(cls): - return any( - _isinstance(obj, arg, nested=nested, treat_var_as_type=treat_var_as_type) - for arg in get_args(cls) - ) - - if is_literal(cls): - return obj in get_args(cls) - - origin = get_origin(cls) - - if origin is None: - # cls is a typed dict - if is_typeddict(cls): - if nested: - return does_obj_satisfy_typed_dict( - obj, - cls, - nested=nested - 1, - treat_var_as_type=treat_var_as_type, - treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable, - ) - return isinstance(obj, dict) - - # cls is a float - if cls is float: - return isinstance(obj, (float, int)) - - # cls is a simple class - return isinstance(obj, cls) - - args = get_args(cls) - - if not args: - if treat_mutable_obj_as_immutable: - if origin is dict: - origin = Mapping - elif origin is list or origin is set: - origin = Sequence - # cls is a simple generic class - return isinstance(obj, origin) - - if origin is Var and args: - # cls is a Var - return _isinstance( - obj, - args[0], - nested=nested, - treat_var_as_type=treat_var_as_type, - treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable, - ) - - if nested > 0 and args: - if origin is list: - expected_class = Sequence if treat_mutable_obj_as_immutable else list - return isinstance(obj, expected_class) and all( - _isinstance( - item, - args[0], - nested=nested - 1, - treat_var_as_type=treat_var_as_type, - ) - for item in obj - ) - if origin is tuple: - if args[-1] is Ellipsis: - return isinstance(obj, tuple) and all( - _isinstance( - item, - args[0], - nested=nested - 1, - treat_var_as_type=treat_var_as_type, - ) - for item in obj - ) - return ( - isinstance(obj, tuple) - and len(obj) == len(args) - and all( - _isinstance( - item, - arg, - nested=nested - 1, - treat_var_as_type=treat_var_as_type, - ) - for item, arg in zip(obj, args, strict=True) - ) - ) - if origin in (dict, Mapping, Breakpoints): - expected_class = ( - dict - if origin is dict and not treat_mutable_obj_as_immutable - else Mapping - ) - return isinstance(obj, expected_class) and all( - _isinstance( - key, args[0], nested=nested - 1, treat_var_as_type=treat_var_as_type - ) - and _isinstance( - value, - args[1], - nested=nested - 1, - treat_var_as_type=treat_var_as_type, - ) - for key, value in obj.items() - ) - if origin is set: - expected_class = Sequence if treat_mutable_obj_as_immutable else set - return isinstance(obj, expected_class) and all( - _isinstance( - item, - args[0], - nested=nested - 1, - treat_var_as_type=treat_var_as_type, - ) - for item in obj - ) - - if args: - from reflex.vars import Field - - if origin is Field: - return _isinstance( - obj, args[0], nested=nested, treat_var_as_type=treat_var_as_type - ) - - return isinstance(obj, get_base_class(cls)) - - -def is_dataframe(value: type) -> bool: - """Check if the given value is a dataframe. - - Args: - value: The value to check. - - Returns: - Whether the value is a dataframe. - """ - if is_generic_alias(value) or value == Any: - return False - return value.__name__ == "DataFrame" - - -def is_valid_var_type(type_: type) -> bool: - """Check if the given type is a valid prop type. - - Args: - type_: The type to check. - - Returns: - Whether the type is a valid prop type. - """ - from reflex.utils import serializers - - if is_union(type_): - return all(is_valid_var_type(arg) for arg in get_args(type_)) - - if is_literal(type_): - types = {type(value) for value in get_args(type_)} - return all(is_valid_var_type(type_) for type_ in types) - - type_ = origin if (origin := get_origin(type_)) is not None else type_ - - return ( - issubclass(type_, StateVarTypes) - or serializers.has_serializer(type_) - or dataclasses.is_dataclass(type_) - ) - - -def is_backend_base_variable(name: str, cls: type[BaseState]) -> bool: - """Check if this variable name correspond to a backend variable. - - Args: - name: The name of the variable to check - cls: The class of the variable to check (must be a BaseState subclass) - - Returns: - bool: The result of the check - """ - if name in RESERVED_BACKEND_VAR_NAMES: - return False - - if not name.startswith("_"): - return False - - if name.startswith("__"): - return False - - if name.startswith(f"_{cls.__name__}__"): - return False - - hints = cls._get_type_hints() - if name in hints: - hint = get_origin(hints[name]) - if hint == ClassVar: - return False - - if name in cls.inherited_backend_vars: - return False - - from reflex.vars.base import is_computed_var - - if name in cls.__dict__: - value = cls.__dict__[name] - if type(value) is classmethod: - return False - if callable(value): - return False - - if isinstance( - value, - ( - types.FunctionType, - property, - cached_property, - ), - ) or is_computed_var(value): - return False - - return True - - -def check_type_in_allowed_types(value_type: type, allowed_types: Iterable) -> bool: - """Check that a value type is found in a list of allowed types. - - Args: - value_type: Type of value. - allowed_types: Iterable of allowed types. - - Returns: - If the type is found in the allowed types. - """ - return get_base_class(value_type) in allowed_types - - -def check_prop_in_allowed_types(prop: Any, allowed_types: Iterable) -> bool: - """Check that a prop value is in a list of allowed types. - Does the check in a way that works regardless if it's a raw value or a state Var. - - Args: - prop: The prop to check. - allowed_types: The list of allowed types. - - Returns: - If the prop type match one of the allowed_types. - """ - from reflex.vars import Var - - type_ = prop._var_type if isinstance(prop, Var) else type(prop) - return type_ in allowed_types - - -def is_encoded_fstring(value: Any) -> bool: - """Check if a value is an encoded Var f-string. - - Args: - value: The value string to check. - - Returns: - Whether the value is an f-string - """ - return isinstance(value, str) and constants.REFLEX_VAR_OPENING_TAG in value - - -def validate_literal(key: str, value: Any, expected_type: type, comp_name: str): - """Check that a value is a valid literal. - - Args: - key: The prop name. - value: The prop value to validate. - expected_type: The expected type(literal type). - comp_name: Name of the component. - - Raises: - ValueError: When the value is not a valid literal. - """ - from reflex.vars import Var - - if ( - is_literal(expected_type) - and not isinstance(value, Var) # validating vars is not supported yet. - and not is_encoded_fstring(value) # f-strings are not supported. - and value not in expected_type.__args__ - ): - allowed_values = expected_type.__args__ - if value not in allowed_values: - allowed_value_str = ",".join([ - str(v) if not isinstance(v, str) else f"'{v}'" for v in allowed_values - ]) - value_str = f"'{value}'" if isinstance(value, str) else value - msg = f"prop value for {key!s} of the `{comp_name}` component should be one of the following: {allowed_value_str}. Got {value_str} instead" - raise ValueError(msg) - - -def safe_issubclass(cls: Any, cls_check: Any | tuple[Any, ...]): - """Check if a class is a subclass of another class. Returns False if internal error occurs. - - Args: - cls: The class to check. - cls_check: The class to check against. - - Returns: - Whether the class is a subclass of the other class. - """ - try: - return issubclass(cls, cls_check) - except TypeError: - return False - - -def typehint_issubclass( - possible_subclass: Any, - possible_superclass: Any, - *, - treat_mutable_superclasss_as_immutable: bool = False, - treat_literals_as_union_of_types: bool = True, - treat_any_as_subtype_of_everything: bool = False, -) -> bool: - """Check if a type hint is a subclass of another type hint. - - Args: - possible_subclass: The type hint to check. - possible_superclass: The type hint to check against. - treat_mutable_superclasss_as_immutable: Whether to treat target classes as immutable. - treat_literals_as_union_of_types: Whether to treat literals as a union of their types. - treat_any_as_subtype_of_everything: Whether to treat Any as a subtype of everything. This is the default behavior in Python. - - Returns: - Whether the type hint is a subclass of the other type hint. - """ - if possible_subclass is possible_superclass or possible_superclass is Any: - return True - if possible_subclass is Any: - return treat_any_as_subtype_of_everything - if possible_subclass is NoReturn: - return True - - provided_type_origin = get_origin(possible_subclass) - accepted_type_origin = get_origin(possible_superclass) - - if provided_type_origin is None and accepted_type_origin is None: - # In this case, we are dealing with a non-generic type, so we can use issubclass - return issubclass(possible_subclass, possible_superclass) - - if treat_literals_as_union_of_types and is_literal(possible_superclass): - args = get_args(possible_superclass) - return any( - typehint_issubclass( - possible_subclass, - type(arg), - treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, - treat_literals_as_union_of_types=treat_literals_as_union_of_types, - treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, - ) - for arg in args - ) - - if is_literal(possible_subclass): - args = get_args(possible_subclass) - return all( - _isinstance( - arg, - possible_superclass, - treat_mutable_obj_as_immutable=treat_mutable_superclasss_as_immutable, - nested=2, - ) - for arg in args - ) - - provided_type_origin = ( - Union if provided_type_origin is types.UnionType else provided_type_origin - ) - accepted_type_origin = ( - Union if accepted_type_origin is types.UnionType else accepted_type_origin - ) - - # Get type arguments (e.g., [float, int] for dict[float, int]) - provided_args = get_args(possible_subclass) - accepted_args = get_args(possible_superclass) - - if accepted_type_origin is Union: - if provided_type_origin is not Union: - return any( - typehint_issubclass( - possible_subclass, - accepted_arg, - treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, - treat_literals_as_union_of_types=treat_literals_as_union_of_types, - treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, - ) - for accepted_arg in accepted_args - ) - return all( - any( - typehint_issubclass( - provided_arg, - accepted_arg, - treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, - treat_literals_as_union_of_types=treat_literals_as_union_of_types, - treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, - ) - for accepted_arg in accepted_args - ) - for provided_arg in provided_args - ) - if provided_type_origin is Union: - return all( - typehint_issubclass( - provided_arg, - possible_superclass, - treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, - treat_literals_as_union_of_types=treat_literals_as_union_of_types, - treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, - ) - for provided_arg in provided_args - ) - - provided_type_origin = provided_type_origin or possible_subclass - accepted_type_origin = accepted_type_origin or possible_superclass - - if treat_mutable_superclasss_as_immutable: - if accepted_type_origin is dict: - accepted_type_origin = Mapping - elif accepted_type_origin is list or accepted_type_origin is set: - accepted_type_origin = Sequence - - # Check if the origin of both types is the same (e.g., list for list[int]) - if not safe_issubclass( - provided_type_origin or possible_subclass, - accepted_type_origin or possible_superclass, - ): - return False - - # Ensure all specific types are compatible with accepted types - # Note this is not necessarily correct, as it doesn't check against contravariance and covariance - # It also ignores when the length of the arguments is different - return all( - typehint_issubclass( - provided_arg, - accepted_arg, - treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable, - treat_literals_as_union_of_types=treat_literals_as_union_of_types, - treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything, - ) - for provided_arg, accepted_arg in zip( - provided_args, accepted_args, strict=False - ) - if accepted_arg is not Any - ) - - -def resolve_annotations( - raw_annotations: Mapping[str, type[Any]], module_name: str | None -) -> dict[str, type[Any]]: - """Partially taken from typing.get_type_hints. - - Resolve string or ForwardRef annotations into type objects if possible. - - Args: - raw_annotations: The raw annotations to resolve. - module_name: The name of the module. - - Returns: - The resolved annotations. - """ - module = sys.modules.get(module_name, None) if module_name is not None else None - - base_globals: dict[str, Any] | None = ( - module.__dict__ if module is not None else None - ) - - annotations = {} - for name, value in raw_annotations.items(): - if isinstance(value, str): - if sys.version_info == (3, 10, 0): - value = ForwardRef(value, is_argument=False) - else: - value = ForwardRef(value, is_argument=False, is_class=True) - try: - if sys.version_info >= (3, 13): - value = _eval_type(value, base_globals, None, type_params=()) - else: - value = _eval_type(value, base_globals, None) - except NameError: - # this is ok, it can be fixed with update_forward_refs - pass - annotations[name] = value - return annotations - - -TYPES_THAT_HAS_DEFAULT_VALUE = (int, float, tuple, list, set, dict, str) - - -def get_default_value_for_type(t: GenericType) -> Any: - """Get the default value of the var. - - Args: - t: The type of the var. - - Returns: - The default value of the var, if it has one, else None. - - Raises: - ImportError: If the var is a dataframe and pandas is not installed. - """ - if is_optional(t): - return None - - origin = get_origin(t) if is_generic_alias(t) else t - if origin is Literal: - args = get_args(t) - return args[0] if args else None - if safe_issubclass(origin, TYPES_THAT_HAS_DEFAULT_VALUE): - return origin() - if safe_issubclass(origin, Mapping): - return {} - if is_dataframe(origin): - try: - import pandas as pd - - return pd.DataFrame() - except ImportError as e: - msg = "Please install pandas to use dataframes in your app." - raise ImportError(msg) from e - return None - - -IMMUTABLE_TYPES = ( - int, - float, - bool, - str, - bytes, - frozenset, - tuple, - type(None), - Enum, -) - - -def is_immutable(i: Any) -> bool: - """Check if a value is immutable. - - Args: - i: The value to check. - - Returns: - Whether the value is immutable. - """ - return isinstance(i, IMMUTABLE_TYPES) +from reflex_core.utils.types import * diff --git a/reflex/vars/__init__.py b/reflex/vars/__init__.py index c1989e5733d..6aa4b67a6c1 100644 --- a/reflex/vars/__init__.py +++ b/reflex/vars/__init__.py @@ -1,59 +1,4 @@ """Immutable-Based Var System.""" -from .base import ( - BaseStateMeta, - EvenMoreBasicBaseState, - Field, - LiteralVar, - Var, - VarData, - field, - get_unique_variable_name, - get_uuid_string_var, - var_operation, - var_operation_return, -) -from .color import ColorVar, LiteralColorVar -from .datetime import DateTimeVar -from .function import FunctionStringVar, FunctionVar, VarOperationCall -from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar -from .object import LiteralObjectVar, ObjectVar, RestProp -from .sequence import ( - ArrayVar, - ConcatVarOperation, - LiteralArrayVar, - LiteralStringVar, - StringVar, -) - -__all__ = [ - "ArrayVar", - "BaseStateMeta", - "BooleanVar", - "ColorVar", - "ConcatVarOperation", - "DateTimeVar", - "EvenMoreBasicBaseState", - "Field", - "FunctionStringVar", - "FunctionVar", - "LiteralArrayVar", - "LiteralBooleanVar", - "LiteralColorVar", - "LiteralNumberVar", - "LiteralObjectVar", - "LiteralStringVar", - "LiteralVar", - "NumberVar", - "ObjectVar", - "RestProp", - "StringVar", - "Var", - "VarData", - "VarOperationCall", - "field", - "get_unique_variable_name", - "get_uuid_string_var", - "var_operation", - "var_operation_return", -] +from reflex_core.vars import * +from reflex_core.vars import __all__ diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 9f51b7c4d90..45dd044f98a 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -1,3717 +1,3 @@ -"""Collection of base classes.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import contextlib -import copy -import dataclasses -import datetime -import functools -import inspect -import json -import re -import string -import uuid -import warnings -from abc import ABCMeta -from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence -from dataclasses import _MISSING_TYPE, MISSING -from decimal import Decimal -from types import CodeType, FunctionType -from typing import ( - TYPE_CHECKING, - Annotated, - Any, - ClassVar, - Generic, - Literal, - NoReturn, - ParamSpec, - Protocol, - TypeGuard, - TypeVar, - cast, - get_args, - get_type_hints, - overload, -) - -from rich.markup import escape -from typing_extensions import dataclass_transform, override - -from reflex import constants -from reflex.constants.compiler import Hooks -from reflex.constants.state import FIELD_MARKER -from reflex.utils import console, exceptions, imports, serializers, types -from reflex.utils.compat import annotations_from_namespace -from reflex.utils.decorator import once -from reflex.utils.exceptions import ( - ComputedVarSignatureError, - UntypedComputedVarError, - VarAttributeError, - VarDependencyError, - VarTypeError, -) -from reflex.utils.format import format_state_name -from reflex.utils.imports import ( - ImmutableImportDict, - ImmutableParsedImportDict, - ImportDict, - ImportVar, - ParsedImportTuple, - parse_imports, -) -from reflex.utils.types import ( - GenericType, - Self, - _isinstance, - get_origin, - has_args, - safe_issubclass, - unionize, -) - -if TYPE_CHECKING: - from reflex.components.component import BaseComponent - from reflex.constants.colors import Color - from reflex.state import BaseState - - from .color import LiteralColorVar - from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar - from .object import LiteralObjectVar, ObjectVar - from .sequence import ArrayVar, LiteralArrayVar, LiteralStringVar, StringVar - - -VAR_TYPE = TypeVar("VAR_TYPE", covariant=True) -OTHER_VAR_TYPE = TypeVar("OTHER_VAR_TYPE") -STRING_T = TypeVar("STRING_T", bound=str) -SEQUENCE_TYPE = TypeVar("SEQUENCE_TYPE", bound=Sequence) - -warnings.filterwarnings("ignore", message="fields may not start with an underscore") - -_PYDANTIC_VALIDATE_VALUES = "__pydantic_validate_values__" - - -def _pydantic_validator(*args, **kwargs): - return None - - -@dataclasses.dataclass( - eq=False, - frozen=True, -) -class VarSubclassEntry: - """Entry for a Var subclass.""" - - var_subclass: type[Var] - to_var_subclass: type[ToOperation] - python_types: tuple[GenericType, ...] - - -_var_subclasses: list[VarSubclassEntry] = [] -_var_literal_subclasses: list[tuple[type[LiteralVar], VarSubclassEntry]] = [] - - -@dataclasses.dataclass( - eq=True, - frozen=True, -) -class VarData: - """Metadata associated with a x.""" - - # The name of the enclosing state. - state: str = dataclasses.field(default="") - - # The name of the field in the state. - field_name: str = dataclasses.field(default="") - - # Imports needed to render this var - imports: ParsedImportTuple = dataclasses.field(default_factory=tuple) - - # Hooks that need to be present in the component to render this var - hooks: tuple[str, ...] = dataclasses.field(default_factory=tuple) - - # Dependencies of the var - deps: tuple[Var, ...] = dataclasses.field(default_factory=tuple) - - # Position of the hook in the component - position: Hooks.HookPosition | None = None - - # Components that are part of this var - components: tuple[BaseComponent, ...] = dataclasses.field(default_factory=tuple) - - def __init__( - self, - state: str = "", - field_name: str = "", - imports: ImmutableImportDict | ImmutableParsedImportDict | None = None, - hooks: Mapping[str, VarData | None] | Sequence[str] | str | None = None, - deps: list[Var] | None = None, - position: Hooks.HookPosition | None = None, - components: Iterable[BaseComponent] | None = None, - ): - """Initialize the var data. - - Args: - state: The name of the enclosing state. - field_name: The name of the field in the state. - imports: Imports needed to render this var. - hooks: Hooks that need to be present in the component to render this var. - deps: Dependencies of the var for useCallback. - position: Position of the hook in the component. - components: Components that are part of this var. - """ - if isinstance(hooks, str): - hooks = [hooks] - if not isinstance(hooks, dict): - hooks = dict.fromkeys(hooks or []) - immutable_imports: ParsedImportTuple = tuple( - (k, tuple(v)) for k, v in parse_imports(imports or {}).items() - ) - object.__setattr__(self, "state", state) - object.__setattr__(self, "field_name", field_name) - object.__setattr__(self, "imports", immutable_imports) - object.__setattr__(self, "hooks", tuple(hooks or {})) - object.__setattr__(self, "deps", tuple(deps or [])) - object.__setattr__(self, "position", position or None) - object.__setattr__(self, "components", tuple(components or [])) - - if hooks and any(hooks.values()): - # Merge our dependencies first, so they can be referenced. - merged_var_data = VarData.merge(*hooks.values(), self) - if merged_var_data is not None: - object.__setattr__(self, "state", merged_var_data.state) - object.__setattr__(self, "field_name", merged_var_data.field_name) - object.__setattr__(self, "imports", merged_var_data.imports) - object.__setattr__(self, "hooks", merged_var_data.hooks) - object.__setattr__(self, "deps", merged_var_data.deps) - object.__setattr__(self, "position", merged_var_data.position) - object.__setattr__(self, "components", merged_var_data.components) - - def old_school_imports(self) -> ImportDict: - """Return the imports as a mutable dict. - - Returns: - The imports as a mutable dict. - """ - return {k: list(v) for k, v in self.imports} - - def merge(*all: VarData | None) -> VarData | None: - """Merge multiple var data objects. - - Args: - *all: The var data objects to merge. - - Returns: - The merged var data object. - - Raises: - ReflexError: If trying to merge VarData with different positions. - - # noqa: DAR102 *all - """ - all_var_datas = list(filter(None, all)) - - if not all_var_datas: - return None - - if len(all_var_datas) == 1: - return all_var_datas[0] - - # Get the first non-empty field name or default to empty string. - field_name = next( - (var_data.field_name for var_data in all_var_datas if var_data.field_name), - "", - ) - - # Get the first non-empty state or default to empty string. - state = next( - (var_data.state for var_data in all_var_datas if var_data.state), "" - ) - - hooks: dict[str, VarData | None] = { - hook: None for var_data in all_var_datas for hook in var_data.hooks - } - - imports_ = imports.merge_imports( - *(var_data.imports for var_data in all_var_datas) - ) - - deps = [dep for var_data in all_var_datas for dep in var_data.deps] - - positions = list( - dict.fromkeys( - var_data.position - for var_data in all_var_datas - if var_data.position is not None - ) - ) - if positions: - if len(positions) > 1: - msg = f"Cannot merge var data with different positions: {positions}" - raise exceptions.ReflexError(msg) - position = positions[0] - else: - position = None - - components = tuple( - component for var_data in all_var_datas for component in var_data.components - ) - - return VarData( - state=state, - field_name=field_name, - imports=imports_, - hooks=hooks, - deps=deps, - position=position, - components=components, - ) - - def __bool__(self) -> bool: - """Check if the var data is non-empty. - - Returns: - True if any field is set to a non-default value. - """ - return bool( - self.state - or self.imports - or self.hooks - or self.field_name - or self.deps - or self.position - or self.components - ) - - @classmethod - def from_state(cls, state: type[BaseState] | str, field_name: str = "") -> VarData: - """Set the state of the var. - - Args: - state: The state to set or the full name of the state. - field_name: The name of the field in the state. Optional. - - Returns: - The var with the set state. - """ - from reflex.utils import format - - state_name = state if isinstance(state, str) else state.get_full_name() - return VarData( - state=state_name, - field_name=field_name, - hooks={ - "const {0} = useContext(StateContexts.{0})".format( - format.format_state_name(state_name) - ): None - }, - imports={ - f"$/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="StateContexts")], - "react": [ImportVar(tag="useContext")], - }, - ) - - -def _decode_var_immutable(value: str) -> tuple[VarData | None, str]: - """Decode the state name from a formatted var. - - Args: - value: The value to extract the state name from. - - Returns: - The extracted state name and the value without the state name. - """ - var_datas = [] - if isinstance(value, str): - # fast path if there is no encoded VarData - if constants.REFLEX_VAR_OPENING_TAG not in value: - return None, value - - offset = 0 - - # Find all tags. - while m := _decode_var_pattern.search(value): - start, end = m.span() - value = value[:start] + value[end:] - - serialized_data = m.group(1) - - if serialized_data.isnumeric() or ( - serialized_data[0] == "-" and serialized_data[1:].isnumeric() - ): - # This is a global immutable var. - var = _global_vars[int(serialized_data)] - var_data = var._get_all_var_data() - - if var_data is not None: - var_datas.append(var_data) - offset += end - start - - return VarData.merge(*var_datas) if var_datas else None, value - - -def can_use_in_object_var(cls: GenericType) -> bool: - """Check if the class can be used in an ObjectVar. - - Args: - cls: The class to check. - - Returns: - Whether the class can be used in an ObjectVar. - """ - if types.is_union(cls): - return all(can_use_in_object_var(t) for t in types.get_args(cls)) - return ( - isinstance(cls, type) - and not safe_issubclass(cls, Var) - and serializers.can_serialize(cls, dict) - ) - - -class MetaclassVar(type): - """Metaclass for the Var class.""" - - def __setattr__(cls, name: str, value: Any): - """Set an attribute on the class. - - Args: - name: The name of the attribute. - value: The value of the attribute. - """ - super().__setattr__( - name, value if name != _PYDANTIC_VALIDATE_VALUES else _pydantic_validator - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, -) -class Var(Generic[VAR_TYPE], metaclass=MetaclassVar): - """Base class for immutable vars.""" - - # The name of the var. - _js_expr: str = dataclasses.field() - - # The type of the var. - _var_type: types.GenericType = dataclasses.field(default=Any) - - # Extra metadata associated with the Var - _var_data: VarData | None = dataclasses.field(default=None) - - def __str__(self) -> str: - """String representation of the var. Guaranteed to be a valid Javascript expression. - - Returns: - The name of the var. - """ - return self._js_expr - - @property - def _var_is_local(self) -> bool: - """Whether this is a local javascript variable. - - Returns: - False - """ - return False - - @property - def _var_is_string(self) -> bool: - """Whether the var is a string literal. - - Returns: - False - """ - return False - - def __init_subclass__( - cls, - python_types: tuple[GenericType, ...] | GenericType = types.Unset(), - default_type: GenericType = types.Unset(), - **kwargs, - ): - """Initialize the subclass. - - Args: - python_types: The python types that the var represents. - default_type: The default type of the var. Defaults to the first python type. - **kwargs: Additional keyword arguments. - """ - super().__init_subclass__(**kwargs) - - if python_types or default_type: - python_types = ( - (python_types if isinstance(python_types, tuple) else (python_types,)) - if python_types - else () - ) - - default_type = default_type or (python_types[0] if python_types else Any) - - @dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, - ) - class ToVarOperation(ToOperation, cls): - """Base class of converting a var to another var type.""" - - _original: Var = dataclasses.field( - default=Var(_js_expr="null", _var_type=None), - ) - - _default_var_type: ClassVar[GenericType] = default_type - - new_to_var_operation_name = f"{cls.__name__.removesuffix('Var')}CastedVar" - ToVarOperation.__qualname__ = ( - ToVarOperation.__qualname__.removesuffix(ToVarOperation.__name__) - + new_to_var_operation_name - ) - ToVarOperation.__name__ = new_to_var_operation_name - - _var_subclasses.append(VarSubclassEntry(cls, ToVarOperation, python_types)) - - def __post_init__(self): - """Post-initialize the var. - - Raises: - TypeError: If _js_expr is not a string. - """ - if not isinstance(self._js_expr, str): - msg = f"Expected _js_expr to be a string, got value {self._js_expr!r} of type {type(self._js_expr).__name__}" - raise TypeError(msg) - - if self._var_data is not None and not isinstance(self._var_data, VarData): - msg = f"Expected _var_data to be a VarData, got value {self._var_data!r} of type {type(self._var_data).__name__}" - raise TypeError(msg) - - # Decode any inline Var markup and apply it to the instance - var_data_, js_expr_ = _decode_var_immutable(self._js_expr) - - if var_data_ or js_expr_ != self._js_expr: - self.__init__( - _js_expr=js_expr_, - _var_type=self._var_type, - _var_data=VarData.merge(self._var_data, var_data_), - ) - - def __hash__(self) -> int: - """Define a hash function for the var. - - Returns: - The hash of the var. - """ - return hash((self._js_expr, self._var_type, self._var_data)) - - def _get_all_var_data(self) -> VarData | None: - """Get all VarData associated with the Var. - - Returns: - The VarData of the components and all of its children. - """ - return self._var_data - - def __deepcopy__(self, memo: dict[int, Any]) -> Self: - """Deepcopy the var. - - Args: - memo: The memo dictionary to use for the deepcopy. - - Returns: - A deepcopy of the var. - """ - return self - - def equals(self, other: Var) -> bool: - """Check if two vars are equal. - - Args: - other: The other var to compare. - - Returns: - Whether the vars are equal. - """ - return ( - self._js_expr == other._js_expr - and self._var_type == other._var_type - and self._get_all_var_data() == other._get_all_var_data() - ) - - @overload - def _replace( - self, - _var_type: type[OTHER_VAR_TYPE], - merge_var_data: VarData | None = None, - **kwargs: Any, - ) -> Var[OTHER_VAR_TYPE]: ... - - @overload - def _replace( - self, - _var_type: GenericType | None = None, - merge_var_data: VarData | None = None, - **kwargs: Any, - ) -> Self: ... - - def _replace( - self, - _var_type: GenericType | None = None, - merge_var_data: VarData | None = None, - **kwargs: Any, - ) -> Self | Var: - """Make a copy of this Var with updated fields. - - Args: - _var_type: The new type of the Var. - merge_var_data: VarData to merge into the existing VarData. - **kwargs: Var fields to update. - - Returns: - A new Var with the updated fields overwriting the corresponding fields in this Var. - - Raises: - TypeError: If _var_is_local, _var_is_string, or _var_full_name_needs_state_prefix is not None. - """ - if kwargs.get("_var_is_local", False) is not False: - msg = "The _var_is_local argument is not supported for Var." - raise TypeError(msg) - - if kwargs.get("_var_is_string", False) is not False: - msg = "The _var_is_string argument is not supported for Var." - raise TypeError(msg) - - if kwargs.get("_var_full_name_needs_state_prefix", False) is not False: - msg = "The _var_full_name_needs_state_prefix argument is not supported for Var." - raise TypeError(msg) - value_with_replaced = dataclasses.replace( - self, - _var_type=_var_type or self._var_type, - _var_data=VarData.merge( - kwargs.get("_var_data", self._var_data), merge_var_data - ), - **kwargs, - ) - - if (js_expr := kwargs.get("_js_expr")) is not None: - object.__setattr__(value_with_replaced, "_js_expr", js_expr) - - return value_with_replaced - - @overload - @classmethod - def create( # pyright: ignore[reportOverlappingOverload] - cls, - value: NoReturn, - _var_data: VarData | None = None, - ) -> Var[Any]: ... - - @overload - @classmethod - def create( # pyright: ignore[reportOverlappingOverload] - cls, - value: bool, - _var_data: VarData | None = None, - ) -> LiteralBooleanVar: ... - - @overload - @classmethod - def create( - cls, - value: int, - _var_data: VarData | None = None, - ) -> LiteralNumberVar[int]: ... - - @overload - @classmethod - def create( - cls, - value: float, - _var_data: VarData | None = None, - ) -> LiteralNumberVar[float]: ... - - @overload - @classmethod - def create( - cls, - value: Decimal, - _var_data: VarData | None = None, - ) -> LiteralNumberVar[Decimal]: ... - - @overload - @classmethod - def create( # pyright: ignore [reportOverlappingOverload] - cls, - value: Color, - _var_data: VarData | None = None, - ) -> LiteralColorVar: ... - - @overload - @classmethod - def create( # pyright: ignore [reportOverlappingOverload] - cls, - value: str, - _var_data: VarData | None = None, - ) -> LiteralStringVar: ... - - @overload - @classmethod - def create( # pyright: ignore [reportOverlappingOverload] - cls, - value: STRING_T, - _var_data: VarData | None = None, - ) -> StringVar[STRING_T]: ... - - @overload - @classmethod - def create( # pyright: ignore[reportOverlappingOverload] - cls, - value: None, - _var_data: VarData | None = None, - ) -> LiteralNoneVar: ... - - @overload - @classmethod - def create( - cls, - value: MAPPING_TYPE, - _var_data: VarData | None = None, - ) -> LiteralObjectVar[MAPPING_TYPE]: ... - - @overload - @classmethod - def create( - cls, - value: SEQUENCE_TYPE, - _var_data: VarData | None = None, - ) -> LiteralArrayVar[SEQUENCE_TYPE]: ... - - @overload - @classmethod - def create( - cls, - value: OTHER_VAR_TYPE, - _var_data: VarData | None = None, - ) -> Var[OTHER_VAR_TYPE]: ... - - @classmethod - def create( - cls, - value: OTHER_VAR_TYPE, - _var_data: VarData | None = None, - ) -> Var[OTHER_VAR_TYPE]: - """Create a var from a value. - - Args: - value: The value to create the var from. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - # If the value is already a var, do nothing. - if isinstance(value, Var): - return value - - return LiteralVar.create(value, _var_data=_var_data) - - def __format__(self, format_spec: str) -> str: - """Format the var into a Javascript equivalent to an f-string. - - Args: - format_spec: The format specifier (Ignored for now). - - Returns: - The formatted var. - """ - hashed_var = hash(self) - - _global_vars[hashed_var] = self - - # Encode the _var_data into the formatted output for tracking purposes. - return f"{constants.REFLEX_VAR_OPENING_TAG}{hashed_var}{constants.REFLEX_VAR_CLOSING_TAG}{self._js_expr}" - - @overload - def to(self, output: type[str]) -> StringVar: ... # pyright: ignore[reportOverlappingOverload] - - @overload - def to(self, output: type[bool]) -> BooleanVar: ... - - @overload - def to(self, output: type[int]) -> NumberVar[int]: ... - - @overload - def to(self, output: type[float]) -> NumberVar[float]: ... - - @overload - def to(self, output: type[Decimal]) -> NumberVar[Decimal]: ... - - @overload - def to( - self, - output: type[SEQUENCE_TYPE], - ) -> ArrayVar[SEQUENCE_TYPE]: ... - - @overload - def to( - self, - output: type[MAPPING_TYPE], - ) -> ObjectVar[MAPPING_TYPE]: ... - - @overload - def to( - self, output: type[ObjectVar], var_type: type[VAR_INSIDE] - ) -> ObjectVar[VAR_INSIDE]: ... - - @overload - def to( - self, output: type[ObjectVar], var_type: None = None - ) -> ObjectVar[VAR_TYPE]: ... - - @overload - def to(self, output: VAR_SUBCLASS, var_type: None = None) -> VAR_SUBCLASS: ... - - @overload - def to( - self, - output: type[OUTPUT] | types.GenericType, - var_type: types.GenericType | None = None, - ) -> OUTPUT: ... - - def to( - self, - output: type[OUTPUT] | types.GenericType, - var_type: types.GenericType | None = None, - ) -> Var: - """Convert the var to a different type. - - Args: - output: The output type. - var_type: The type of the var. - - Returns: - The converted var. - """ - from .object import ObjectVar - - fixed_output_type = get_origin(output) or output - - # If the first argument is a python type, we map it to the corresponding Var type. - for var_subclass in _var_subclasses[::-1]: - if fixed_output_type in var_subclass.python_types or safe_issubclass( - fixed_output_type, var_subclass.python_types - ): - return self.to(var_subclass.var_subclass, output) - - if fixed_output_type is None: - return get_to_operation(NoneVar).create(self) # pyright: ignore [reportReturnType] - - # Handle fixed_output_type being Base or a dataclass. - if can_use_in_object_var(output): - return self.to(ObjectVar, output) - - if isinstance(output, type): - for var_subclass in _var_subclasses[::-1]: - if safe_issubclass(output, var_subclass.var_subclass): - current_var_type = self._var_type - if current_var_type is Any: - new_var_type = var_type - else: - new_var_type = var_type or current_var_type - return var_subclass.to_var_subclass.create( # pyright: ignore [reportReturnType] - value=self, _var_type=new_var_type - ) - - # If we can't determine the first argument, we just replace the _var_type. - if not safe_issubclass(output, Var) or var_type is None: - return dataclasses.replace( - self, - _var_type=output, - ) - - # We couldn't determine the output type to be any other Var type, so we replace the _var_type. - if var_type is not None: - return dataclasses.replace( - self, - _var_type=var_type, - ) - - return self - - @overload - def guess_type(self: Var[NoReturn]) -> Var[Any]: ... # pyright: ignore [reportOverlappingOverload] - - @overload - def guess_type(self: Var[str]) -> StringVar: ... - - @overload - def guess_type(self: Var[bool]) -> BooleanVar: ... - - @overload - def guess_type(self: Var[int] | Var[float] | Var[int | float]) -> NumberVar: ... - - @overload - def guess_type(self: Var[BASE_TYPE]) -> ObjectVar[BASE_TYPE]: ... - - @overload - def guess_type(self) -> Self: ... - - def guess_type(self) -> Var: - """Guesses the type of the variable based on its `_var_type` attribute. - - Returns: - Var: The guessed type of the variable. - - Raises: - TypeError: If the type is not supported for guessing. - """ - from .object import ObjectVar - - var_type = self._var_type - if var_type is None: - return self.to(None) - if var_type is NoReturn: - return self.to(Any) - - var_type = types.value_inside_optional(var_type) - - if var_type is Any: - return self - - fixed_type = get_origin(var_type) or var_type - - if fixed_type in types.UnionTypes: - inner_types = get_args(var_type) - non_optional_inner_types = [ - types.value_inside_optional(inner_type) for inner_type in inner_types - ] - fixed_inner_types = [ - get_origin(inner_type) or inner_type - for inner_type in non_optional_inner_types - ] - - for var_subclass in _var_subclasses[::-1]: - if all( - safe_issubclass(t, var_subclass.python_types) - for t in fixed_inner_types - ): - return self.to(var_subclass.var_subclass, self._var_type) - - if can_use_in_object_var(var_type): - return self.to(ObjectVar, self._var_type) - - return self - - if fixed_type is Literal: - args = get_args(var_type) - fixed_type = unionize(*(type(arg) for arg in args)) - - if not isinstance(fixed_type, type): - msg = f"Unsupported type {var_type} for guess_type." - raise TypeError(msg) - - if fixed_type is None: - return self.to(None) - - for var_subclass in _var_subclasses[::-1]: - if safe_issubclass(fixed_type, var_subclass.python_types): - return self.to(var_subclass.var_subclass, self._var_type) - - if can_use_in_object_var(fixed_type): - return self.to(ObjectVar, self._var_type) - - return self - - @staticmethod - def _get_setter_name_for_name( - name: str, - ) -> str: - """Get the name of the var's generated setter function. - - Args: - name: The name of the var. - - Returns: - The name of the setter function. - """ - return constants.SETTER_PREFIX + name - - def _get_setter(self, name: str) -> Callable[[BaseState, Any], None]: - """Get the var's setter function. - - Args: - name: The name of the var. - - Returns: - A function that that creates a setter for the var. - """ - setter_name = Var._get_setter_name_for_name(name) - - def setter(state: Any, value: Any): - """Get the setter for the var. - - Args: - state: The state within which we add the setter function. - value: The value to set. - """ - if self._var_type in [int, float]: - try: - value = self._var_type(value) - setattr(state, name, value) - except ValueError: - console.debug( - f"{type(state).__name__}.{self._js_expr}: Failed conversion of {value!s} to '{self._var_type.__name__}'. Value not set.", - ) - else: - setattr(state, name, value) - - setter.__annotations__["value"] = self._var_type - - setter.__qualname__ = setter_name - - return setter - - def _var_set_state(self, state: type[BaseState] | str) -> Self: - """Set the state of the var. - - Args: - state: The state to set. - - Returns: - The var with the state set. - """ - formatted_state_name = ( - state - if isinstance(state, str) - else format_state_name(state.get_full_name()) - ) - - return StateOperation.create( # pyright: ignore [reportReturnType] - formatted_state_name, - self, - _var_data=VarData.merge( - VarData.from_state(state, self._js_expr), self._var_data - ), - ).guess_type() - - def __eq__(self, other: Var | Any) -> BooleanVar: - """Check if the current variable is equal to the given variable. - - Args: - other (Var | Any): The variable to compare with. - - Returns: - BooleanVar: A BooleanVar object representing the result of the equality check. - """ - from .number import equal_operation - - return equal_operation(self, other) - - def __ne__(self, other: Var | Any) -> BooleanVar: - """Check if the current object is not equal to the given object. - - Parameters: - other (Var | Any): The object to compare with. - - Returns: - BooleanVar: A BooleanVar object representing the result of the comparison. - """ - from .number import equal_operation - - return ~equal_operation(self, other) - - def bool(self) -> BooleanVar: - """Convert the var to a boolean. - - Returns: - The boolean var. - """ - from .number import boolify - - return boolify(self) - - def is_none(self) -> BooleanVar: - """Check if the var is None. - - Returns: - A BooleanVar object representing the result of the check. - """ - from .number import is_not_none_operation - - return ~is_not_none_operation(self) - - def is_not_none(self) -> BooleanVar: - """Check if the var is not None. - - Returns: - A BooleanVar object representing the result of the check. - """ - from .number import is_not_none_operation - - return is_not_none_operation(self) - - def __and__( - self, other: Var[OTHER_VAR_TYPE] | Any - ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: - """Perform a logical AND operation on the current instance and another variable. - - Args: - other: The variable to perform the logical AND operation with. - - Returns: - A `BooleanVar` object representing the result of the logical AND operation. - """ - return and_operation(self, other) - - def __rand__( - self, other: Var[OTHER_VAR_TYPE] | Any - ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: - """Perform a logical AND operation on the current instance and another variable. - - Args: - other: The variable to perform the logical AND operation with. - - Returns: - A `BooleanVar` object representing the result of the logical AND operation. - """ - return and_operation(other, self) - - def __or__( - self, other: Var[OTHER_VAR_TYPE] | Any - ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: - """Perform a logical OR operation on the current instance and another variable. - - Args: - other: The variable to perform the logical OR operation with. - - Returns: - A `BooleanVar` object representing the result of the logical OR operation. - """ - return or_operation(self, other) - - def __ror__( - self, other: Var[OTHER_VAR_TYPE] | Any - ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: - """Perform a logical OR operation on the current instance and another variable. - - Args: - other: The variable to perform the logical OR operation with. - - Returns: - A `BooleanVar` object representing the result of the logical OR operation. - """ - return or_operation(other, self) - - def __invert__(self) -> BooleanVar: - """Perform a logical NOT operation on the current instance. - - Returns: - A `BooleanVar` object representing the result of the logical NOT operation. - """ - return ~self.bool() - - def to_string(self, use_json: bool = True) -> StringVar: - """Convert the var to a string. - - Args: - use_json: Whether to use JSON stringify. If False, uses Object.prototype.toString. - - Returns: - The string var. - """ - from .function import JSON_STRINGIFY, PROTOTYPE_TO_STRING - from .sequence import StringVar - - return ( - JSON_STRINGIFY.call(self).to(StringVar) - if use_json - else PROTOTYPE_TO_STRING.call(self).to(StringVar) - ) - - def _as_ref(self) -> Var: - """Get a reference to the var. - - Returns: - The reference to the var. - """ - return Var( - _js_expr=f"refs[{Var.create(str(self))}]", - _var_data=VarData( - imports={ - f"$/{constants.Dirs.STATE_PATH}": [imports.ImportVar(tag="refs")] - } - ), - ).to(str) - - def js_type(self) -> StringVar: - """Returns the javascript type of the object. - - This method uses the `typeof` function from the `FunctionStringVar` class - to determine the type of the object. - - Returns: - StringVar: A string variable representing the type of the object. - """ - from .function import FunctionStringVar - from .sequence import StringVar - - type_of = FunctionStringVar("typeof") - return type_of.call(self).to(StringVar) - - def _without_data(self): - """Create a copy of the var without the data. - - Returns: - The var without the data. - """ - return dataclasses.replace(self, _var_data=None) - - def _decode(self) -> Any: - """Decode Var as a python value. - - Note that Var with state set cannot be decoded python-side and will be - returned as full_name. - - Returns: - The decoded value or the Var name. - """ - if isinstance(self, LiteralVar): - return self._var_value - try: - return json.loads(str(self)) - except ValueError: - return str(self) - - @property - def _var_state(self) -> str: - """Compat method for getting the state. - - Returns: - The state name associated with the var. - """ - var_data = self._get_all_var_data() - return var_data.state if var_data else "" - - @overload - @classmethod - def range(cls, stop: int | NumberVar, /) -> ArrayVar[Sequence[int]]: ... - - @overload - @classmethod - def range( - cls, - start: int | NumberVar, - end: int | NumberVar, - step: int | NumberVar = 1, - /, - ) -> ArrayVar[Sequence[int]]: ... - - @classmethod - def range( - cls, - first_endpoint: int | NumberVar, - second_endpoint: int | NumberVar | None = None, - step: int | NumberVar | None = None, - ) -> ArrayVar[Sequence[int]]: - """Create a range of numbers. - - Args: - first_endpoint: The end of the range if second_endpoint is not provided, otherwise the start of the range. - second_endpoint: The end of the range. - step: The step of the range. - - Returns: - The range of numbers. - """ - from .sequence import ArrayVar - - return ArrayVar.range(first_endpoint, second_endpoint, step) - - if not TYPE_CHECKING: - - def __getitem__(self, key: Any) -> Var: - """Get the item from the var. - - Args: - key: The key to get. - - Raises: - UntypedVarError: If the var type is Any. - TypeError: If the var type is Any. - - # noqa: DAR101 self - """ - if self._var_type is Any: - raise exceptions.UntypedVarError( - self, - f"access the item '{key}'", - ) - msg = f"Var of type {self._var_type} does not support item access." - raise TypeError(msg) - - def __getattr__(self, name: str): - """Get an attribute of the var. - - Args: - name: The name of the attribute. - - Raises: - VarAttributeError: If the attribute does not exist. - UntypedVarError: If the var type is Any. - TypeError: If the var type is Any. - - # noqa: DAR101 self - """ - if name.startswith("_"): - msg = f"Attribute {name} not found." - raise VarAttributeError(msg) - - if name == "contains": - msg = f"Var of type {self._var_type} does not support contains check." - raise TypeError(msg) - if name == "reverse": - msg = "Cannot reverse non-list var." - raise TypeError(msg) - - if self._var_type is Any: - raise exceptions.UntypedVarError( - self, - f"access the attribute '{name}'", - ) - - msg = f"The State var {escape(self._js_expr)} of type {escape(str(self._var_type))} has no attribute '{name}' or may have been annotated wrongly." - raise VarAttributeError(msg) - - def __bool__(self) -> bool: - """Raise exception if using Var in a boolean context. - - Raises: - VarTypeError: when attempting to bool-ify the Var. - - # noqa: DAR101 self - """ - msg = ( - f"Cannot convert Var {str(self)!r} to bool for use with `if`, `and`, `or`, and `not`. " - "Instead use `rx.cond` and bitwise operators `&` (and), `|` (or), `~` (invert)." - ) - raise VarTypeError(msg) - - def __iter__(self) -> Any: - """Raise exception if using Var in an iterable context. - - Raises: - VarTypeError: when attempting to iterate over the Var. - - # noqa: DAR101 self - """ - msg = f"Cannot iterate over Var {str(self)!r}. Instead use `rx.foreach`." - raise VarTypeError(msg) - - def __contains__(self, _: Any) -> Var: - """Override the 'in' operator to alert the user that it is not supported. - - Raises: - VarTypeError: the operation is not supported - - # noqa: DAR101 self - """ - msg = ( - "'in' operator not supported for Var types, use Var.contains() instead." - ) - raise VarTypeError(msg) - - -OUTPUT = TypeVar("OUTPUT", bound=Var) - -VAR_SUBCLASS = TypeVar("VAR_SUBCLASS", bound=Var) -VAR_INSIDE = TypeVar("VAR_INSIDE") - - -class ToOperation: - """A var operation that converts a var to another type.""" - - def __getattr__(self, name: str) -> Any: - """Get an attribute of the var. - - Args: - name: The name of the attribute. - - Returns: - The attribute of the var. - """ - from .object import ObjectVar - - if isinstance(self, ObjectVar) and name != "_js_expr": - return ObjectVar.__getattr__(self, name) - return getattr(self._original, name) - - def __post_init__(self): - """Post initialization.""" - object.__delattr__(self, "_js_expr") - - def __hash__(self) -> int: - """Calculate the hash value of the object. - - Returns: - int: The hash value of the object. - """ - return hash(self._original) - - def _get_all_var_data(self) -> VarData | None: - """Get all the var data. - - Returns: - The var data. - """ - return VarData.merge( - self._original._get_all_var_data(), - self._var_data, - ) - - @classmethod - def create( - cls, - value: Var, - _var_type: GenericType | None = None, - _var_data: VarData | None = None, - ): - """Create a ToOperation. - - Args: - value: The value of the var. - _var_type: The type of the Var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The ToOperation. - """ - return cls( - _js_expr="", # pyright: ignore [reportCallIssue] - _var_data=_var_data, # pyright: ignore [reportCallIssue] - _var_type=_var_type or cls._default_var_type, # pyright: ignore [reportCallIssue, reportAttributeAccessIssue] - _original=value, # pyright: ignore [reportCallIssue] - ) - - -class LiteralVar(Var): - """Base class for immutable literal vars.""" - - def __init_subclass__(cls, **kwargs): - """Initialize the subclass. - - Args: - **kwargs: Additional keyword arguments. - - Raises: - TypeError: If the LiteralVar subclass does not have a corresponding Var subclass. - """ - super().__init_subclass__(**kwargs) - - bases = cls.__bases__ - - bases_normalized = [ - base if isinstance(base, type) else get_origin(base) for base in bases - ] - - possible_bases = [ - base - for base in bases_normalized - if safe_issubclass(base, Var) and base != LiteralVar - ] - - if not possible_bases: - msg = f"LiteralVar subclass {cls} must have a base class that is a subclass of Var and not LiteralVar." - raise TypeError(msg) - - var_subclasses = [ - var_subclass - for var_subclass in _var_subclasses - if var_subclass.var_subclass in possible_bases - ] - - if not var_subclasses: - msg = f"LiteralVar {cls} must have a base class annotated with `python_types`." - raise TypeError(msg) - - if len(var_subclasses) != 1: - msg = f"LiteralVar {cls} must have exactly one base class annotated with `python_types`." - raise TypeError(msg) - - var_subclass = var_subclasses[0] - - # Remove the old subclass, happens because __init_subclass__ is called twice - # for each subclass. This is because of __slots__ in dataclasses. - for var_literal_subclass in list(_var_literal_subclasses): - if var_literal_subclass[1] is var_subclass: - _var_literal_subclasses.remove(var_literal_subclass) - - _var_literal_subclasses.append((cls, var_subclass)) - - @classmethod - def _create_literal_var( - cls, - value: Any, - _var_data: VarData | None = None, - ) -> Var: - """Create a var from a value. - - Args: - value: The value to create the var from. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - - Raises: - TypeError: If the value is not a supported type for LiteralVar. - """ - from .object import LiteralObjectVar - from .sequence import ArrayVar, LiteralStringVar - - if isinstance(value, Var): - if _var_data is None: - return value - return value._replace(merge_var_data=_var_data) - - for literal_subclass, var_subclass in _var_literal_subclasses[::-1]: - if isinstance(value, var_subclass.python_types): - return literal_subclass.create(value, _var_data=_var_data) - - if ( - (as_var_method := getattr(value, "_as_var", None)) is not None - and callable(as_var_method) - and isinstance((resulting_var := as_var_method()), Var) - ): - return resulting_var - - from reflex.event import EventHandler - from reflex.utils.format import get_event_handler_parts - - if isinstance(value, EventHandler): - return Var(_js_expr=".".join(filter(None, get_event_handler_parts(value)))) - - serialized_value = serializers.serialize(value) - if serialized_value is not None: - if isinstance(serialized_value, Mapping): - return LiteralObjectVar.create( - serialized_value, - _var_type=type(value), - _var_data=_var_data, - ) - if isinstance(serialized_value, str): - return LiteralStringVar.create( - serialized_value, _var_type=type(value), _var_data=_var_data - ) - return LiteralVar.create(serialized_value, _var_data=_var_data) - - if dataclasses.is_dataclass(value) and not isinstance(value, type): - return LiteralObjectVar.create( - { - k.name: (None if callable(v := getattr(value, k.name)) else v) - for k in dataclasses.fields(value) - }, - _var_type=type(value), - _var_data=_var_data, - ) - - if isinstance(value, range): - return ArrayVar.range(value.start, value.stop, value.step) - - msg = f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value}." - raise TypeError(msg) - - if not TYPE_CHECKING: - create = _create_literal_var - - def __post_init__(self): - """Post-initialize the var.""" - - @classmethod - def _get_all_var_data_without_creating_var( - cls, - value: Any, - ) -> VarData | None: - return cls.create(value)._get_all_var_data() - - @classmethod - def _get_all_var_data_without_creating_var_dispatch( - cls, - value: Any, - ) -> VarData | None: - """Get all the var data without creating a var. - - Args: - value: The value to get the var data from. - - Returns: - The var data or None. - - Raises: - TypeError: If the value is not a supported type for LiteralVar. - """ - from .object import LiteralObjectVar - from .sequence import LiteralStringVar - - if isinstance(value, Var): - return value._get_all_var_data() - - for literal_subclass, var_subclass in _var_literal_subclasses[::-1]: - if isinstance(value, var_subclass.python_types): - return literal_subclass._get_all_var_data_without_creating_var(value) - - if ( - (as_var_method := getattr(value, "_as_var", None)) is not None - and callable(as_var_method) - and isinstance((resulting_var := as_var_method()), Var) - ): - return resulting_var._get_all_var_data() - - from reflex.event import EventHandler - from reflex.utils.format import get_event_handler_parts - - if isinstance(value, EventHandler): - return Var( - _js_expr=".".join(filter(None, get_event_handler_parts(value))) - )._get_all_var_data() - - serialized_value = serializers.serialize(value) - if serialized_value is not None: - if isinstance(serialized_value, Mapping): - return LiteralObjectVar._get_all_var_data_without_creating_var( - serialized_value - ) - if isinstance(serialized_value, str): - return LiteralStringVar._get_all_var_data_without_creating_var( - serialized_value - ) - return LiteralVar._get_all_var_data_without_creating_var_dispatch( - serialized_value - ) - - if dataclasses.is_dataclass(value) and not isinstance(value, type): - return LiteralObjectVar._get_all_var_data_without_creating_var({ - k.name: (None if callable(v := getattr(value, k.name)) else v) - for k in dataclasses.fields(value) - }) - - if isinstance(value, range): - return None - - msg = f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value}." - raise TypeError(msg) - - @property - def _var_value(self) -> Any: - msg = "LiteralVar subclasses must implement the _var_value property." - raise NotImplementedError(msg) - - def json(self) -> str: - """Serialize the var to a JSON string. - - Raises: - NotImplementedError: If the method is not implemented. - """ - msg = "LiteralVar subclasses must implement the json method." - raise NotImplementedError(msg) - - -@serializers.serializer -def serialize_literal(value: LiteralVar): - """Serialize a Literal type. - - Args: - value: The Literal to serialize. - - Returns: - The serialized Literal. - """ - return value._var_value - - -def get_python_literal(value: LiteralVar | Any) -> Any | None: - """Get the Python literal value. - - Args: - value: The value to get the Python literal value of. - - Returns: - The Python literal value. - """ - if isinstance(value, LiteralVar): - return value._var_value - if isinstance(value, Var): - return None - return value - - -P = ParamSpec("P") -T = TypeVar("T") - - -# NoReturn is used to match CustomVarOperationReturn with no type hint. -@overload -def var_operation( # pyright: ignore [reportOverlappingOverload] - func: Callable[P, CustomVarOperationReturn[NoReturn]], -) -> Callable[P, Var]: ... - - -@overload -def var_operation( - func: Callable[P, CustomVarOperationReturn[None]], -) -> Callable[P, NoneVar]: ... - - -@overload -def var_operation( # pyright: ignore [reportOverlappingOverload] - func: Callable[P, CustomVarOperationReturn[bool]] - | Callable[P, CustomVarOperationReturn[bool | None]], -) -> Callable[P, BooleanVar]: ... - - -NUMBER_T = TypeVar("NUMBER_T", int, float, int | float) - - -@overload -def var_operation( - func: Callable[P, CustomVarOperationReturn[NUMBER_T]] - | Callable[P, CustomVarOperationReturn[NUMBER_T | None]], -) -> Callable[P, NumberVar[NUMBER_T]]: ... - - -@overload -def var_operation( - func: Callable[P, CustomVarOperationReturn[str]] - | Callable[P, CustomVarOperationReturn[str | None]], -) -> Callable[P, StringVar]: ... - - -LIST_T = TypeVar("LIST_T", bound=Sequence) - - -@overload -def var_operation( - func: Callable[P, CustomVarOperationReturn[LIST_T]] - | Callable[P, CustomVarOperationReturn[LIST_T | None]], -) -> Callable[P, ArrayVar[LIST_T]]: ... - - -OBJECT_TYPE = TypeVar("OBJECT_TYPE", bound=Mapping) - - -@overload -def var_operation( - func: Callable[P, CustomVarOperationReturn[OBJECT_TYPE]] - | Callable[P, CustomVarOperationReturn[OBJECT_TYPE | None]], -) -> Callable[P, ObjectVar[OBJECT_TYPE]]: ... - - -@overload -def var_operation( - func: Callable[P, CustomVarOperationReturn[T]] - | Callable[P, CustomVarOperationReturn[T | None]], -) -> Callable[P, Var[T]]: ... - - -def var_operation( # pyright: ignore [reportInconsistentOverload] - func: Callable[P, CustomVarOperationReturn[T]], -) -> Callable[P, Var[T]]: - """Decorator for creating a var operation. - - Example: - ```python - @var_operation - def add(a: NumberVar, b: NumberVar): - return custom_var_operation(f"{a} + {b}") - ``` - - Args: - func: The function to decorate. - - Returns: - The decorated function. - """ - func_args = list(inspect.signature(func).parameters) - - @functools.wraps(func) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> Var[T]: - args_vars = { - func_args[i]: (LiteralVar.create(arg) if not isinstance(arg, Var) else arg) - for i, arg in enumerate(args) - } - kwargs_vars = { - key: LiteralVar.create(value) if not isinstance(value, Var) else value - for key, value in kwargs.items() - } - - return CustomVarOperation.create( - name=func.__name__, - args=tuple(list(args_vars.items()) + list(kwargs_vars.items())), - return_var=func(*args_vars.values(), **kwargs_vars), # pyright: ignore [reportCallIssue, reportReturnType] - ).guess_type() - - return wrapper - - -def figure_out_type(value: Any) -> types.GenericType: - """Figure out the type of the value. - - Args: - value: The value to figure out the type of. - - Returns: - The type of the value. - """ - if isinstance(value, (list, set, tuple, Mapping, Var)): - if isinstance(value, Var): - return value._var_type - if has_args(value_type := type(value)): - return value_type - if isinstance(value, list): - if not value: - return Sequence[NoReturn] - return Sequence[unionize(*{figure_out_type(v) for v in value[:100]})] - if isinstance(value, set): - return set[unionize(*{figure_out_type(v) for v in value})] - if isinstance(value, tuple): - if not value: - return tuple[NoReturn, ...] - if len(value) <= 5: - return tuple[tuple(figure_out_type(v) for v in value)] - return tuple[unionize(*{figure_out_type(v) for v in value[:100]}), ...] - if isinstance(value, Mapping): - if not value: - return Mapping[NoReturn, NoReturn] - return Mapping[ - unionize(*{figure_out_type(k) for k in list(value.keys())[:100]}), - unionize(*{figure_out_type(v) for v in list(value.values())[:100]}), - ] - return type(value) - - -GLOBAL_CACHE = {} - - -class cached_property: # noqa: N801 - """A cached property that caches the result of the function.""" - - def __init__(self, func: Callable): - """Initialize the cached_property. - - Args: - func: The function to cache. - """ - self._func = func - self._attrname = None - - def __set_name__(self, owner: Any, name: str): - """Set the name of the cached property. - - Args: - owner: The owner of the cached property. - name: The name of the cached property. - - Raises: - TypeError: If the cached property is assigned to two different names. - """ - if self._attrname is None: - self._attrname = name - - original_del = getattr(owner, "__del__", None) - - def delete_property(this: Any): - """Delete the cached property. - - Args: - this: The object to delete the cached property from. - """ - cached_field_name = "_reflex_cache_" + name - try: - unique_id = object.__getattribute__(this, cached_field_name) - except AttributeError: - if original_del is not None: - original_del(this) - return - GLOBAL_CACHE.pop(unique_id, None) - - if original_del is not None: - original_del(this) - - owner.__del__ = delete_property - - elif name != self._attrname: - msg = ( - "Cannot assign the same cached_property to two different names " - f"({self._attrname!r} and {name!r})." - ) - raise TypeError(msg) - - def __get__(self, instance: Any, owner: type | None = None): - """Get the cached property. - - Args: - instance: The instance to get the cached property from. - owner: The owner of the cached property. - - Returns: - The cached property. - - Raises: - TypeError: If the class does not have __set_name__. - """ - if self._attrname is None: - msg = "Cannot use cached_property on a class without __set_name__." - raise TypeError(msg) - cached_field_name = "_reflex_cache_" + self._attrname - try: - unique_id = object.__getattribute__(instance, cached_field_name) - except AttributeError: - unique_id = uuid.uuid4().int - object.__setattr__(instance, cached_field_name, unique_id) - if unique_id not in GLOBAL_CACHE: - GLOBAL_CACHE[unique_id] = self._func(instance) - return GLOBAL_CACHE[unique_id] - - -cached_property_no_lock = cached_property - - -class VarProtocol(Protocol): - """A protocol for Var.""" - - __dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]] - - @property - def _js_expr(self) -> str: ... - - @property - def _var_type(self) -> types.GenericType: ... - - @property - def _var_data(self) -> VarData: ... - - -class CachedVarOperation: - """Base class for cached var operations to lower boilerplate code.""" - - def __post_init__(self): - """Post-initialize the CachedVarOperation.""" - object.__delattr__(self, "_js_expr") - - def __getattr__(self, name: str) -> Any: - """Get an attribute of the var. - - Args: - name: The name of the attribute. - - Returns: - The attribute. - """ - if name == "_js_expr": - return self._cached_var_name - - parent_classes = inspect.getmro(type(self)) - - next_class = parent_classes[parent_classes.index(CachedVarOperation) + 1] - - return next_class.__getattr__(self, name) - - def _get_all_var_data(self) -> VarData | None: - """Get all VarData associated with the Var. - - Returns: - The VarData of the components and all of its children. - """ - return self._cached_get_all_var_data - - @cached_property_no_lock - def _cached_get_all_var_data(self: VarProtocol) -> VarData | None: - """Get the cached VarData. - - Returns: - The cached VarData. - """ - return VarData.merge( - *( - value._get_all_var_data() if isinstance(value, Var) else None - for value in ( - getattr(self, field.name) for field in dataclasses.fields(self) - ) - ), - self._var_data, - ) - - def __hash__(self: DataclassInstance) -> int: - """Calculate the hash of the object. - - Returns: - The hash of the object. - """ - return hash(( - type(self).__name__, - *[ - getattr(self, field.name) - for field in dataclasses.fields(self) - if field.name not in ["_js_expr", "_var_data", "_var_type"] - ], - )) - - -def and_operation( - a: Var[VAR_TYPE] | Any, b: Var[OTHER_VAR_TYPE] | Any -) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: - """Perform a logical AND operation on two variables. - - Args: - a: The first variable. - b: The second variable. - - Returns: - The result of the logical AND operation. - """ - return _and_operation(a, b) - - -@var_operation -def _and_operation(a: Var, b: Var): - """Perform a logical AND operation on two variables. - - Args: - a: The first variable. - b: The second variable. - - Returns: - The result of the logical AND operation. - """ - return var_operation_return( - js_expression=f"({a} && {b})", - var_type=unionize(a._var_type, b._var_type), - ) - - -def or_operation( - a: Var[VAR_TYPE] | Any, b: Var[OTHER_VAR_TYPE] | Any -) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: - """Perform a logical OR operation on two variables. - - Args: - a: The first variable. - b: The second variable. - - Returns: - The result of the logical OR operation. - """ - return _or_operation(a, b) - - -@var_operation -def _or_operation(a: Var, b: Var): - """Perform a logical OR operation on two variables. - - Args: - a: The first variable. - b: The second variable. - - Returns: - The result of the logical OR operation. - """ - return var_operation_return( - js_expression=f"({a} || {b})", - var_type=unionize(a._var_type, b._var_type), - ) - - -RETURN_TYPE = TypeVar("RETURN_TYPE") - -DICT_KEY = TypeVar("DICT_KEY") -DICT_VAL = TypeVar("DICT_VAL") - -LIST_INSIDE = TypeVar("LIST_INSIDE") - - -class FakeComputedVarBaseClass(property): - """A fake base class for ComputedVar to avoid inheriting from property.""" - - __pydantic_run_validation__ = False - - -def is_computed_var(obj: Any) -> TypeGuard[ComputedVar]: - """Check if the object is a ComputedVar. - - Args: - obj: The object to check. - - Returns: - Whether the object is a ComputedVar. - """ - return isinstance(obj, FakeComputedVarBaseClass) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class ComputedVar(Var[RETURN_TYPE]): - """A field with computed getters.""" - - # Whether to track dependencies and cache computed values - _cache: bool = dataclasses.field(default=False) - - # Whether the computed var is a backend var - _backend: bool = dataclasses.field(default=False) - - # The initial value of the computed var - _initial_value: RETURN_TYPE | types.Unset = dataclasses.field(default=types.Unset()) - - # Explicit var dependencies to track - _static_deps: dict[str | None, set[str]] = dataclasses.field(default_factory=dict) - - # 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: datetime.timedelta | None = dataclasses.field(default=None) - - _fget: Callable[[BaseState], RETURN_TYPE] = dataclasses.field( - default_factory=lambda: lambda _: None - ) # pyright: ignore [reportAssignmentType] - - _name: str = dataclasses.field(default="") - - def __init__( - self, - fget: Callable[[BASE_STATE], RETURN_TYPE], - initial_value: RETURN_TYPE | types.Unset = types.Unset(), - cache: bool = True, - deps: list[str | Var] | None = None, - auto_deps: bool = True, - interval: int | datetime.timedelta | None = None, - backend: bool | None = None, - **kwargs, - ): - """Initialize a ComputedVar. - - Args: - 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. - backend: Whether the computed var is a backend var. - **kwargs: additional attributes to set on the instance - - Raises: - TypeError: If the computed var dependencies are not Var instances or var names. - UntypedComputedVarError: If the computed var is untyped. - """ - hint = kwargs.pop("return_type", None) or get_type_hints(fget).get( - "return", Any - ) - - if hint is Any: - raise UntypedComputedVarError(var_name=fget.__name__) - is_using_fget_name = "_js_expr" not in kwargs - js_expr = kwargs.pop("_js_expr", fget.__name__ + FIELD_MARKER) - kwargs.setdefault("_var_type", hint) - - Var.__init__( - self, - _js_expr=js_expr, - _var_type=kwargs.pop("_var_type"), - _var_data=kwargs.pop( - "_var_data", - VarData(field_name=fget.__name__) if is_using_fget_name else None, - ), - ) - - if kwargs: - msg = f"Unexpected keyword arguments: {tuple(kwargs)}" - raise TypeError(msg) - - if backend is None: - backend = fget.__name__.startswith("_") - - object.__setattr__(self, "_backend", backend) - object.__setattr__(self, "_initial_value", initial_value) - object.__setattr__(self, "_cache", cache) - object.__setattr__(self, "_name", fget.__name__) - - if isinstance(interval, int): - interval = datetime.timedelta(seconds=interval) - - object.__setattr__(self, "_update_interval", interval) - - object.__setattr__( - self, - "_static_deps", - self._calculate_static_deps(deps), - ) - object.__setattr__(self, "_auto_deps", auto_deps) - - object.__setattr__(self, "_fget", fget) - - def _calculate_static_deps( - self, - deps: list[str | Var] | dict[str | None, set[str]] | None = None, - ) -> dict[str | None, set[str]]: - """Calculate the static dependencies of the computed var from user input or existing dependencies. - - Args: - deps: The user input dependencies or existing dependencies. - - Returns: - The static dependencies. - """ - if isinstance(deps, dict): - # Assume a dict is coming from _replace, so no special processing. - return deps - static_deps = {} - if deps is not None: - for dep in deps: - static_deps = self._add_static_dep(dep, static_deps) - return static_deps - - def _add_static_dep( - self, dep: str | Var, deps: dict[str | None, set[str]] | None = None - ) -> dict[str | None, set[str]]: - """Add a static dependency to the computed var or existing dependency set. - - Args: - dep: The dependency to add. - deps: The existing dependency set. - - Returns: - The updated dependency set. - - Raises: - TypeError: If the computed var dependencies are not Var instances or var names. - """ - if deps is None: - deps = self._static_deps - if isinstance(dep, Var): - state_name = ( - all_var_data.state - if (all_var_data := dep._get_all_var_data()) and all_var_data.state - else None - ) - if all_var_data is not None: - var_name = all_var_data.field_name - else: - var_name = dep._js_expr - deps.setdefault(state_name, set()).add(var_name) - elif isinstance(dep, str) and dep != "": - deps.setdefault(None, set()).add(dep) - else: - msg = "ComputedVar dependencies must be Var instances or var names (non-empty strings)." - raise TypeError(msg) - return deps - - @override - def _replace( - self, - merge_var_data: VarData | None = None, - **kwargs: Any, - ) -> Self: - """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. - - Raises: - TypeError: If kwargs contains keys that are not allowed. - """ - if "deps" in kwargs: - kwargs["deps"] = self._calculate_static_deps(kwargs["deps"]) - field_values = { - "fget": kwargs.pop("fget", self._fget), - "initial_value": kwargs.pop("initial_value", self._initial_value), - "cache": kwargs.pop("cache", self._cache), - "deps": kwargs.pop("deps", copy.copy(self._static_deps)), - "auto_deps": kwargs.pop("auto_deps", self._auto_deps), - "interval": kwargs.pop("interval", self._update_interval), - "backend": kwargs.pop("backend", self._backend), - "_js_expr": kwargs.pop("_js_expr", self._js_expr), - "_var_type": kwargs.pop("_var_type", self._var_type), - "_var_data": kwargs.pop( - "_var_data", VarData.merge(self._var_data, merge_var_data) - ), - "return_type": kwargs.pop("return_type", self._var_type), - } - - if kwargs: - unexpected_kwargs = ", ".join(kwargs.keys()) - msg = f"Unexpected keyword arguments: {unexpected_kwargs}" - raise TypeError(msg) - - return type(self)(**field_values) - - @property - def _cache_attr(self) -> str: - """Get the attribute used to cache the value on the instance. - - Returns: - An attribute name. - """ - return f"__cached_{self._js_expr}" - - @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._js_expr}" - - 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 - - @overload - def __get__( - self: ComputedVar[bool], - instance: None, - owner: type, - ) -> BooleanVar: ... - - @overload - def __get__( - self: ComputedVar[int] | ComputedVar[float], - instance: None, - owner: type, - ) -> NumberVar: ... - - @overload - def __get__( - self: ComputedVar[str], - instance: None, - owner: type, - ) -> StringVar: ... - - @overload - def __get__( - self: ComputedVar[MAPPING_TYPE], - instance: None, - owner: type, - ) -> ObjectVar[MAPPING_TYPE]: ... - - @overload - def __get__( - self: ComputedVar[list[LIST_INSIDE]], - instance: None, - owner: type, - ) -> ArrayVar[list[LIST_INSIDE]]: ... - - @overload - def __get__( - self: ComputedVar[tuple[LIST_INSIDE, ...]], - instance: None, - owner: type, - ) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ... - - @overload - def __get__( - self: ComputedVar[BASE_TYPE], - instance: None, - owner: type, - ) -> ObjectVar[BASE_TYPE]: ... - - @overload - def __get__( - self: ComputedVar[SQLA_TYPE], - instance: None, - owner: type, - ) -> ObjectVar[SQLA_TYPE]: ... - - if TYPE_CHECKING: - - @overload - def __get__( - self: ComputedVar[DATACLASS_TYPE], instance: None, owner: Any - ) -> ObjectVar[DATACLASS_TYPE]: ... - - @overload - def __get__(self, instance: None, owner: type) -> ComputedVar[RETURN_TYPE]: ... - - @overload - def __get__(self, instance: BaseState, owner: type) -> RETURN_TYPE: ... - - def __get__(self, instance: BaseState | None, owner: type): - """Get the ComputedVar value. - - If the value is already cached on the instance, return the cached value. - - Args: - instance: the instance of the class accessing this computed var. - owner: the class that this descriptor is attached to. - - Returns: - The value of the var for the given instance. - """ - if instance is None: - state_where_defined = owner - while self._name in state_where_defined.inherited_vars: - state_where_defined = state_where_defined.get_parent_state() - - field_name = ( - format_state_name(state_where_defined.get_full_name()) - + "." - + self._js_expr - ) - - return dispatch( - field_name, - var_data=VarData.from_state(state_where_defined, self._name), - result_var_type=self._var_type, - existing_var=self, - ) - - if not self._cache: - value = self.fget(instance) - else: - # handle caching - if not hasattr(instance, self._cache_attr) or self.needs_update(instance): - # Set cache attr on state instance. - setattr(instance, self._cache_attr, self.fget(instance)) - # 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()) - value = getattr(instance, self._cache_attr) - - self._check_deprecated_return_type(instance, value) - - return value - - def _check_deprecated_return_type(self, instance: BaseState, value: Any) -> None: - if not _isinstance(value, self._var_type, nested=1, treat_var_as_type=False): - console.error( - f"Computed var '{type(instance).__name__}.{self._name}' must return" - f" a value of type '{escape(str(self._var_type))}', got '{value!s}' of type {type(value)}." - ) - - def _deps( - self, - objclass: type[BaseState], - obj: FunctionType | CodeType | None = None, - ) -> dict[str, set[str]]: - """Determine var dependencies of this ComputedVar. - - Save references to attributes accessed on "self" or other fetched states. - - Recursively called when the function makes a method call on "self" or - define comprehensions or nested functions that may reference "self". - - Args: - objclass: the class obj this ComputedVar is attached to. - obj: the object to disassemble (defaults to the fget function). - - Returns: - A dictionary mapping state names to the set of variable names - accessed by the given obj. - """ - from .dep_tracking import DependencyTracker - - d = {} - if self._static_deps: - d.update(self._static_deps) - # None is a placeholder for the current state class. - if None in d: - d[objclass.get_full_name()] = d.pop(None) - - if not self._auto_deps: - return d - - if obj is None: - fget = self._fget - if fget is not None: - obj = cast(FunctionType, fget) - else: - return d - - try: - return DependencyTracker( - func=obj, state_cls=objclass, dependencies=d - ).dependencies - except Exception as e: - console.warn( - "Failed to automatically determine dependencies for computed var " - f"{objclass.__name__}.{self._name}: {e}. " - "Set auto_deps=False and provide accurate deps=['var1', 'var2'] to suppress this warning." - ) - return d - - def mark_dirty(self, instance: BaseState) -> None: - """Mark this ComputedVar as dirty. - - Args: - instance: the state instance that needs to recompute the value. - """ - with contextlib.suppress(AttributeError): - delattr(instance, self._cache_attr) - - def add_dependency(self, objclass: type[BaseState], dep: Var): - """Explicitly add a dependency to the ComputedVar. - - After adding the dependency, when the `dep` changes, this computed var - will be marked dirty. - - Args: - objclass: The class obj this ComputedVar is attached to. - dep: The dependency to add. - - Raises: - VarDependencyError: If the dependency is not a Var instance with a - state and field name - """ - if all_var_data := dep._get_all_var_data(): - state_name = all_var_data.state - if state_name: - var_name = all_var_data.field_name - if var_name: - self._static_deps.setdefault(state_name, set()).add(var_name) - target_state_class = objclass.get_root_state().get_class_substate( - state_name - ) - target_state_class._var_dependencies.setdefault( - var_name, set() - ).add(( - objclass.get_full_name(), - self._name, - )) - target_state_class._potentially_dirty_states.add( - objclass.get_full_name() - ) - return - msg = ( - "ComputedVar dependencies must be Var instances with a state and " - f"field name, got {dep!r}." - ) - raise VarDependencyError(msg) - - def _determine_var_type(self) -> type: - """Get the type of the var. - - Returns: - The type of the var. - """ - hints = get_type_hints(self._fget) - if "return" in hints: - return hints["return"] - return Any # pyright: ignore [reportReturnType] - - @property - def __class__(self) -> type: - """Get the class of the var. - - Returns: - The class of the var. - """ - return FakeComputedVarBaseClass - - @property - def fget(self) -> Callable[[BaseState], RETURN_TYPE]: - """Get the getter function. - - Returns: - The getter function. - """ - return self._fget - - -class DynamicRouteVar(ComputedVar[str | list[str]]): - """A ComputedVar that represents a dynamic route.""" - - -async def _default_async_computed_var(_self: BaseState) -> Any: # noqa: RUF029 - return None - - -@dataclasses.dataclass( - eq=False, - frozen=True, - init=False, - slots=True, -) -class AsyncComputedVar(ComputedVar[RETURN_TYPE]): - """A computed var that wraps a coroutinefunction.""" - - _fget: Callable[[BaseState], Coroutine[None, None, RETURN_TYPE]] = ( - dataclasses.field(default=_default_async_computed_var) - ) - - @overload - def __get__( - self: AsyncComputedVar[bool], - instance: None, - owner: type, - ) -> BooleanVar: ... - - @overload - def __get__( - self: AsyncComputedVar[int] | ComputedVar[float], - instance: None, - owner: type, - ) -> NumberVar: ... - - @overload - def __get__( - self: AsyncComputedVar[str], - instance: None, - owner: type, - ) -> StringVar: ... - - @overload - def __get__( - self: AsyncComputedVar[MAPPING_TYPE], - instance: None, - owner: type, - ) -> ObjectVar[MAPPING_TYPE]: ... - - @overload - def __get__( - self: AsyncComputedVar[list[LIST_INSIDE]], - instance: None, - owner: type, - ) -> ArrayVar[list[LIST_INSIDE]]: ... - - @overload - def __get__( - self: AsyncComputedVar[tuple[LIST_INSIDE, ...]], - instance: None, - owner: type, - ) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ... - - @overload - def __get__( - self: AsyncComputedVar[BASE_TYPE], - instance: None, - owner: type, - ) -> ObjectVar[BASE_TYPE]: ... - - @overload - def __get__( - self: AsyncComputedVar[SQLA_TYPE], - instance: None, - owner: type, - ) -> ObjectVar[SQLA_TYPE]: ... - - if TYPE_CHECKING: - - @overload - def __get__( - self: AsyncComputedVar[DATACLASS_TYPE], instance: None, owner: Any - ) -> ObjectVar[DATACLASS_TYPE]: ... - - @overload - def __get__(self, instance: None, owner: type) -> AsyncComputedVar[RETURN_TYPE]: ... - - @overload - def __get__( - self, instance: BaseState, owner: type - ) -> Coroutine[None, None, RETURN_TYPE]: ... - - def __get__( - self, instance: BaseState | None, owner - ) -> Var | Coroutine[None, None, RETURN_TYPE]: - """Get the ComputedVar value. - - If the value is already cached on the instance, return the cached value. - - Args: - instance: the instance of the class accessing this computed var. - owner: the class that this descriptor is attached to. - - Returns: - The value of the var for the given instance. - """ - if instance is None: - return super(AsyncComputedVar, self).__get__(instance, owner) - - if not self._cache: - - async def _awaitable_result(instance: BaseState = instance) -> RETURN_TYPE: - value = await self.fget(instance) - self._check_deprecated_return_type(instance, value) - return value - - return _awaitable_result() - - # handle caching - async def _awaitable_result(instance: BaseState = instance) -> RETURN_TYPE: - if not hasattr(instance, self._cache_attr) or self.needs_update(instance): - # Set cache attr on state instance. - setattr(instance, self._cache_attr, await self.fget(instance)) - # 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()) - value = getattr(instance, self._cache_attr) - self._check_deprecated_return_type(instance, value) - return value - - return _awaitable_result() - - @property - def fget(self) -> Callable[[BaseState], Coroutine[None, None, RETURN_TYPE]]: - """Get the getter function. - - Returns: - The getter function. - """ - return self._fget - - -if TYPE_CHECKING: - BASE_STATE = TypeVar("BASE_STATE", bound=BaseState) - - -class _ComputedVarDecorator(Protocol): - """A protocol for the ComputedVar decorator.""" - - @overload - def __call__( - self, - fget: Callable[[BASE_STATE], Coroutine[Any, Any, RETURN_TYPE]], - ) -> AsyncComputedVar[RETURN_TYPE]: ... - - @overload - def __call__( - self, - fget: Callable[[BASE_STATE], RETURN_TYPE], - ) -> ComputedVar[RETURN_TYPE]: ... - - def __call__( - self, - fget: Callable[[BASE_STATE], Any], - ) -> ComputedVar[Any]: ... - - -@overload -def computed_var( - fget: None = None, - initial_value: Any | types.Unset = types.Unset(), - cache: bool = True, - deps: list[str | Var] | None = None, - auto_deps: bool = True, - interval: datetime.timedelta | int | None = None, - backend: bool | None = None, - **kwargs, -) -> _ComputedVarDecorator: ... - - -@overload -def computed_var( - fget: Callable[[BASE_STATE], Coroutine[Any, Any, RETURN_TYPE]], - initial_value: RETURN_TYPE | types.Unset = types.Unset(), - cache: bool = True, - deps: list[str | Var] | None = None, - auto_deps: bool = True, - interval: datetime.timedelta | int | None = None, - backend: bool | None = None, - **kwargs, -) -> AsyncComputedVar[RETURN_TYPE]: ... - - -@overload -def computed_var( - fget: Callable[[BASE_STATE], RETURN_TYPE], - initial_value: RETURN_TYPE | types.Unset = types.Unset(), - cache: bool = True, - deps: list[str | Var] | None = None, - auto_deps: bool = True, - interval: datetime.timedelta | int | None = None, - backend: bool | None = None, - **kwargs, -) -> ComputedVar[RETURN_TYPE]: ... - - -def computed_var( - fget: Callable[[BASE_STATE], Any] | None = None, - initial_value: Any | types.Unset = types.Unset(), - cache: bool = True, - deps: list[str | Var] | None = None, - auto_deps: bool = True, - interval: datetime.timedelta | int | None = None, - backend: bool | None = None, - **kwargs, -) -> ComputedVar | Callable[[Callable[[BASE_STATE], Any]], ComputedVar]: - """A ComputedVar decorator with or without kwargs. - - Args: - 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. - backend: Whether the computed var is a backend var. - **kwargs: additional attributes to set on the instance - - Returns: - A ComputedVar instance. - - Raises: - ValueError: If caching is disabled and an update interval is set. - VarDependencyError: If user supplies dependencies without caching. - ComputedVarSignatureError: If the getter function has more than one argument. - """ - if cache is False and interval is not None: - msg = "Cannot set update interval without caching." - raise ValueError(msg) - - if cache is False and (deps is not None or auto_deps is False): - msg = "Cannot track dependencies without caching." - raise VarDependencyError(msg) - - if fget is not None: - sign = inspect.signature(fget) - if len(sign.parameters) != 1: - raise ComputedVarSignatureError(fget.__name__, signature=str(sign)) - - if inspect.iscoroutinefunction(fget): - computed_var_cls = AsyncComputedVar - else: - computed_var_cls = ComputedVar - return computed_var_cls( - fget, - initial_value=initial_value, - cache=cache, - deps=deps, - auto_deps=auto_deps, - interval=interval, - backend=backend, - **kwargs, - ) - - def wrapper(fget: Callable[[BASE_STATE], Any]) -> ComputedVar: - if inspect.iscoroutinefunction(fget): - computed_var_cls = AsyncComputedVar - else: - computed_var_cls = ComputedVar - return computed_var_cls( - fget, - initial_value=initial_value, - cache=cache, - deps=deps, - auto_deps=auto_deps, - interval=interval, - backend=backend, - **kwargs, - ) - - return wrapper - - -RETURN = TypeVar("RETURN") - - -class CustomVarOperationReturn(Var[RETURN]): - """Base class for custom var operations.""" - - @classmethod - def create( - cls, - js_expression: str, - _var_type: type[RETURN] | None = None, - _var_data: VarData | None = None, - ) -> CustomVarOperationReturn[RETURN]: - """Create a CustomVarOperation. - - Args: - js_expression: The JavaScript expression to evaluate. - _var_type: The type of the var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The CustomVarOperation. - """ - return CustomVarOperationReturn( - _js_expr=js_expression, - _var_type=_var_type or Any, - _var_data=_var_data, - ) - - -def var_operation_return( - js_expression: str, - var_type: type[RETURN] | GenericType | None = None, - var_data: VarData | None = None, -) -> CustomVarOperationReturn[RETURN]: - """Shortcut for creating a CustomVarOperationReturn. - - Args: - js_expression: The JavaScript expression to evaluate. - var_type: The type of the var. - var_data: Additional hooks and imports associated with the Var. - - Returns: - The CustomVarOperationReturn. - """ - return CustomVarOperationReturn.create( - js_expression, - var_type, - var_data, - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class CustomVarOperation(CachedVarOperation, Var[T]): - """Base class for custom var operations.""" - - _name: str = dataclasses.field(default="") - - _args: tuple[tuple[str, Var], ...] = dataclasses.field(default_factory=tuple) - - _return: CustomVarOperationReturn[T] = dataclasses.field( - default_factory=lambda: CustomVarOperationReturn.create("") - ) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """Get the cached var name. - - Returns: - The cached var name. - """ - return str(self._return) - - @cached_property_no_lock - def _cached_get_all_var_data(self) -> VarData | None: - """Get the cached VarData. - - Returns: - The cached VarData. - """ - return VarData.merge( - *(arg[1]._get_all_var_data() for arg in self._args), - self._return._get_all_var_data(), - self._var_data, - ) - - @classmethod - def create( - cls, - name: str, - args: tuple[tuple[str, Var], ...], - return_var: CustomVarOperationReturn[T], - _var_data: VarData | None = None, - ) -> CustomVarOperation[T]: - """Create a CustomVarOperation. - - Args: - name: The name of the operation. - args: The arguments to the operation. - return_var: The return var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The CustomVarOperation. - """ - return CustomVarOperation( - _js_expr="", - _var_type=return_var._var_type, - _var_data=_var_data, - _name=name, - _args=args, - _return=return_var, - ) - - -class NoneVar(Var[None], python_types=type(None)): - """A var representing None.""" - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralNoneVar(LiteralVar, NoneVar): - """A var representing None.""" - - _var_value: None = None - - def json(self) -> str: - """Serialize the var to a JSON string. - - Returns: - The JSON string. - """ - return "null" - - @classmethod - def _get_all_var_data_without_creating_var(cls, value: None) -> VarData | None: - return None - - @classmethod - def create( - cls, - value: None = None, - _var_data: VarData | None = None, - ) -> LiteralNoneVar: - """Create a var from a value. - - Args: - value: The value of the var. Must be None. Existed for compatibility with LiteralVar. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return LiteralNoneVar( - _js_expr="null", - _var_type=None, - _var_data=_var_data, - ) - - -def get_to_operation(var_subclass: type[Var]) -> type[ToOperation]: - """Get the ToOperation class for a given Var subclass. - - Args: - var_subclass: The Var subclass. - - Returns: - The ToOperation class. - - Raises: - ValueError: If the ToOperation class cannot be found. - """ - possible_classes = [ - saved_var_subclass.to_var_subclass - for saved_var_subclass in _var_subclasses - if saved_var_subclass.var_subclass is var_subclass - ] - if not possible_classes: - msg = f"Could not find ToOperation for {var_subclass}." - raise ValueError(msg) - return possible_classes[0] - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class StateOperation(CachedVarOperation, Var): - """A var operation that accesses a field on an object.""" - - _state_name: str = dataclasses.field(default="") - _field: Var = dataclasses.field(default_factory=lambda: LiteralNoneVar.create()) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """Get the cached var name. - - Returns: - The cached var name. - """ - return f"{self._state_name!s}.{self._field!s}" - - def __getattr__(self, name: str) -> Any: - """Get an attribute of the var. - - Args: - name: The name of the attribute. - - Returns: - The attribute. - """ - if name == "_js_expr": - return self._cached_var_name - - return getattr(self._field, name) - - @classmethod - def create( - cls, - state_name: str, - field: Var, - _var_data: VarData | None = None, - ) -> StateOperation: - """Create a DotOperation. - - Args: - state_name: The name of the state. - field: The field of the state. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The DotOperation. - """ - return StateOperation( - _js_expr="", - _var_type=field._var_type, - _var_data=_var_data, - _state_name=state_name, - _field=field, - ) - - -def get_uuid_string_var() -> Var: - """Return a Var that generates a single memoized UUID via .web/utils/state.js. - - useMemo with an empty dependency array ensures that the generated UUID is - consistent across re-renders of the component. - - Returns: - A Var that generates a UUID at runtime. - """ - from reflex.utils.imports import ImportVar - from reflex.vars import Var - - unique_uuid_var = get_unique_variable_name() - unique_uuid_var_data = VarData( - imports={ - f"$/{constants.Dirs.STATE_PATH}": ImportVar(tag="generateUUID"), - "react": "useMemo", - }, - hooks={f"const {unique_uuid_var} = useMemo(generateUUID, [])": None}, - ) - - return Var( - _js_expr=unique_uuid_var, - _var_type=str, - _var_data=unique_uuid_var_data, - ) - - -# Set of unique variable names. -USED_VARIABLES = set() - - -@once -def _rng(): - import random - - return random.Random(42) - - -def get_unique_variable_name() -> str: - """Get a unique variable name. - - Returns: - The unique variable name. - """ - name = "".join([_rng().choice(string.ascii_lowercase) for _ in range(8)]) - if name not in USED_VARIABLES: - USED_VARIABLES.add(name) - return name - return get_unique_variable_name() - - -# Compile regex for finding reflex var tags. -_decode_var_pattern_re = ( - rf"{constants.REFLEX_VAR_OPENING_TAG}(.*?){constants.REFLEX_VAR_CLOSING_TAG}" -) -_decode_var_pattern = re.compile(_decode_var_pattern_re, flags=re.DOTALL) - -# Defined global immutable vars. -_global_vars: dict[int, Var] = {} - - -dispatchers: dict[GenericType, Callable[[Var], Var]] = {} - - -def transform(fn: Callable[[Var], Var]) -> Callable[[Var], Var]: - """Register a function to transform a Var. - - Args: - fn: The function to register. - - Returns: - The decorator. - - Raises: - TypeError: If the return type of the function is not a Var. - TypeError: If the Var return type does not have a generic type. - ValueError: If a function for the generic type is already registered. - """ - types = get_type_hints(fn) - return_type = types["return"] - - origin = get_origin(return_type) - - if origin is not Var: - msg = f"Expected return type of {fn.__name__} to be a Var, got {origin}." - raise TypeError(msg) - - generic_args = get_args(return_type) - - if not generic_args: - msg = f"Expected Var return type of {fn.__name__} to have a generic type." - raise TypeError(msg) - - generic_type = get_origin(generic_args[0]) or generic_args[0] - - if generic_type in dispatchers: - msg = f"Function for {generic_type} already registered." - raise ValueError(msg) - - dispatchers[generic_type] = fn - - return fn - - -def dispatch( - field_name: str, - var_data: VarData, - result_var_type: GenericType, - existing_var: Var | None = None, -) -> Var: - """Dispatch a Var to the appropriate transformation function. - - Args: - field_name: The name of the field. - var_data: The VarData associated with the Var. - result_var_type: The type of the Var. - existing_var: The existing Var to transform. Optional. - - Returns: - The transformed Var. - - Raises: - TypeError: If the return type of the function is not a Var. - TypeError: If the Var return type does not have a generic type. - TypeError: If the first argument of the function is not a Var. - TypeError: If the first argument of the function does not have a generic type - """ - result_origin_var_type = get_origin(result_var_type) or result_var_type - - if result_origin_var_type in dispatchers: - fn = dispatchers[result_origin_var_type] - fn_types = get_type_hints(fn) - fn_first_arg_type = fn_types.get( - next(iter(inspect.signature(fn).parameters.values())).name, Any - ) - - fn_return = fn_types.get("return", Any) - - fn_return_origin = get_origin(fn_return) or fn_return - - if fn_return_origin is not Var: - msg = f"Expected return type of {fn.__name__} to be a Var, got {fn_return}." - raise TypeError(msg) - - fn_return_generic_args = get_args(fn_return) - - if not fn_return_generic_args: - msg = f"Expected generic type of {fn_return} to be a type." - raise TypeError(msg) - - arg_origin = get_origin(fn_first_arg_type) or fn_first_arg_type - - if arg_origin is not Var: - msg = f"Expected first argument of {fn.__name__} to be a Var, got {fn_first_arg_type}." - raise TypeError(msg) - - arg_generic_args = get_args(fn_first_arg_type) - - if not arg_generic_args: - msg = f"Expected generic type of {fn_first_arg_type} to be a type." - raise TypeError(msg) - - fn_return_type = fn_return_generic_args[0] - - var = ( - Var( - field_name, - _var_data=var_data, - _var_type=fn_return_type, - ).guess_type() - if existing_var is None - else existing_var._replace( - _var_type=fn_return_type, - _var_data=var_data, - _js_expr=field_name, - ).guess_type() - ) - - return fn(var) - - if existing_var is not None: - return existing_var._replace( - _js_expr=field_name, - _var_data=var_data, - _var_type=result_var_type, - ).guess_type() - return Var( - field_name, - _var_data=var_data, - _var_type=result_var_type, - ).guess_type() - - -if TYPE_CHECKING: - from _typeshed import DataclassInstance - from sqlalchemy.orm import DeclarativeBase - - from reflex.base import Base - - SQLA_TYPE = TypeVar("SQLA_TYPE", bound=DeclarativeBase | None) - BASE_TYPE = TypeVar("BASE_TYPE", bound=Base | None) - DATACLASS_TYPE = TypeVar("DATACLASS_TYPE", bound=DataclassInstance | None) - MAPPING_TYPE = TypeVar("MAPPING_TYPE", bound=Mapping | None) - V = TypeVar("V") - - -FIELD_TYPE = TypeVar("FIELD_TYPE") - - -class Field(Generic[FIELD_TYPE]): - """A field for a state.""" - - if TYPE_CHECKING: - type_: GenericType - default: FIELD_TYPE | _MISSING_TYPE - default_factory: Callable[[], FIELD_TYPE] | None - - def __init__( - self, - default: FIELD_TYPE | _MISSING_TYPE = MISSING, - default_factory: Callable[[], FIELD_TYPE] | None = None, - is_var: bool = True, - annotated_type: GenericType # pyright: ignore [reportRedeclaration] - | _MISSING_TYPE = MISSING, - ) -> None: - """Initialize the field. - - Args: - default: The default value for the field. - default_factory: The default factory for the field. - is_var: Whether the field is a Var. - annotated_type: The annotated type for the field. - """ - self.default = default - self.default_factory = default_factory - self.is_var = is_var - if annotated_type is not MISSING: - type_origin = get_origin(annotated_type) or annotated_type - if type_origin is Field and ( - args := getattr(annotated_type, "__args__", None) - ): - annotated_type: GenericType = args[0] - type_origin = get_origin(annotated_type) or annotated_type - - if self.default is MISSING and self.default_factory is None: - default_value = types.get_default_value_for_type(annotated_type) - if default_value is None and not types.is_optional(annotated_type): - annotated_type = annotated_type | None - if types.is_immutable(default_value): - self.default = default_value - else: - self.default_factory = functools.partial( - copy.deepcopy, default_value - ) - self.outer_type_ = self.annotated_type = annotated_type - - if type_origin is Annotated: - type_origin = annotated_type.__origin__ # pyright: ignore [reportAttributeAccessIssue] - - self.type_ = self.type_origin = type_origin - else: - self.outer_type_ = self.annotated_type = self.type_ = self.type_origin = Any - - def default_value(self) -> FIELD_TYPE: - """Get the default value for the field. - - Returns: - The default value for the field. - - Raises: - ValueError: If no default value or factory is provided. - """ - if self.default is not MISSING: - return self.default - if self.default_factory is not None: - return self.default_factory() - msg = "No default value or factory provided." - raise ValueError(msg) - - def __repr__(self) -> str: - """Represent the field in a readable format. - - Returns: - The string representation of the field. - """ - annotated_type_str = ( - f", annotated_type={self.annotated_type!r}" - if self.annotated_type is not MISSING - else "" - ) - if self.default is not MISSING: - return f"Field(default={self.default!r}, is_var={self.is_var}{annotated_type_str})" - return f"Field(default_factory={self.default_factory!r}, is_var={self.is_var}{annotated_type_str})" - - if TYPE_CHECKING: - - def __set__(self, instance: Any, value: FIELD_TYPE): - """Set the Var. - - Args: - instance: The instance of the class setting the Var. - value: The value to set the Var to. - - # noqa: DAR101 self - """ - - @overload - def __get__(self: Field[None], instance: None, owner: Any) -> NoneVar: ... - - @overload - def __get__( - self: Field[bool] | Field[bool | None], instance: None, owner: Any - ) -> BooleanVar: ... - - @overload - def __get__( - self: Field[int] | Field[int | None], - instance: None, - owner: Any, - ) -> NumberVar[int]: ... - - @overload - def __get__( - self: Field[float] - | Field[int | float] - | Field[float | None] - | Field[int | float | None], - instance: None, - owner: Any, - ) -> NumberVar: ... - - @overload - def __get__( - self: Field[str] | Field[str | None], instance: None, owner: Any - ) -> StringVar: ... - - @overload - def __get__( - self: Field[list[V]] - | Field[set[V]] - | Field[list[V] | None] - | Field[set[V] | None], - instance: None, - owner: Any, - ) -> ArrayVar[Sequence[V]]: ... - - @overload - def __get__( - self: Field[SEQUENCE_TYPE] | Field[SEQUENCE_TYPE | None], - instance: None, - owner: Any, - ) -> ArrayVar[SEQUENCE_TYPE]: ... - - @overload - def __get__( - self: Field[MAPPING_TYPE] | Field[MAPPING_TYPE | None], - instance: None, - owner: Any, - ) -> ObjectVar[MAPPING_TYPE]: ... - - @overload - def __get__( - self: Field[BASE_TYPE] | Field[BASE_TYPE | None], instance: None, owner: Any - ) -> ObjectVar[BASE_TYPE]: ... - - @overload - def __get__( - self: Field[SQLA_TYPE] | Field[SQLA_TYPE | None], instance: None, owner: Any - ) -> ObjectVar[SQLA_TYPE]: ... - - if TYPE_CHECKING: - - @overload - def __get__( - self: Field[DATACLASS_TYPE] | Field[DATACLASS_TYPE | None], - instance: None, - owner: Any, - ) -> ObjectVar[DATACLASS_TYPE]: ... - - @overload - def __get__(self, instance: None, owner: Any) -> Var[FIELD_TYPE]: ... - - @overload - def __get__(self, instance: Any, owner: Any) -> FIELD_TYPE: ... - - def __get__(self, instance: Any, owner: Any): # pyright: ignore [reportInconsistentOverload] - """Get the Var. - - Args: - instance: The instance of the class accessing the Var. - owner: The class that the Var is attached to. - """ - - -@overload -def field( - default: FIELD_TYPE | _MISSING_TYPE = MISSING, - *, - is_var: Literal[False], - default_factory: Callable[[], FIELD_TYPE] | None = None, -) -> FIELD_TYPE: ... - - -@overload -def field( - default: FIELD_TYPE | _MISSING_TYPE = MISSING, - *, - default_factory: Callable[[], FIELD_TYPE] | None = None, - is_var: Literal[True] = True, -) -> Field[FIELD_TYPE]: ... - - -def field( - default: FIELD_TYPE | _MISSING_TYPE = MISSING, - *, - default_factory: Callable[[], FIELD_TYPE] | None = None, - is_var: bool = True, -) -> Field[FIELD_TYPE] | FIELD_TYPE: - """Create a field for a state. - - Args: - default: The default value for the field. - default_factory: The default factory for the field. - is_var: Whether the field is a Var. - - Returns: - The field for the state. - - Raises: - ValueError: If both default and default_factory are specified. - """ - if default is not MISSING and default_factory is not None: - msg = "cannot specify both default and default_factory" - raise ValueError(msg) - if default is not MISSING and not types.is_immutable(default): - console.warn( - "Mutable default values are not recommended. " - "Use default_factory instead to avoid unexpected behavior." - ) - return Field( - default_factory=functools.partial(copy.deepcopy, default), - is_var=is_var, - ) - return Field( - default=default, - default_factory=default_factory, - is_var=is_var, - ) - - -@dataclass_transform(kw_only_default=True, field_specifiers=(field,)) -class BaseStateMeta(ABCMeta): - """Meta class for BaseState.""" - - if TYPE_CHECKING: - __inherited_fields__: Mapping[str, Field] - __own_fields__: dict[str, Field] - __fields__: dict[str, Field] - - # Whether this state class is a mixin and should not be instantiated. - _mixin: bool = False - - def __new__( - cls, - name: str, - bases: tuple[type, ...], - namespace: dict[str, Any], - mixin: bool = False, - ) -> type: - """Create a new class. - - Args: - name: The name of the class. - bases: The bases of the class. - namespace: The namespace of the class. - mixin: Whether the class is a mixin and should not be instantiated. - - Returns: - The new class. - """ - state_bases = [ - base for base in bases if issubclass(base, EvenMoreBasicBaseState) - ] - mixin = mixin or ( - bool(state_bases) and all(base._mixin for base in state_bases) - ) - # Add the field to the class - inherited_fields: dict[str, Field] = {} - own_fields: dict[str, Field] = {} - resolved_annotations = types.resolve_annotations( - annotations_from_namespace(namespace), namespace["__module__"] - ) - - for base in bases[::-1]: - if hasattr(base, "__inherited_fields__"): - inherited_fields.update(base.__inherited_fields__) - for base in bases[::-1]: - if hasattr(base, "__own_fields__"): - inherited_fields.update(base.__own_fields__) - - for key, value in [ - (key, value) - for key, value in namespace.items() - if key not in resolved_annotations - ]: - if isinstance(value, Field): - if value.annotated_type is not Any: - new_value = value - elif value.default is not MISSING: - new_value = Field( - default=value.default, - is_var=value.is_var, - annotated_type=figure_out_type(value.default), - ) - else: - new_value = Field( - default_factory=value.default_factory, - is_var=value.is_var, - annotated_type=Any, - ) - elif ( - not key.startswith("__") - and not callable(value) - and not isinstance(value, (staticmethod, classmethod, property, Var)) - ): - if types.is_immutable(value): - new_value = Field( - default=value, - annotated_type=figure_out_type(value), - ) - else: - new_value = Field( - default_factory=functools.partial(copy.deepcopy, value), - annotated_type=figure_out_type(value), - ) - else: - continue - - own_fields[key] = new_value - - for key, annotation in resolved_annotations.items(): - value = namespace.get(key, MISSING) - - if types.is_classvar(annotation): - # If the annotation is a classvar, skip it. - continue - - if value is MISSING: - value = Field( - annotated_type=annotation, - ) - elif not isinstance(value, Field): - if types.is_immutable(value): - value = Field( - default=value, - annotated_type=annotation, - ) - else: - value = Field( - default_factory=functools.partial(copy.deepcopy, value), - annotated_type=annotation, - ) - else: - value = Field( - default=value.default, - default_factory=value.default_factory, - is_var=value.is_var, - annotated_type=annotation, - ) - - own_fields[key] = value - - namespace["__own_fields__"] = own_fields - namespace["__inherited_fields__"] = inherited_fields - namespace["__fields__"] = inherited_fields | own_fields - namespace["_mixin"] = mixin - return super().__new__(cls, name, bases, namespace) - - -class EvenMoreBasicBaseState(metaclass=BaseStateMeta): - """A simplified base state class that provides basic functionality.""" - - def __init__( - self, - **kwargs, - ): - """Initialize the state with the given kwargs. - - Args: - **kwargs: The kwargs to pass to the state. - """ - super().__init__() - for key, value in kwargs.items(): - object.__setattr__(self, key, value) - for name, value in type(self).get_fields().items(): - if name not in kwargs: - default_value = value.default_value() - object.__setattr__(self, name, default_value) - - def set(self, **kwargs): - """Mutate the state by setting the given kwargs. Returns the state. - - Args: - **kwargs: The kwargs to set. - - Returns: - The state with the fields set to the given kwargs. - """ - for key, value in kwargs.items(): - setattr(self, key, value) - return self - - @classmethod - def get_fields(cls) -> Mapping[str, Field]: - """Get the fields of the component. - - Returns: - The fields of the component. - """ - return cls.__fields__ - - @classmethod - def add_field(cls, name: str, var: Var, default_value: Any): - """Add a field to the class after class definition. - - Used by State.add_var() to correctly handle the new variable. - - Args: - name: The name of the field to add. - var: The variable to add a field for. - default_value: The default value of the field. - """ - if types.is_immutable(default_value): - new_field = Field( - default=default_value, - annotated_type=var._var_type, - ) - else: - new_field = Field( - default_factory=functools.partial(copy.deepcopy, default_value), - annotated_type=var._var_type, - ) - cls.__fields__[name] = new_field +from reflex_core.vars.base import * diff --git a/reflex/vars/color.py b/reflex/vars/color.py index e3c3f7d5c1b..7fdf802e9b4 100644 --- a/reflex/vars/color.py +++ b/reflex/vars/color.py @@ -1,166 +1,3 @@ -"""Vars for colors.""" +"""Re-export from reflex_core.""" -import dataclasses - -from reflex.constants.colors import Color -from reflex.vars.base import ( - CachedVarOperation, - LiteralVar, - Var, - VarData, - cached_property_no_lock, - get_python_literal, -) -from reflex.vars.number import ternary_operation -from reflex.vars.sequence import ConcatVarOperation, LiteralStringVar, StringVar - - -class ColorVar(StringVar[Color], python_types=Color): - """Base class for immutable color vars.""" - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralColorVar(CachedVarOperation, LiteralVar, ColorVar): - """Base class for immutable literal color vars.""" - - _var_value: Color = dataclasses.field(default_factory=lambda: Color(color="black")) - - @classmethod - def _get_all_var_data_without_creating_var( - cls, - value: Color, - ) -> VarData | None: - return VarData.merge( - LiteralStringVar._get_all_var_data_without_creating_var(value.color) - if isinstance(value.color, str) - else value.color._get_all_var_data(), - value.alpha._get_all_var_data() - if not isinstance(value.alpha, bool) - else None, - value.shade._get_all_var_data() - if not isinstance(value.shade, int) - else None, - ) - - @classmethod - def create( - cls, - value: Color, - _var_type: type[Color] | None = None, - _var_data: VarData | None = None, - ) -> ColorVar: - """Create a var from a string value. - - Args: - value: The value to create the var from. - _var_type: The type of the var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _js_expr="", - _var_type=_var_type or Color, - _var_data=_var_data, - _var_value=value, - ) - - def __hash__(self) -> int: - """Get the hash of the var. - - Returns: - The hash of the var. - """ - return hash(( - self.__class__.__name__, - self._var_value.color, - self._var_value.alpha, - self._var_value.shade, - )) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - alpha = self._var_value.alpha - alpha = ( - ternary_operation( - alpha, - LiteralStringVar.create("a"), - LiteralStringVar.create(""), - ) - if isinstance(alpha, Var) - else LiteralStringVar.create("a" if alpha else "") - ) - - shade = self._var_value.shade - shade = ( - shade.to_string(use_json=False) - if isinstance(shade, Var) - else LiteralStringVar.create(str(shade)) - ) - return str( - ConcatVarOperation.create( - LiteralStringVar.create("var(--"), - self._var_value.color, - LiteralStringVar.create("-"), - alpha, - shade, - LiteralStringVar.create(")"), - ) - ) - - @cached_property_no_lock - def _cached_get_all_var_data(self) -> VarData | None: - """Get all the var data. - - Returns: - The var data. - """ - return VarData.merge( - LiteralStringVar._get_all_var_data_without_creating_var( - self._var_value.color - ) - if isinstance(self._var_value.color, str) - else self._var_value.color._get_all_var_data(), - self._var_value.alpha._get_all_var_data() - if not isinstance(self._var_value.alpha, bool) - else None, - self._var_value.shade._get_all_var_data() - if not isinstance(self._var_value.shade, int) - else None, - self._var_data, - ) - - def json(self) -> str: - """Get the JSON representation of the var. - - Returns: - The JSON representation of the var. - - Raises: - TypeError: If the color is not a valid color. - """ - color, alpha, shade = map( - get_python_literal, - (self._var_value.color, self._var_value.alpha, self._var_value.shade), - ) - if color is None or alpha is None or shade is None: - msg = "Cannot serialize color that contains non-literal vars." - raise TypeError(msg) - if ( - not isinstance(color, str) - or not isinstance(alpha, bool) - or not isinstance(shade, int) - ): - msg = "Color is not a valid color." - raise TypeError(msg) - return f"var(--{color}-{'a' if alpha else ''}{shade})" +from reflex_core.vars.color import * diff --git a/reflex/vars/datetime.py b/reflex/vars/datetime.py index 89a787e3e06..da48d048787 100644 --- a/reflex/vars/datetime.py +++ b/reflex/vars/datetime.py @@ -1,202 +1,3 @@ -"""Immutable datetime and date vars.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import dataclasses -from datetime import date, datetime -from typing import Any, TypeVar - -from reflex.utils.exceptions import VarTypeError -from reflex.vars.number import BooleanVar - -from .base import ( - CustomVarOperationReturn, - LiteralVar, - Var, - VarData, - var_operation, - var_operation_return, -) - -DATETIME_T = TypeVar("DATETIME_T", datetime, date) - -datetime_types = datetime | date - - -def raise_var_type_error(): - """Raise a VarTypeError. - - Raises: - VarTypeError: Cannot compare a datetime object with a non-datetime object. - """ - msg = "Cannot compare a datetime object with a non-datetime object." - raise VarTypeError(msg) - - -class DateTimeVar(Var[DATETIME_T], python_types=(datetime, date)): - """A variable that holds a datetime or date object.""" - - def __lt__(self, other: datetime_types | DateTimeVar) -> BooleanVar: - """Less than comparison. - - Args: - other: The other datetime to compare. - - Returns: - The result of the comparison. - """ - if not isinstance(other, DATETIME_TYPES): - raise_var_type_error() - return date_lt_operation(self, other) - - def __le__(self, other: datetime_types | DateTimeVar) -> BooleanVar: - """Less than or equal comparison. - - Args: - other: The other datetime to compare. - - Returns: - The result of the comparison. - """ - if not isinstance(other, DATETIME_TYPES): - raise_var_type_error() - return date_le_operation(self, other) - - def __gt__(self, other: datetime_types | DateTimeVar) -> BooleanVar: - """Greater than comparison. - - Args: - other: The other datetime to compare. - - Returns: - The result of the comparison. - """ - if not isinstance(other, DATETIME_TYPES): - raise_var_type_error() - return date_gt_operation(self, other) - - def __ge__(self, other: datetime_types | DateTimeVar) -> BooleanVar: - """Greater than or equal comparison. - - Args: - other: The other datetime to compare. - - Returns: - The result of the comparison. - """ - if not isinstance(other, DATETIME_TYPES): - raise_var_type_error() - return date_ge_operation(self, other) - - -@var_operation -def date_gt_operation(lhs: DateTimeVar | Any, rhs: DateTimeVar | Any): - """Greater than comparison. - - Args: - lhs: The left-hand side of the operation. - rhs: The right-hand side of the operation. - - Returns: - The result of the operation. - """ - return date_compare_operation(rhs, lhs, strict=True) - - -@var_operation -def date_lt_operation(lhs: DateTimeVar | Any, rhs: DateTimeVar | Any): - """Less than comparison. - - Args: - lhs: The left-hand side of the operation. - rhs: The right-hand side of the operation. - - Returns: - The result of the operation. - """ - return date_compare_operation(lhs, rhs, strict=True) - - -@var_operation -def date_le_operation(lhs: DateTimeVar | Any, rhs: DateTimeVar | Any): - """Less than or equal comparison. - - Args: - lhs: The left-hand side of the operation. - rhs: The right-hand side of the operation. - - Returns: - The result of the operation. - """ - return date_compare_operation(lhs, rhs) - - -@var_operation -def date_ge_operation(lhs: DateTimeVar | Any, rhs: DateTimeVar | Any): - """Greater than or equal comparison. - - Args: - lhs: The left-hand side of the operation. - rhs: The right-hand side of the operation. - - Returns: - The result of the operation. - """ - return date_compare_operation(rhs, lhs) - - -def date_compare_operation( - lhs: DateTimeVar[DATETIME_T] | Any, - rhs: DateTimeVar[DATETIME_T] | Any, - strict: bool = False, -) -> CustomVarOperationReturn[bool]: - """Check if the value is less than the other value. - - Args: - lhs: The left-hand side of the operation. - rhs: The right-hand side of the operation. - strict: Whether to use strict comparison. - - Returns: - The result of the operation. - """ - return var_operation_return( - f"({lhs} {'<' if strict else '<='} {rhs})", - bool, - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralDatetimeVar(LiteralVar, DateTimeVar): - """Base class for immutable datetime and date vars.""" - - _var_value: date = dataclasses.field(default=datetime.now()) - - @classmethod - def _get_all_var_data_without_creating_var(cls, value: date) -> VarData | None: - return None - - @classmethod - def create(cls, value: date, _var_data: VarData | None = None): - """Create a new instance of the class. - - Args: - value: The value to set. - - Returns: - LiteralDatetimeVar: The new instance of the class. - """ - js_expr = f'"{value!s}"' - return cls( - _js_expr=js_expr, - _var_type=type(value), - _var_value=value, - _var_data=_var_data, - ) - - -DATETIME_TYPES = (datetime, date, DateTimeVar) +from reflex_core.vars.datetime import * diff --git a/reflex/vars/dep_tracking.py b/reflex/vars/dep_tracking.py index 2315eb9f814..4d65fca8fe6 100644 --- a/reflex/vars/dep_tracking.py +++ b/reflex/vars/dep_tracking.py @@ -1,485 +1,3 @@ -"""Collection of base classes.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import contextlib -import dataclasses -import dis -import enum -import importlib -import inspect -import sys -from types import CellType, CodeType, FunctionType, ModuleType -from typing import TYPE_CHECKING, Any, ClassVar, cast - -from reflex.utils.exceptions import VarValueError - -if TYPE_CHECKING: - from reflex.state import BaseState - - from .base import Var - - -CellEmpty = object() - - -def get_cell_value(cell: CellType) -> Any: - """Get the value of a cell object. - - Args: - cell: The cell object to get the value from. (func.__closure__ objects) - - Returns: - The value from the cell or CellEmpty if a ValueError is raised. - """ - try: - return cell.cell_contents - except ValueError: - return CellEmpty - - -class ScanStatus(enum.Enum): - """State of the dis instruction scanning loop.""" - - SCANNING = enum.auto() - GETTING_ATTR = enum.auto() - GETTING_STATE = enum.auto() - GETTING_STATE_POST_AWAIT = enum.auto() - GETTING_VAR = enum.auto() - GETTING_IMPORT = enum.auto() - - -class UntrackedLocalVarError(VarValueError): - """Raised when a local variable is referenced, but it is not tracked in the current scope.""" - - -def assert_base_state( - local_value: Any, - local_name: str | None = None, -) -> type[BaseState]: - """Assert that a local variable is a BaseState subclass. - - Args: - local_value: The value of the local variable to check. - local_name: The name of the local variable to check. - - Returns: - The local variable value if it is a BaseState subclass. - - Raises: - VarValueError: If the object is not a BaseState subclass. - """ - from reflex.state import BaseState - - if not isinstance(local_value, type) or not issubclass(local_value, BaseState): - msg = f"Cannot determine dependencies in fetched state {local_name!r}: {local_value!r} is not a BaseState." - raise VarValueError(msg) - return local_value - - -@dataclasses.dataclass -class DependencyTracker: - """State machine for identifying state attributes that are accessed by a function.""" - - func: FunctionType | CodeType = dataclasses.field() - state_cls: type[BaseState] = dataclasses.field() - - dependencies: dict[str, set[str]] = dataclasses.field(default_factory=dict) - - scan_status: ScanStatus = dataclasses.field(default=ScanStatus.SCANNING) - top_of_stack: str | None = dataclasses.field(default=None) - - tracked_locals: dict[str, type[BaseState] | ModuleType] = dataclasses.field( - default_factory=dict - ) - - _getting_state_class: type[BaseState] | ModuleType | None = dataclasses.field( - default=None - ) - _get_var_value_positions: dis.Positions | None = dataclasses.field(default=None) - _last_import_name: str | None = dataclasses.field(default=None) - - INVALID_NAMES: ClassVar[list[str]] = ["parent_state", "substates", "get_substate"] - - def __post_init__(self): - """After initializing, populate the dependencies dict.""" - with contextlib.suppress(AttributeError): - # unbox functools.partial - self.func = cast(FunctionType, self.func.func) # pyright: ignore[reportAttributeAccessIssue] - with contextlib.suppress(AttributeError): - # unbox EventHandler - self.func = cast(FunctionType, self.func.fn) # pyright: ignore[reportAttributeAccessIssue,reportFunctionMemberAccess] - - if isinstance(self.func, FunctionType): - with contextlib.suppress(AttributeError, IndexError): - # the first argument to the function is the name of "self" arg - self.tracked_locals[self.func.__code__.co_varnames[0]] = self.state_cls - - self._populate_dependencies() - - def _merge_deps(self, tracker: DependencyTracker) -> None: - """Merge dependencies from another DependencyTracker. - - Args: - tracker: The DependencyTracker to merge dependencies from. - """ - for state_name, dep_name in tracker.dependencies.items(): - self.dependencies.setdefault(state_name, set()).update(dep_name) - - def get_tracked_local(self, local_name: str) -> type[BaseState] | ModuleType: - """Get the value of a local name tracked in the current function scope. - - Args: - local_name: The name of the local variable to fetch. - - Returns: - The value of local name tracked in the current scope (a referenced - BaseState subclass or imported module). - - Raises: - UntrackedLocalVarError: If the local variable is not being tracked. - """ - try: - local_value = self.tracked_locals[local_name] - except KeyError as ke: - msg = f"{local_name!r} is not tracked in the current scope." - raise UntrackedLocalVarError(msg) from ke - return local_value - - def load_attr_or_method(self, instruction: dis.Instruction) -> None: - """Handle loading an attribute or method from the object on top of the stack. - - This method directly tracks attributes and recursively merges - dependencies from analyzing the dependencies of any methods called. - - Args: - instruction: The dis instruction to process. - - Raises: - VarValueError: if the attribute is an disallowed name or attribute - does not reference a BaseState. - """ - from .base import ComputedVar - - if instruction.argval in self.INVALID_NAMES: - msg = f"Cached var {self!s} cannot access arbitrary state via `{instruction.argval}`." - raise VarValueError(msg) - if instruction.argval == "get_state": - # Special case: arbitrary state access requested. - self.scan_status = ScanStatus.GETTING_STATE - return - if instruction.argval == "get_var_value": - # Special case: arbitrary var access requested. - if sys.version_info >= (3, 11): - self._get_var_value_positions = instruction.positions - self.scan_status = ScanStatus.GETTING_VAR - return - - # Reset status back to SCANNING after attribute is accessed. - self.scan_status = ScanStatus.SCANNING - if not self.top_of_stack: - return - target_obj = self.get_tracked_local(self.top_of_stack) - try: - target_state = assert_base_state(target_obj, local_name=self.top_of_stack) - except VarValueError: - # If the target state is not a BaseState, we cannot track dependencies on it. - return - try: - ref_obj = getattr(target_state, instruction.argval) - except AttributeError: - # Not found on this state class, maybe it is a dynamic attribute that will be picked up later. - ref_obj = None - - if isinstance(ref_obj, property) and not isinstance(ref_obj, ComputedVar): - # recurse into property fget functions - ref_obj = ref_obj.fget - if callable(ref_obj): - # recurse into callable attributes - self._merge_deps( - type(self)(func=cast(FunctionType, ref_obj), state_cls=target_state) - ) - elif ( - instruction.argval in target_state.backend_vars - or instruction.argval in target_state.vars - ): - # var access - self.dependencies.setdefault(target_state.get_full_name(), set()).add( - instruction.argval - ) - - def _get_globals(self) -> dict[str, Any]: - """Get the globals of the function. - - Returns: - The var names and values in the globals of the function. - """ - if isinstance(self.func, CodeType): - return {} - return self.func.__globals__ # pyright: ignore[reportAttributeAccessIssue] - - def _get_closure(self) -> dict[str, Any]: - """Get the closure of the function, with unbound values omitted. - - Returns: - The var names and values in the closure of the function. - """ - if isinstance(self.func, CodeType): - return {} - return { - var_name: get_cell_value(cell) - for var_name, cell in zip( - self.func.__code__.co_freevars, # pyright: ignore[reportAttributeAccessIssue] - self.func.__closure__ or (), - strict=False, - ) - if get_cell_value(cell) is not CellEmpty - } - - def handle_getting_state(self, instruction: dis.Instruction) -> None: - """Handle bytecode analysis when `get_state` was called in the function. - - If the wrapped function is getting an arbitrary state and saving it to a - local variable, this method associates the local variable name with the - state class in self.tracked_locals. - - When an attribute/method is accessed on a tracked local, it will be - associated with this state. - - Args: - instruction: The dis instruction to process. - - Raises: - VarValueError: if the state class cannot be determined from the instruction. - """ - if isinstance(self.func, CodeType): - msg = "Dependency detection cannot identify get_state class from a code object." - raise VarValueError(msg) - if instruction.opname in ("LOAD_FAST", "LOAD_FAST_BORROW"): - self._getting_state_class = self.get_tracked_local( - local_name=instruction.argval, - ) - elif instruction.opname == "LOAD_GLOBAL": - # Special case: referencing state class from global scope. - try: - self._getting_state_class = self._get_globals()[instruction.argval] - except (ValueError, KeyError) as ve: - msg = f"Cached var {self!s} cannot access arbitrary state `{instruction.argval}`, not found in globals." - raise VarValueError(msg) from ve - elif instruction.opname == "LOAD_DEREF": - # Special case: referencing state class from closure. - try: - self._getting_state_class = self._get_closure()[instruction.argval] - except (ValueError, KeyError) as ve: - msg = f"Cached var {self!s} cannot access arbitrary state `{instruction.argval}`, is it defined yet?" - raise VarValueError(msg) from ve - elif instruction.opname in ("LOAD_ATTR", "LOAD_METHOD"): - self._getting_state_class = getattr( - self._getting_state_class, - instruction.argval, - ) - elif instruction.opname == "GET_AWAITABLE": - # Now inside the `await` machinery, subsequent instructions - # operate on the result of the `get_state` call. - self.scan_status = ScanStatus.GETTING_STATE_POST_AWAIT - if self._getting_state_class is not None: - self.top_of_stack = "_" - self.tracked_locals[self.top_of_stack] = self._getting_state_class - self._getting_state_class = None - - def handle_getting_state_post_await(self, instruction: dis.Instruction) -> None: - """Handle bytecode analysis after `get_state` was called in the function. - - This function is called _after_ awaiting self.get_state to capture the - local variable holding the state instance or directly record access to - attributes accessed on the result of get_state. - - Args: - instruction: The dis instruction to process. - - Raises: - VarValueError: if the state class cannot be determined from the instruction. - """ - if instruction.opname == "STORE_FAST" and self.top_of_stack: - # Storing the result of get_state in a local variable. - self.tracked_locals[instruction.argval] = self.tracked_locals.pop( - self.top_of_stack - ) - self.top_of_stack = None - self.scan_status = ScanStatus.SCANNING - elif instruction.opname in ("LOAD_ATTR", "LOAD_METHOD"): - # Attribute access on an inline `get_state`, not assigned to a variable. - self.load_attr_or_method(instruction) - - def _eval_var(self, positions: dis.Positions) -> Var: - """Evaluate instructions from the wrapped function to get the Var object. - - Args: - positions: The disassembly positions of the get_var_value call. - - Returns: - The Var object. - - Raises: - VarValueError: if the source code for the var cannot be determined. - """ - # Get the original source code and eval it to get the Var. - module = inspect.getmodule(self.func) - if module is None or self._get_var_value_positions is None: - msg = f"Cannot determine the source code for the var in {self.func!r}." - raise VarValueError(msg) - start_line = self._get_var_value_positions.end_lineno - start_column = self._get_var_value_positions.end_col_offset - end_line = positions.end_lineno - end_column = positions.end_col_offset - if ( - start_line is None - or start_column is None - or end_line is None - or end_column is None - ): - msg = f"Cannot determine the source code for the var in {self.func!r}." - raise VarValueError(msg) - source = inspect.getsource(module).splitlines(True)[start_line - 1 : end_line] - # Create a python source string snippet. - if len(source) > 1: - snipped_source = "".join([ - *source[0][start_column:], - *source[1:-1], - *source[-1][:end_column], - ]) - else: - snipped_source = source[0][start_column:end_column] - # Evaluate the string in the context of the function's globals, closure and tracked local scope. - return eval( - f"({snipped_source})", - self._get_globals(), - {**self._get_closure(), **self.tracked_locals}, - ) - - def handle_getting_var(self, instruction: dis.Instruction) -> None: - """Handle bytecode analysis when `get_var_value` was called in the function. - - This only really works if the expression passed to `get_var_value` is - evaluable in the function's global scope or closure, so getting the var - value from a var saved in a local variable or in the class instance is - not possible. - - Args: - instruction: The dis instruction to process. - - Raises: - VarValueError: if the source code for the var cannot be determined. - """ - if instruction.opname == "CALL": - if instruction.positions is None: - msg = f"Cannot determine the source code for the var in {self.func!r}." - raise VarValueError(msg) - the_var = self._eval_var(instruction.positions) - the_var_data = the_var._get_all_var_data() - if the_var_data is None: - msg = f"Cannot determine the source code for the var in {self.func!r}." - raise VarValueError(msg) - self.dependencies.setdefault(the_var_data.state, set()).add( - the_var_data.field_name - ) - self.scan_status = ScanStatus.SCANNING - - def _populate_dependencies(self) -> None: - """Update self.dependencies based on the disassembly of self.func. - - Save references to attributes accessed on "self" or other fetched states. - - Recursively called when the function makes a method call on "self" or - define comprehensions or nested functions that may reference "self". - """ - for instruction in dis.get_instructions(self.func): - if self.scan_status == ScanStatus.GETTING_STATE: - self.handle_getting_state(instruction) - elif self.scan_status == ScanStatus.GETTING_STATE_POST_AWAIT: - self.handle_getting_state_post_await(instruction) - elif self.scan_status == ScanStatus.GETTING_VAR: - self.handle_getting_var(instruction) - elif ( - instruction.opname - in ( - "LOAD_FAST", - "LOAD_DEREF", - "LOAD_FAST_BORROW", - "LOAD_FAST_CHECK", - "LOAD_FAST_AND_CLEAR", - ) - and instruction.argval in self.tracked_locals - ): - # bytecode loaded the class instance to the top of stack, next load instruction - # is referencing an attribute on self - self.top_of_stack = instruction.argval - self.scan_status = ScanStatus.GETTING_ATTR - elif ( - instruction.opname - in ( - "LOAD_FAST_LOAD_FAST", - "LOAD_FAST_BORROW_LOAD_FAST_BORROW", - "STORE_FAST_LOAD_FAST", - ) - and instruction.argval[-1] in self.tracked_locals - ): - # Double LOAD_FAST family instructions load multiple values onto the stack, - # the last value in the argval list is the top of the stack. - self.top_of_stack = instruction.argval[-1] - self.scan_status = ScanStatus.GETTING_ATTR - elif self.scan_status == ScanStatus.GETTING_ATTR and instruction.opname in ( - "LOAD_ATTR", - "LOAD_METHOD", - ): - self.load_attr_or_method(instruction) - self.top_of_stack = None - elif instruction.opname == "LOAD_CONST" and isinstance( - instruction.argval, CodeType - ): - # recurse into nested functions / comprehensions, which can reference - # instance attributes from the outer scope - self._merge_deps( - type(self)( - func=instruction.argval, - state_cls=self.state_cls, - tracked_locals=self.tracked_locals, - ) - ) - elif instruction.opname == "IMPORT_NAME" and instruction.argval is not None: - self.scan_status = ScanStatus.GETTING_IMPORT - self._last_import_name = instruction.argval - importlib.import_module(instruction.argval) - top_module_name = instruction.argval.split(".")[0] - self.tracked_locals[instruction.argval] = sys.modules[top_module_name] - self.top_of_stack = instruction.argval - elif instruction.opname == "IMPORT_FROM": - if not self._last_import_name: - msg = f"Cannot find package associated with import {instruction.argval} in {self.func!r}." - raise VarValueError(msg) - if instruction.argval in self._last_import_name.split("."): - # `import ... as ...` case: - # import from interim package, update tracked_locals for the last imported name. - self.tracked_locals[self._last_import_name] = getattr( - self.tracked_locals[self._last_import_name], instruction.argval - ) - continue - # Importing a name from a package/module. - if self._last_import_name is not None and self.top_of_stack: - # The full import name does NOT end up in scope for a `from ... import`. - self.tracked_locals.pop(self._last_import_name) - self.tracked_locals[instruction.argval] = getattr( - importlib.import_module(self._last_import_name), - instruction.argval, - ) - # If we see a STORE_FAST, we can assign the top of stack to an aliased name. - self.top_of_stack = instruction.argval - elif ( - self.scan_status == ScanStatus.GETTING_IMPORT - and instruction.opname == "STORE_FAST" - and self.top_of_stack is not None - ): - self.tracked_locals[instruction.argval] = self.tracked_locals.pop( - self.top_of_stack - ) - self.top_of_stack = None +from reflex_core.vars.dep_tracking import * diff --git a/reflex/vars/function.py b/reflex/vars/function.py index 709b9b03b05..4343403cbc4 100644 --- a/reflex/vars/function.py +++ b/reflex/vars/function.py @@ -1,555 +1,3 @@ -"""Immutable function vars.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import dataclasses -from collections.abc import Callable, Sequence -from typing import Any, Concatenate, Generic, ParamSpec, Protocol, TypeVar, overload - -from reflex.utils import format -from reflex.utils.types import GenericType - -from .base import CachedVarOperation, LiteralVar, Var, VarData, cached_property_no_lock - -P = ParamSpec("P") -V1 = TypeVar("V1") -V2 = TypeVar("V2") -V3 = TypeVar("V3") -V4 = TypeVar("V4") -V5 = TypeVar("V5") -V6 = TypeVar("V6") -R = TypeVar("R") - - -class ReflexCallable(Protocol[P, R]): - """Protocol for a callable.""" - - __call__: Callable[P, R] - - -CALLABLE_TYPE = TypeVar("CALLABLE_TYPE", bound=ReflexCallable, covariant=True) -OTHER_CALLABLE_TYPE = TypeVar( - "OTHER_CALLABLE_TYPE", bound=ReflexCallable, covariant=True -) - - -def _is_js_identifier_start(char: str) -> bool: - """Check whether a character can start a JavaScript identifier. - - Returns: - True if the character is valid as the first character of a JS identifier. - """ - return char == "$" or char == "_" or char.isalpha() - - -def _is_js_identifier_char(char: str) -> bool: - """Check whether a character can continue a JavaScript identifier. - - Returns: - True if the character is valid within a JS identifier. - """ - return _is_js_identifier_start(char) or char.isdigit() - - -def _starts_with_arrow_function(expr: str) -> bool: - """Check whether an expression starts with an inline arrow function. - - Returns: - True if the expression begins with an arrow function. - """ - if "=>" not in expr: - return False - - expr = expr.lstrip() - if not expr: - return False - - if expr.startswith("async"): - async_remainder = expr[len("async") :] - if async_remainder[:1].isspace(): - expr = async_remainder.lstrip() - - if not expr: - return False - - if _is_js_identifier_start(expr[0]): - end_index = 1 - while end_index < len(expr) and _is_js_identifier_char(expr[end_index]): - end_index += 1 - return expr[end_index:].lstrip().startswith("=>") - - if not expr.startswith("("): - return False - - depth = 0 - string_delimiter: str | None = None - escaped = False - - for index, char in enumerate(expr): - if string_delimiter is not None: - if escaped: - escaped = False - elif char == "\\": - escaped = True - elif char == string_delimiter: - string_delimiter = None - continue - - if char in {"'", '"', "`"}: - string_delimiter = char - continue - - if char == "(": - depth += 1 - continue - - if char == ")": - depth -= 1 - if depth == 0: - return expr[index + 1 :].lstrip().startswith("=>") - - return False - - -class FunctionVar(Var[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any]): - """Base class for immutable function vars.""" - - @overload - def partial(self) -> FunctionVar[CALLABLE_TYPE]: ... - - @overload - def partial( - self: FunctionVar[ReflexCallable[Concatenate[V1, P], R]], - arg1: V1 | Var[V1], - ) -> FunctionVar[ReflexCallable[P, R]]: ... - - @overload - def partial( - self: FunctionVar[ReflexCallable[Concatenate[V1, V2, P], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - ) -> FunctionVar[ReflexCallable[P, R]]: ... - - @overload - def partial( - self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, P], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - arg3: V3 | Var[V3], - ) -> FunctionVar[ReflexCallable[P, R]]: ... - - @overload - def partial( - self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, V4, P], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - arg3: V3 | Var[V3], - arg4: V4 | Var[V4], - ) -> FunctionVar[ReflexCallable[P, R]]: ... - - @overload - def partial( - self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, V4, V5, P], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - arg3: V3 | Var[V3], - arg4: V4 | Var[V4], - arg5: V5 | Var[V5], - ) -> FunctionVar[ReflexCallable[P, R]]: ... - - @overload - def partial( - self: FunctionVar[ReflexCallable[Concatenate[V1, V2, V3, V4, V5, V6, P], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - arg3: V3 | Var[V3], - arg4: V4 | Var[V4], - arg5: V5 | Var[V5], - arg6: V6 | Var[V6], - ) -> FunctionVar[ReflexCallable[P, R]]: ... - - @overload - def partial( - self: FunctionVar[ReflexCallable[P, R]], *args: Var | Any - ) -> FunctionVar[ReflexCallable[P, R]]: ... - - @overload - def partial(self, *args: Var | Any) -> FunctionVar: ... - - def partial(self, *args: Var | Any) -> FunctionVar: # pyright: ignore [reportInconsistentOverload] - """Partially apply the function with the given arguments. - - Args: - *args: The arguments to partially apply the function with. - - Returns: - The partially applied function. - """ - if not args: - return self - return ArgsFunctionOperation.create( - ("...args",), - VarOperationCall.create(self, *args, Var(_js_expr="...args")), - ) - - @overload - def call( - self: FunctionVar[ReflexCallable[[V1], R]], arg1: V1 | Var[V1] - ) -> VarOperationCall[[V1], R]: ... - - @overload - def call( - self: FunctionVar[ReflexCallable[[V1, V2], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - ) -> VarOperationCall[[V1, V2], R]: ... - - @overload - def call( - self: FunctionVar[ReflexCallable[[V1, V2, V3], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - arg3: V3 | Var[V3], - ) -> VarOperationCall[[V1, V2, V3], R]: ... - - @overload - def call( - self: FunctionVar[ReflexCallable[[V1, V2, V3, V4], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - arg3: V3 | Var[V3], - arg4: V4 | Var[V4], - ) -> VarOperationCall[[V1, V2, V3, V4], R]: ... - - @overload - def call( - self: FunctionVar[ReflexCallable[[V1, V2, V3, V4, V5], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - arg3: V3 | Var[V3], - arg4: V4 | Var[V4], - arg5: V5 | Var[V5], - ) -> VarOperationCall[[V1, V2, V3, V4, V5], R]: ... - - @overload - def call( - self: FunctionVar[ReflexCallable[[V1, V2, V3, V4, V5, V6], R]], - arg1: V1 | Var[V1], - arg2: V2 | Var[V2], - arg3: V3 | Var[V3], - arg4: V4 | Var[V4], - arg5: V5 | Var[V5], - arg6: V6 | Var[V6], - ) -> VarOperationCall[[V1, V2, V3, V4, V5, V6], R]: ... - - @overload - def call( - self: FunctionVar[ReflexCallable[P, R]], *args: Var | Any - ) -> VarOperationCall[P, R]: ... - - @overload - def call(self, *args: Var | Any) -> Var: ... - - def call(self, *args: Var | Any) -> Var: # pyright: ignore [reportInconsistentOverload] - """Call the function with the given arguments. - - Args: - *args: The arguments to call the function with. - - Returns: - The function call operation. - """ - return VarOperationCall.create(self, *args).guess_type() - - __call__ = call - - -class BuilderFunctionVar( - FunctionVar[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any] -): - """Base class for immutable function vars with the builder pattern.""" - - __call__ = FunctionVar.partial - - -class FunctionStringVar(FunctionVar[CALLABLE_TYPE]): - """Base class for immutable function vars from a string.""" - - @classmethod - def create( - cls, - func: str, - _var_type: type[OTHER_CALLABLE_TYPE] = ReflexCallable[Any, Any], - _var_data: VarData | None = None, - ) -> FunctionStringVar[OTHER_CALLABLE_TYPE]: - """Create a new function var from a string. - - Args: - func: The function to call. - _var_type: The type of the Var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The function var. - """ - return FunctionStringVar( - _js_expr=func, - _var_type=_var_type, - _var_data=_var_data, - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class VarOperationCall(Generic[P, R], CachedVarOperation, Var[R]): - """Base class for immutable vars that are the result of a function call.""" - - _func: FunctionVar[ReflexCallable[P, R]] | None = dataclasses.field(default=None) - _args: tuple[Var | Any, ...] = dataclasses.field(default_factory=tuple) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - func_expr = str(self._func) - if _starts_with_arrow_function(func_expr) and not format.is_wrapped( - func_expr, "(" - ): - func_expr = format.wrap(func_expr, "(") - - return f"({func_expr}({', '.join([str(LiteralVar.create(arg)) for arg in self._args])}))" - - @cached_property_no_lock - def _cached_get_all_var_data(self) -> VarData | None: - """Get all the var data associated with the var. - - Returns: - All the var data associated with the var. - """ - return VarData.merge( - self._func._get_all_var_data() if self._func is not None else None, - *[LiteralVar.create(arg)._get_all_var_data() for arg in self._args], - self._var_data, - ) - - @classmethod - def create( - cls, - func: FunctionVar[ReflexCallable[P, R]], - *args: Var | Any, - _var_type: GenericType = Any, - _var_data: VarData | None = None, - ) -> VarOperationCall: - """Create a new function call var. - - Args: - func: The function to call. - *args: The arguments to call the function with. - _var_type: The type of the Var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The function call var. - """ - function_return_type = ( - func._var_type.__args__[1] - if getattr(func._var_type, "__args__", None) - else Any - ) - var_type = _var_type if _var_type is not Any else function_return_type - return cls( - _js_expr="", - _var_type=var_type, - _var_data=_var_data, - _func=func, - _args=args, - ) - - -@dataclasses.dataclass(frozen=True) -class DestructuredArg: - """Class for destructured arguments.""" - - fields: tuple[str, ...] = () - rest: str | None = None - - def to_javascript(self) -> str: - """Convert the destructured argument to JavaScript. - - Returns: - The destructured argument in JavaScript. - """ - return format.wrap( - ", ".join(self.fields) + (f", ...{self.rest}" if self.rest else ""), - "{", - "}", - ) - - -@dataclasses.dataclass( - frozen=True, -) -class FunctionArgs: - """Class for function arguments.""" - - args: tuple[str | DestructuredArg, ...] = () - rest: str | None = None - - -def format_args_function_operation( - args: FunctionArgs, return_expr: Var | Any, explicit_return: bool -) -> str: - """Format an args function operation. - - Args: - args: The function arguments. - return_expr: The return expression. - explicit_return: Whether to use explicit return syntax. - - Returns: - The formatted args function operation. - """ - arg_names_str = ", ".join([ - arg if isinstance(arg, str) else arg.to_javascript() for arg in args.args - ]) + (f", ...{args.rest}" if args.rest else "") - - return_expr_str = str(LiteralVar.create(return_expr)) - - # Wrap return expression in curly braces if explicit return syntax is used. - return_expr_str_wrapped = ( - format.wrap(return_expr_str, "{", "}") if explicit_return else return_expr_str - ) - - return f"(({arg_names_str}) => {return_expr_str_wrapped})" - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class ArgsFunctionOperation(CachedVarOperation, FunctionVar): - """Base class for immutable function defined via arguments and return expression.""" - - _args: FunctionArgs = dataclasses.field(default_factory=FunctionArgs) - _return_expr: Var | Any = dataclasses.field(default=None) - _explicit_return: bool = dataclasses.field(default=False) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return format_args_function_operation( - self._args, self._return_expr, self._explicit_return - ) - - @classmethod - def create( - cls, - args_names: Sequence[str | DestructuredArg], - return_expr: Var | Any, - rest: str | None = None, - explicit_return: bool = False, - _var_type: GenericType = Callable, - _var_data: VarData | None = None, - ): - """Create a new function var. - - Args: - args_names: The names of the arguments. - return_expr: The return expression of the function. - rest: The name of the rest argument. - explicit_return: Whether to use explicit return syntax. - _var_type: The type of the Var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The function var. - """ - return_expr = Var.create(return_expr) - return cls( - _js_expr="", - _var_type=_var_type, - _var_data=_var_data, - _args=FunctionArgs(args=tuple(args_names), rest=rest), - _return_expr=return_expr, - _explicit_return=explicit_return, - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class ArgsFunctionOperationBuilder(CachedVarOperation, BuilderFunctionVar): - """Base class for immutable function defined via arguments and return expression with the builder pattern.""" - - _args: FunctionArgs = dataclasses.field(default_factory=FunctionArgs) - _return_expr: Var | Any = dataclasses.field(default=None) - _explicit_return: bool = dataclasses.field(default=False) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return format_args_function_operation( - self._args, self._return_expr, self._explicit_return - ) - - @classmethod - def create( - cls, - args_names: Sequence[str | DestructuredArg], - return_expr: Var | Any, - rest: str | None = None, - explicit_return: bool = False, - _var_type: GenericType = Callable, - _var_data: VarData | None = None, - ): - """Create a new function var. - - Args: - args_names: The names of the arguments. - return_expr: The return expression of the function. - rest: The name of the rest argument. - explicit_return: Whether to use explicit return syntax. - _var_type: The type of the Var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The function var. - """ - return_expr = Var.create(return_expr) - return cls( - _js_expr="", - _var_type=_var_type, - _var_data=_var_data, - _args=FunctionArgs(args=tuple(args_names), rest=rest), - _return_expr=return_expr, - _explicit_return=explicit_return, - ) - - -JSON_STRINGIFY = FunctionStringVar.create( - "JSON.stringify", _var_type=ReflexCallable[[Any], str] -) -ARRAY_ISARRAY = FunctionStringVar.create( - "Array.isArray", _var_type=ReflexCallable[[Any], bool] -) -PROTOTYPE_TO_STRING = FunctionStringVar.create( - "((__to_string) => __to_string.toString())", - _var_type=ReflexCallable[[Any], str], -) +from reflex_core.vars.function import * diff --git a/reflex/vars/number.py b/reflex/vars/number.py index 4f639c799d0..72565a5c469 100644 --- a/reflex/vars/number.py +++ b/reflex/vars/number.py @@ -1,1148 +1,3 @@ -"""Immutable number vars.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import dataclasses -import decimal -import json -import math -from collections.abc import Callable -from typing import TYPE_CHECKING, Any, NoReturn, TypeVar, overload - -from typing_extensions import TypeVar as TypeVarExt - -from reflex.constants.base import Dirs -from reflex.utils.exceptions import ( - PrimitiveUnserializableToJSONError, - VarTypeError, - VarValueError, -) -from reflex.utils.imports import ImportDict, ImportVar -from reflex.utils.types import safe_issubclass - -from .base import ( - CustomVarOperationReturn, - LiteralVar, - Var, - VarData, - unionize, - var_operation, - var_operation_return, -) - -NUMBER_T = TypeVarExt( - "NUMBER_T", - bound=(int | float | decimal.Decimal), - default=(int | float | decimal.Decimal), - covariant=True, -) - -if TYPE_CHECKING: - from .sequence import ArrayVar - - -def raise_unsupported_operand_types( - operator: str, operands_types: tuple[type, ...] -) -> NoReturn: - """Raise an unsupported operand types error. - - Args: - operator: The operator. - operands_types: The types of the operands. - - Raises: - VarTypeError: The operand types are unsupported. - """ - msg = f"Unsupported Operand type(s) for {operator}: {', '.join(t.__name__ for t in operands_types)}" - raise VarTypeError(msg) - - -class NumberVar(Var[NUMBER_T], python_types=(int, float, decimal.Decimal)): - """Base class for immutable number vars.""" - - def __add__(self, other: number_types) -> NumberVar: - """Add two numbers. - - Args: - other: The other number. - - Returns: - The number addition operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("+", (type(self), type(other))) - return number_add_operation(self, +other) - - def __radd__(self, other: number_types) -> NumberVar: - """Add two numbers. - - Args: - other: The other number. - - Returns: - The number addition operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("+", (type(other), type(self))) - return number_add_operation(+other, self) - - def __sub__(self, other: number_types) -> NumberVar: - """Subtract two numbers. - - Args: - other: The other number. - - Returns: - The number subtraction operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("-", (type(self), type(other))) - - return number_subtract_operation(self, +other) - - def __rsub__(self, other: number_types) -> NumberVar: - """Subtract two numbers. - - Args: - other: The other number. - - Returns: - The number subtraction operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("-", (type(other), type(self))) - - return number_subtract_operation(+other, self) - - def __abs__(self): - """Get the absolute value of the number. - - Returns: - The number absolute operation. - """ - return number_abs_operation(self) - - @overload - def __mul__(self, other: number_types | boolean_types) -> NumberVar: ... - - @overload - def __mul__(self, other: list | tuple | set | ArrayVar) -> ArrayVar: ... - - def __mul__(self, other: Any): - """Multiply two numbers. - - Args: - other: The other number. - - Returns: - The number multiplication operation. - """ - from .sequence import ArrayVar, LiteralArrayVar - - if isinstance(other, (list, tuple, ArrayVar)): - if isinstance(other, ArrayVar): - return other * self - return LiteralArrayVar.create(other) * self - - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("*", (type(self), type(other))) - - return number_multiply_operation(self, +other) - - @overload - def __rmul__(self, other: number_types | boolean_types) -> NumberVar: ... - - @overload - def __rmul__(self, other: list | tuple | set | ArrayVar) -> ArrayVar: ... - - def __rmul__(self, other: Any): - """Multiply two numbers. - - Args: - other: The other number. - - Returns: - The number multiplication operation. - """ - from .sequence import ArrayVar, LiteralArrayVar - - if isinstance(other, (list, tuple, ArrayVar)): - if isinstance(other, ArrayVar): - return other * self - return LiteralArrayVar.create(other) * self - - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("*", (type(other), type(self))) - - return number_multiply_operation(+other, self) - - def __truediv__(self, other: number_types) -> NumberVar: - """Divide two numbers. - - Args: - other: The other number. - - Returns: - The number true division operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("/", (type(self), type(other))) - - return number_true_division_operation(self, +other) - - def __rtruediv__(self, other: number_types) -> NumberVar: - """Divide two numbers. - - Args: - other: The other number. - - Returns: - The number true division operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("/", (type(other), type(self))) - - return number_true_division_operation(+other, self) - - def __floordiv__(self, other: number_types) -> NumberVar: - """Floor divide two numbers. - - Args: - other: The other number. - - Returns: - The number floor division operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("//", (type(self), type(other))) - - return number_floor_division_operation(self, +other) - - def __rfloordiv__(self, other: number_types) -> NumberVar: - """Floor divide two numbers. - - Args: - other: The other number. - - Returns: - The number floor division operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("//", (type(other), type(self))) - - return number_floor_division_operation(+other, self) - - def __mod__(self, other: number_types) -> NumberVar: - """Modulo two numbers. - - Args: - other: The other number. - - Returns: - The number modulo operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("%", (type(self), type(other))) - - return number_modulo_operation(self, +other) - - def __rmod__(self, other: number_types) -> NumberVar: - """Modulo two numbers. - - Args: - other: The other number. - - Returns: - The number modulo operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("%", (type(other), type(self))) - - return number_modulo_operation(+other, self) - - def __pow__(self, other: number_types) -> NumberVar: - """Exponentiate two numbers. - - Args: - other: The other number. - - Returns: - The number exponent operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("**", (type(self), type(other))) - - return number_exponent_operation(self, +other) - - def __rpow__(self, other: number_types) -> NumberVar: - """Exponentiate two numbers. - - Args: - other: The other number. - - Returns: - The number exponent operation. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("**", (type(other), type(self))) - - return number_exponent_operation(+other, self) - - def __neg__(self) -> NumberVar: - """Negate the number. - - Returns: - The number negation operation. - """ - return number_negate_operation(self) # pyright: ignore [reportReturnType] - - def __invert__(self): - """Boolean NOT the number. - - Returns: - The boolean NOT operation. - """ - return boolean_not_operation(self.bool()) - - def __pos__(self) -> NumberVar: - """Positive the number. - - Returns: - The number. - """ - return self - - def __round__(self, ndigits: int | NumberVar = 0) -> NumberVar: - """Round the number. - - Args: - ndigits: The number of digits to round. - - Returns: - The number round operation. - """ - if not isinstance(ndigits, NUMBER_TYPES): - raise_unsupported_operand_types("round", (type(self), type(ndigits))) - - return number_round_operation(self, +ndigits) - - def __ceil__(self): - """Ceil the number. - - Returns: - The number ceil operation. - """ - return number_ceil_operation(self) - - def __floor__(self): - """Floor the number. - - Returns: - The number floor operation. - """ - return number_floor_operation(self) - - def __trunc__(self): - """Trunc the number. - - Returns: - The number trunc operation. - """ - return number_trunc_operation(self) - - def __lt__(self, other: number_types) -> BooleanVar: - """Less than comparison. - - Args: - other: The other number. - - Returns: - The result of the comparison. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("<", (type(self), type(other))) - return less_than_operation(+self, +other) - - def __le__(self, other: number_types) -> BooleanVar: - """Less than or equal comparison. - - Args: - other: The other number. - - Returns: - The result of the comparison. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types("<=", (type(self), type(other))) - return less_than_or_equal_operation(+self, +other) - - def __eq__(self, other: Any): - """Equal comparison. - - Args: - other: The other number. - - Returns: - The result of the comparison. - """ - if isinstance(other, NUMBER_TYPES): - return equal_operation(+self, +other) - return equal_operation(self, other) - - def __ne__(self, other: Any): - """Not equal comparison. - - Args: - other: The other number. - - Returns: - The result of the comparison. - """ - if isinstance(other, NUMBER_TYPES): - return not_equal_operation(+self, +other) - return not_equal_operation(self, other) - - def __gt__(self, other: number_types) -> BooleanVar: - """Greater than comparison. - - Args: - other: The other number. - - Returns: - The result of the comparison. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types(">", (type(self), type(other))) - return greater_than_operation(+self, +other) - - def __ge__(self, other: number_types) -> BooleanVar: - """Greater than or equal comparison. - - Args: - other: The other number. - - Returns: - The result of the comparison. - """ - if not isinstance(other, NUMBER_TYPES): - raise_unsupported_operand_types(">=", (type(self), type(other))) - return greater_than_or_equal_operation(+self, +other) - - def _is_strict_float(self) -> bool: - """Check if the number is a float. - - Returns: - bool: True if the number is a float. - """ - return safe_issubclass(self._var_type, float) - - def _is_strict_int(self) -> bool: - """Check if the number is an int. - - Returns: - bool: True if the number is an int. - """ - return safe_issubclass(self._var_type, int) - - def __format__(self, format_spec: str) -> str: - """Format the number. - - Args: - format_spec: The format specifier. - - Returns: - The formatted number. - - Raises: - VarValueError: If the format specifier is not supported. - """ - from .sequence import ( - get_decimal_string_operation, - get_decimal_string_separator_operation, - ) - - separator = "" - - if format_spec and format_spec[:1] == ",": - separator = "," - format_spec = format_spec[1:] - elif format_spec and format_spec[:1] == "_": - separator = "_" - format_spec = format_spec[1:] - - if ( - format_spec - and format_spec[-1] == "f" - and format_spec[0] == "." - and format_spec[1:-1].isdigit() - ): - how_many_decimals = int(format_spec[1:-1]) - return f"{get_decimal_string_operation(self, Var.create(how_many_decimals), Var.create(separator))}" - - if not format_spec and separator: - return ( - f"{get_decimal_string_separator_operation(self, Var.create(separator))}" - ) - - if format_spec: - msg = ( - f"Unknown format code '{format_spec}' for object of type 'NumberVar'. It is only supported to use ',', '_', and '.f' for float numbers." - "If possible, use computed variables instead: https://reflex.dev/docs/vars/computed-vars/" - ) - raise VarValueError(msg) - - return super().__format__(format_spec) - - -def binary_number_operation( - func: Callable[[NumberVar, NumberVar], str], -) -> Callable[[number_types, number_types], NumberVar]: - """Decorator to create a binary number operation. - - Args: - func: The binary number operation function. - - Returns: - The binary number operation. - """ - - @var_operation - def operation(lhs: NumberVar, rhs: NumberVar): - return var_operation_return( - js_expression=func(lhs, rhs), - var_type=unionize(lhs._var_type, rhs._var_type), - ) - - def wrapper(lhs: number_types, rhs: number_types) -> NumberVar: - """Create the binary number operation. - - Args: - lhs: The first number. - rhs: The second number. - - Returns: - The binary number operation. - """ - return operation(lhs, rhs) # pyright: ignore [reportReturnType, reportArgumentType] - - return wrapper - - -@binary_number_operation -def number_add_operation(lhs: NumberVar, rhs: NumberVar): - """Add two numbers. - - Args: - lhs: The first number. - rhs: The second number. - - Returns: - The number addition operation. - """ - return f"({lhs} + {rhs})" - - -@binary_number_operation -def number_subtract_operation(lhs: NumberVar, rhs: NumberVar): - """Subtract two numbers. - - Args: - lhs: The first number. - rhs: The second number. - - Returns: - The number subtraction operation. - """ - return f"({lhs} - {rhs})" - - -@var_operation -def number_abs_operation(value: NumberVar): - """Get the absolute value of the number. - - Args: - value: The number. - - Returns: - The number absolute operation. - """ - return var_operation_return( - js_expression=f"Math.abs({value})", var_type=value._var_type - ) - - -@binary_number_operation -def number_multiply_operation(lhs: NumberVar, rhs: NumberVar): - """Multiply two numbers. - - Args: - lhs: The first number. - rhs: The second number. - - Returns: - The number multiplication operation. - """ - return f"({lhs} * {rhs})" - - -@var_operation -def number_negate_operation( - value: NumberVar[NUMBER_T], -) -> CustomVarOperationReturn[NUMBER_T]: - """Negate the number. - - Args: - value: The number. - - Returns: - The number negation operation. - """ - return var_operation_return(js_expression=f"-({value})", var_type=value._var_type) - - -@binary_number_operation -def number_true_division_operation(lhs: NumberVar, rhs: NumberVar): - """Divide two numbers. - - Args: - lhs: The first number. - rhs: The second number. - - Returns: - The number true division operation. - """ - return f"({lhs} / {rhs})" - - -@binary_number_operation -def number_floor_division_operation(lhs: NumberVar, rhs: NumberVar): - """Floor divide two numbers. - - Args: - lhs: The first number. - rhs: The second number. - - Returns: - The number floor division operation. - """ - return f"Math.floor({lhs} / {rhs})" - - -@binary_number_operation -def number_modulo_operation(lhs: NumberVar, rhs: NumberVar): - """Modulo two numbers. - - Args: - lhs: The first number. - rhs: The second number. - - Returns: - The number modulo operation. - """ - return f"({lhs} % {rhs})" - - -@binary_number_operation -def number_exponent_operation(lhs: NumberVar, rhs: NumberVar): - """Exponentiate two numbers. - - Args: - lhs: The first number. - rhs: The second number. - - Returns: - The number exponent operation. - """ - return f"({lhs} ** {rhs})" - - -@var_operation -def number_round_operation(value: NumberVar, ndigits: NumberVar | int): - """Round the number. - - Args: - value: The number. - ndigits: The number of digits. - - Returns: - The number round operation. - """ - if (isinstance(ndigits, LiteralNumberVar) and ndigits._var_value == 0) or ( - isinstance(ndigits, int) and ndigits == 0 - ): - return var_operation_return(js_expression=f"Math.round({value})", var_type=int) - return var_operation_return( - js_expression=f"(+{value}.toFixed({ndigits}))", var_type=float - ) - - -@var_operation -def number_ceil_operation(value: NumberVar): - """Ceil the number. - - Args: - value: The number. - - Returns: - The number ceil operation. - """ - return var_operation_return(js_expression=f"Math.ceil({value})", var_type=int) - - -@var_operation -def number_floor_operation(value: NumberVar): - """Floor the number. - - Args: - value: The number. - - Returns: - The number floor operation. - """ - return var_operation_return(js_expression=f"Math.floor({value})", var_type=int) - - -@var_operation -def number_trunc_operation(value: NumberVar): - """Trunc the number. - - Args: - value: The number. - - Returns: - The number trunc operation. - """ - return var_operation_return(js_expression=f"Math.trunc({value})", var_type=int) - - -class BooleanVar(NumberVar[bool], python_types=bool): - """Base class for immutable boolean vars.""" - - def __invert__(self): - """NOT the boolean. - - Returns: - The boolean NOT operation. - """ - return boolean_not_operation(self) - - def __int__(self): - """Convert the boolean to an int. - - Returns: - The boolean to int operation. - """ - return boolean_to_number_operation(self) - - def __pos__(self): - """Convert the boolean to an int. - - Returns: - The boolean to int operation. - """ - return boolean_to_number_operation(self) - - def bool(self) -> BooleanVar: - """Boolean conversion. - - Returns: - The boolean value of the boolean. - """ - return self - - def __lt__(self, other: Any): - """Less than comparison. - - Args: - other: The other boolean. - - Returns: - The result of the comparison. - """ - return +self < other - - def __le__(self, other: Any): - """Less than or equal comparison. - - Args: - other: The other boolean. - - Returns: - The result of the comparison. - """ - return +self <= other - - def __gt__(self, other: Any): - """Greater than comparison. - - Args: - other: The other boolean. - - Returns: - The result of the comparison. - """ - return +self > other - - def __ge__(self, other: Any): - """Greater than or equal comparison. - - Args: - other: The other boolean. - - Returns: - The result of the comparison. - """ - return +self >= other - - -@var_operation -def boolean_to_number_operation(value: BooleanVar): - """Convert the boolean to a number. - - Args: - value: The boolean. - - Returns: - The boolean to number operation. - """ - return var_operation_return(js_expression=f"Number({value})", var_type=int) - - -def comparison_operator( - func: Callable[[Var, Var], str], -) -> Callable[[Var | Any, Var | Any], BooleanVar]: - """Decorator to create a comparison operation. - - Args: - func: The comparison operation function. - - Returns: - The comparison operation. - """ - - @var_operation - def operation(lhs: Var, rhs: Var): - return var_operation_return( - js_expression=func(lhs, rhs), - var_type=bool, - ) - - def wrapper(lhs: Var | Any, rhs: Var | Any) -> BooleanVar: - """Create the comparison operation. - - Args: - lhs: The first value. - rhs: The second value. - - Returns: - The comparison operation. - """ - return operation(lhs, rhs) - - return wrapper - - -@comparison_operator -def greater_than_operation(lhs: Var, rhs: Var): - """Greater than comparison. - - Args: - lhs: The first value. - rhs: The second value. - - Returns: - The result of the comparison. - """ - return f"({lhs} > {rhs})" - - -@comparison_operator -def greater_than_or_equal_operation(lhs: Var, rhs: Var): - """Greater than or equal comparison. - - Args: - lhs: The first value. - rhs: The second value. - - Returns: - The result of the comparison. - """ - return f"({lhs} >= {rhs})" - - -@comparison_operator -def less_than_operation(lhs: Var, rhs: Var): - """Less than comparison. - - Args: - lhs: The first value. - rhs: The second value. - - Returns: - The result of the comparison. - """ - return f"({lhs} < {rhs})" - - -@comparison_operator -def less_than_or_equal_operation(lhs: Var, rhs: Var): - """Less than or equal comparison. - - Args: - lhs: The first value. - rhs: The second value. - - Returns: - The result of the comparison. - """ - return f"({lhs} <= {rhs})" - - -@comparison_operator -def equal_operation(lhs: Var, rhs: Var): - """Equal comparison. - - Args: - lhs: The first value. - rhs: The second value. - - Returns: - The result of the comparison. - """ - return f"({lhs}?.valueOf?.() === {rhs}?.valueOf?.())" - - -@comparison_operator -def not_equal_operation(lhs: Var, rhs: Var): - """Not equal comparison. - - Args: - lhs: The first value. - rhs: The second value. - - Returns: - The result of the comparison. - """ - return f"({lhs}?.valueOf?.() !== {rhs}?.valueOf?.())" - - -@var_operation -def boolean_not_operation(value: BooleanVar): - """Boolean NOT the boolean. - - Args: - value: The boolean. - - Returns: - The boolean NOT operation. - """ - return var_operation_return(js_expression=f"!({value})", var_type=bool) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralNumberVar(LiteralVar, NumberVar[NUMBER_T]): - """Base class for immutable literal number vars.""" - - _var_value: float | int | decimal.Decimal = dataclasses.field(default=0) - - def json(self) -> str: - """Get the JSON representation of the var. - - Returns: - The JSON representation of the var. - - Raises: - PrimitiveUnserializableToJSONError: If the var is unserializable to JSON. - """ - if isinstance(self._var_value, decimal.Decimal): - return json.dumps(float(self._var_value)) - if math.isinf(self._var_value) or math.isnan(self._var_value): - msg = f"No valid JSON representation for {self}" - raise PrimitiveUnserializableToJSONError(msg) - return json.dumps(self._var_value) - - def __hash__(self) -> int: - """Calculate the hash value of the object. - - Returns: - int: The hash value of the object. - """ - return hash((type(self).__name__, self._var_value)) - - @classmethod - def _get_all_var_data_without_creating_var( - cls, value: float | int | decimal.Decimal - ) -> VarData | None: - """Get all the var data without creating the var. - - Args: - value: The value of the var. - - Returns: - The var data. - """ - return None - - @classmethod - def create( - cls, value: float | int | decimal.Decimal, _var_data: VarData | None = None - ): - """Create the number var. - - Args: - value: The value of the var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The number var. - """ - if math.isinf(value): - js_expr = "Infinity" if value > 0 else "-Infinity" - elif math.isnan(value): - js_expr = "NaN" - else: - js_expr = str(value) - - return cls( - _js_expr=js_expr, - _var_type=type(value), - _var_data=_var_data, - _var_value=value, - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralBooleanVar(LiteralVar, BooleanVar): - """Base class for immutable literal boolean vars.""" - - _var_value: bool = dataclasses.field(default=False) - - def json(self) -> str: - """Get the JSON representation of the var. - - Returns: - The JSON representation of the var. - """ - return "true" if self._var_value else "false" - - def __hash__(self) -> int: - """Calculate the hash value of the object. - - Returns: - int: The hash value of the object. - """ - return hash((type(self).__name__, self._var_value)) - - @classmethod - def _get_all_var_data_without_creating_var(cls, value: bool) -> VarData | None: - """Get all the var data without creating the var. - - Args: - value: The value of the var. - - Returns: - The var data. - """ - return None - - @classmethod - def create(cls, value: bool, _var_data: VarData | None = None): - """Create the boolean var. - - Args: - value: The value of the var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The boolean var. - """ - return cls( - _js_expr="true" if value else "false", - _var_type=bool, - _var_data=_var_data, - _var_value=value, - ) - - -number_types = NumberVar | int | float | decimal.Decimal -boolean_types = BooleanVar | bool - - -_IS_TRUE_IMPORT: ImportDict = { - f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")], -} - -_IS_NOT_NULL_OR_UNDEFINED_IMPORT: ImportDict = { - f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isNotNullOrUndefined")], -} - - -@var_operation -def boolify(value: Var): - """Convert the value to a boolean. - - Args: - value: The value. - - Returns: - The boolean value. - """ - return var_operation_return( - js_expression=f"isTrue({value})", - var_type=bool, - var_data=VarData(imports=_IS_TRUE_IMPORT), - ) - - -@var_operation -def is_not_none_operation(value: Var): - """Check if the value is not None. - - Args: - value: The value. - - Returns: - The boolean value. - """ - return var_operation_return( - js_expression=f"isNotNullOrUndefined({value})", - var_type=bool, - var_data=VarData(imports=_IS_NOT_NULL_OR_UNDEFINED_IMPORT), - ) - - -T = TypeVar("T") -U = TypeVar("U") - - -@var_operation -def ternary_operation( - condition: Var[bool], if_true: Var[T], if_false: Var[U] -) -> CustomVarOperationReturn[T | U]: - """Create a ternary operation. - - Args: - condition: The condition. - if_true: The value if the condition is true. - if_false: The value if the condition is false. - - Returns: - The ternary operation. - """ - type_value: type[T] | type[U] = unionize(if_true._var_type, if_false._var_type) - value: CustomVarOperationReturn[T | U] = var_operation_return( - js_expression=f"({condition} ? {if_true} : {if_false})", - var_type=type_value, - ) - return value - - -NUMBER_TYPES = (int, float, decimal.Decimal, NumberVar) +from reflex_core.vars.number import * diff --git a/reflex/vars/object.py b/reflex/vars/object.py index 0bcc195a0fc..bfadb1e4ee0 100644 --- a/reflex/vars/object.py +++ b/reflex/vars/object.py @@ -1,648 +1,3 @@ -"""Classes for immutable object vars.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import collections.abc -import dataclasses -import typing -from collections.abc import Mapping -from importlib.util import find_spec -from typing import ( - Any, - NoReturn, - TypeVar, - get_args, - get_type_hints, - is_typeddict, - overload, -) - -from rich.markup import escape - -from reflex.utils import types -from reflex.utils.exceptions import VarAttributeError -from reflex.utils.types import ( - GenericType, - get_attribute_access_type, - get_origin, - safe_issubclass, - unionize, -) - -from .base import ( - CachedVarOperation, - LiteralVar, - Var, - VarData, - cached_property_no_lock, - figure_out_type, - var_operation, - var_operation_return, -) -from .number import BooleanVar, NumberVar, raise_unsupported_operand_types -from .sequence import ArrayVar, LiteralArrayVar, StringVar - -OBJECT_TYPE = TypeVar("OBJECT_TYPE", covariant=True) - -KEY_TYPE = TypeVar("KEY_TYPE") -VALUE_TYPE = TypeVar("VALUE_TYPE") - -ARRAY_INNER_TYPE = TypeVar("ARRAY_INNER_TYPE") - -OTHER_KEY_TYPE = TypeVar("OTHER_KEY_TYPE") - - -def _determine_value_type(var_type: GenericType): - origin_var_type = get_origin(var_type) or var_type - - if origin_var_type in types.UnionTypes: - return unionize(*[ - _determine_value_type(arg) - for arg in get_args(var_type) - if arg is not type(None) - ]) - - if is_typeddict(origin_var_type) or dataclasses.is_dataclass(origin_var_type): - annotations = get_type_hints(origin_var_type) - return unionize(*annotations.values()) - - if origin_var_type in [dict, Mapping, collections.abc.Mapping]: - args = get_args(var_type) - return args[1] if args else Any - - return Any - - -PYTHON_TYPES = (Mapping,) -if find_spec("pydantic"): - import pydantic - import pydantic.v1 - - PYTHON_TYPES += (pydantic.BaseModel, pydantic.v1.BaseModel) - - -class ObjectVar(Var[OBJECT_TYPE], python_types=PYTHON_TYPES): - """Base class for immutable object vars.""" - - def _key_type(self) -> type: - """Get the type of the keys of the object. - - Returns: - The type of the keys of the object. - """ - return str - - @overload - def _value_type( - self: ObjectVar[Mapping[Any, VALUE_TYPE]], - ) -> type[VALUE_TYPE]: ... - - @overload - def _value_type(self) -> GenericType: ... - - def _value_type(self) -> GenericType: - """Get the type of the values of the object. - - Returns: - The type of the values of the object. - """ - return _determine_value_type(self._var_type) - - def keys(self) -> ArrayVar[list[str]]: - """Get the keys of the object. - - Returns: - The keys of the object. - """ - return object_keys_operation(self) - - @overload - def values( - self: ObjectVar[Mapping[Any, VALUE_TYPE]], - ) -> ArrayVar[list[VALUE_TYPE]]: ... - - @overload - def values(self) -> ArrayVar: ... - - def values(self) -> ArrayVar: - """Get the values of the object. - - Returns: - The values of the object. - """ - return object_values_operation(self) - - @overload - def entries( - self: ObjectVar[Mapping[Any, VALUE_TYPE]], - ) -> ArrayVar[list[tuple[str, VALUE_TYPE]]]: ... - - @overload - def entries(self) -> ArrayVar: ... - - def entries(self) -> ArrayVar: - """Get the entries of the object. - - Returns: - The entries of the object. - """ - return object_entries_operation(self) - - items = entries - - def length(self) -> NumberVar[int]: - """Get the length of the object. - - Returns: - The length of the object. - """ - return self.keys().length() - - def merge(self, other: ObjectVar): - """Merge two objects. - - Args: - other: The other object to merge. - - Returns: - The merged object. - """ - return object_merge_operation(self, other) - - # NoReturn is used here to catch when key value is Any - @overload - def __getitem__( # pyright: ignore [reportOverlappingOverload] - self: ObjectVar[Mapping[Any, NoReturn]], - key: Var | Any, - ) -> Var: ... - - @overload - def __getitem__( - self: (ObjectVar[Mapping[Any, bool]]), - key: Var | Any, - ) -> BooleanVar: ... - - @overload - def __getitem__( - self: ( - ObjectVar[Mapping[Any, int]] - | ObjectVar[Mapping[Any, float]] - | ObjectVar[Mapping[Any, int | float]] - ), - key: Var | Any, - ) -> NumberVar: ... - - @overload - def __getitem__( - self: ObjectVar[Mapping[Any, str]], - key: Var | Any, - ) -> StringVar: ... - - @overload - def __getitem__( - self: ObjectVar[Mapping[Any, list[ARRAY_INNER_TYPE]]], - key: Var | Any, - ) -> ArrayVar[list[ARRAY_INNER_TYPE]]: ... - - @overload - def __getitem__( - self: ObjectVar[Mapping[Any, tuple[ARRAY_INNER_TYPE, ...]]], - key: Var | Any, - ) -> ArrayVar[tuple[ARRAY_INNER_TYPE, ...]]: ... - - @overload - def __getitem__( - self: ObjectVar[Mapping[Any, Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]], - key: Var | Any, - ) -> ObjectVar[Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]: ... - - @overload - def __getitem__( - self: ObjectVar[Mapping[Any, VALUE_TYPE]], - key: Var | Any, - ) -> Var[VALUE_TYPE]: ... - - def __getitem__(self, key: Var | Any) -> Var: - """Get an item from the object. - - Args: - key: The key to get from the object. - - Returns: - The item from the object. - """ - from .sequence import LiteralStringVar - - if not isinstance(key, (StringVar, str, int, NumberVar)) or ( - isinstance(key, NumberVar) and key._is_strict_float() - ): - raise_unsupported_operand_types("[]", (type(self), type(key))) - if isinstance(key, str) and isinstance(Var.create(key), LiteralStringVar): - return self.__getattr__(key) - return ObjectItemOperation.create(self, key).guess_type() - - def get(self, key: Var | Any, default: Var | Any | None = None) -> Var: - """Get an item from the object. - - Args: - key: The key to get from the object. - default: The default value if the key is not found. - - Returns: - The item from the object. - """ - from reflex.components.core.cond import cond - - if default is None: - default = Var.create(None) - - value = self.__getitem__(key) # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue,reportUnknownMemberType] - - return cond( # pyright: ignore[reportUnknownVariableType] - value, - value, - default, - ) - - # NoReturn is used here to catch when key value is Any - @overload - def __getattr__( # pyright: ignore [reportOverlappingOverload] - self: ObjectVar[Mapping[Any, NoReturn]], - name: str, - ) -> Var: ... - - @overload - def __getattr__( - self: ( - ObjectVar[Mapping[Any, int]] - | ObjectVar[Mapping[Any, float]] - | ObjectVar[Mapping[Any, int | float]] - ), - name: str, - ) -> NumberVar: ... - - @overload - def __getattr__( - self: ObjectVar[Mapping[Any, str]], - name: str, - ) -> StringVar: ... - - @overload - def __getattr__( - self: ObjectVar[Mapping[Any, list[ARRAY_INNER_TYPE]]], - name: str, - ) -> ArrayVar[list[ARRAY_INNER_TYPE]]: ... - - @overload - def __getattr__( - self: ObjectVar[Mapping[Any, tuple[ARRAY_INNER_TYPE, ...]]], - name: str, - ) -> ArrayVar[tuple[ARRAY_INNER_TYPE, ...]]: ... - - @overload - def __getattr__( - self: ObjectVar[Mapping[Any, Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]], - name: str, - ) -> ObjectVar[Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]: ... - - @overload - def __getattr__( - self: ObjectVar, - name: str, - ) -> ObjectItemOperation: ... - - def __getattr__(self, name: str) -> Var: - """Get an attribute of the var. - - Args: - name: The name of the attribute. - - Returns: - The attribute of the var. - - Raises: - VarAttributeError: The State var has no such attribute or may have been annotated wrongly. - """ - if name.startswith("__") and name.endswith("__"): - return getattr(super(type(self), self), name) - - var_type = self._var_type - - var_type = types.value_inside_optional(var_type) - - fixed_type = get_origin(var_type) or var_type - - if ( - is_typeddict(fixed_type) - or ( - isinstance(fixed_type, type) - and not safe_issubclass(fixed_type, Mapping) - ) - or (fixed_type in types.UnionTypes) - ): - attribute_type = get_attribute_access_type(var_type, name) - if attribute_type is None: - msg = ( - f"The State var `{self!s}` of type {escape(str(self._var_type))} has no attribute '{name}' or may have been annotated " - f"wrongly." - ) - raise VarAttributeError(msg) - return ObjectItemOperation.create(self, name, attribute_type).guess_type() - return ObjectItemOperation.create(self, name).guess_type() - - def contains(self, key: Var | Any) -> BooleanVar: - """Check if the object contains a key. - - Args: - key: The key to check. - - Returns: - The result of the check. - """ - return object_has_own_property_operation(self, key) - - -class RestProp(ObjectVar[dict[str, Any]]): - """A special object var representing forwarded rest props.""" - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): - """Base class for immutable literal object vars.""" - - _var_value: Mapping[Var | Any, Var | Any] = dataclasses.field(default_factory=dict) - - def _key_type(self) -> type: - """Get the type of the keys of the object. - - Returns: - The type of the keys of the object. - """ - args_list = typing.get_args(self._var_type) - return args_list[0] if args_list else Any # pyright: ignore [reportReturnType] - - def _value_type(self) -> type: - """Get the type of the values of the object. - - Returns: - The type of the values of the object. - """ - args_list = typing.get_args(self._var_type) - return args_list[1] if args_list else Any # pyright: ignore [reportReturnType] - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return ( - "({ " - + ", ".join([ - f"[{LiteralVar.create(key)!s}] : {LiteralVar.create(value)!s}" - for key, value in self._var_value.items() - ]) - + " })" - ) - - def json(self) -> str: - """Get the JSON representation of the object. - - Returns: - The JSON representation of the object. - - Raises: - TypeError: The keys and values of the object must be literal vars to get the JSON representation - """ - keys_and_values = [] - for key, value in self._var_value.items(): - key = LiteralVar.create(key) - value = LiteralVar.create(value) - if not isinstance(key, LiteralVar) or not isinstance(value, LiteralVar): - msg = "The keys and values of the object must be literal vars to get the JSON representation." - raise TypeError(msg) - keys_and_values.append(f"{key.json()}:{value.json()}") - return "{" + ", ".join(keys_and_values) + "}" - - def __hash__(self) -> int: - """Get the hash of the var. - - Returns: - The hash of the var. - """ - return hash((type(self).__name__, self._js_expr)) - - @classmethod - def _get_all_var_data_without_creating_var( - cls, - value: Mapping, - ) -> VarData | None: - """Get all the var data without creating a var. - - Args: - value: The value to get the var data from. - - Returns: - The var data. - """ - return VarData.merge( - LiteralArrayVar._get_all_var_data_without_creating_var(value), - LiteralArrayVar._get_all_var_data_without_creating_var(value.values()), - ) - - @cached_property_no_lock - def _cached_get_all_var_data(self) -> VarData | None: - """Get all the var data. - - Returns: - The var data. - """ - return VarData.merge( - LiteralArrayVar._get_all_var_data_without_creating_var(self._var_value), - LiteralArrayVar._get_all_var_data_without_creating_var( - self._var_value.values() - ), - self._var_data, - ) - - @classmethod - def create( - cls, - _var_value: Mapping, - _var_type: type[OBJECT_TYPE] | None = None, - _var_data: VarData | None = None, - ) -> LiteralObjectVar[OBJECT_TYPE]: - """Create the literal object var. - - Args: - _var_value: The value of the var. - _var_type: The type of the var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The literal object var. - - Raises: - TypeError: If the value is not a mapping type or a dataclass. - """ - if not isinstance(_var_value, collections.abc.Mapping): - from reflex.utils.serializers import serialize - - serialized = serialize(_var_value, get_type=False) - if not isinstance(serialized, collections.abc.Mapping): - msg = f"Expected a mapping type or a dataclass, got {_var_value!r} of type {type(_var_value).__name__}." - raise TypeError(msg) - - return LiteralObjectVar( - _js_expr="", - _var_type=(type(_var_value) if _var_type is None else _var_type), - _var_data=_var_data, - _var_value=serialized, - ) - - return LiteralObjectVar( - _js_expr="", - _var_type=(figure_out_type(_var_value) if _var_type is None else _var_type), - _var_data=_var_data, - _var_value=_var_value, - ) - - -@var_operation -def object_keys_operation(value: ObjectVar): - """Get the keys of an object. - - Args: - value: The object to get the keys from. - - Returns: - The keys of the object. - """ - return var_operation_return( - js_expression=f"Object.keys({value} ?? {{}})", - var_type=list[str], - ) - - -@var_operation -def object_values_operation(value: ObjectVar): - """Get the values of an object. - - Args: - value: The object to get the values from. - - Returns: - The values of the object. - """ - return var_operation_return( - js_expression=f"Object.values({value} ?? {{}})", - var_type=list[value._value_type()], - ) - - -@var_operation -def object_entries_operation(value: ObjectVar): - """Get the entries of an object. - - Args: - value: The object to get the entries from. - - Returns: - The entries of the object. - """ - return var_operation_return( - js_expression=f"Object.entries({value} ?? {{}})", - var_type=list[tuple[str, value._value_type()]], - ) - - -@var_operation -def object_merge_operation(lhs: ObjectVar, rhs: ObjectVar): - """Merge two objects. - - Args: - lhs: The first object to merge. - rhs: The second object to merge. - - Returns: - The merged object. - """ - return var_operation_return( - js_expression=f"({{...{lhs}, ...{rhs}}})", - var_type=Mapping[ - lhs._key_type() | rhs._key_type(), - lhs._value_type() | rhs._value_type(), - ], - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class ObjectItemOperation(CachedVarOperation, Var): - """Operation to get an item from an object.""" - - _object: ObjectVar = dataclasses.field( - default_factory=lambda: LiteralObjectVar.create({}) - ) - _key: Var | Any = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the operation. - - Returns: - The name of the operation. - """ - return f"{self._object!s}?.[{self._key!s}]" - - @classmethod - def create( - cls, - object: ObjectVar, - key: Var | Any, - _var_type: GenericType | None = None, - _var_data: VarData | None = None, - ) -> ObjectItemOperation: - """Create the object item operation. - - Args: - object: The object to get the item from. - key: The key to get from the object. - _var_type: The type of the item. - _var_data: Additional hooks and imports associated with the operation. - - Returns: - The object item operation. - """ - return cls( - _js_expr="", - _var_type=object._value_type() if _var_type is None else _var_type, - _var_data=_var_data, - _object=object, - _key=key if isinstance(key, Var) else LiteralVar.create(key), - ) - - -@var_operation -def object_has_own_property_operation(object: ObjectVar, key: Var): - """Check if an object has a key. - - Args: - object: The object to check. - key: The key to check. - - Returns: - The result of the check. - """ - return var_operation_return( - js_expression=f"{object}.hasOwnProperty({key})", - var_type=bool, - ) +from reflex_core.vars.object import * diff --git a/reflex/vars/sequence.py b/reflex/vars/sequence.py index 3824ba8760b..0a26e20d514 100644 --- a/reflex/vars/sequence.py +++ b/reflex/vars/sequence.py @@ -1,1847 +1,3 @@ -"""Collection of string classes and utilities.""" +"""Re-export from reflex_core.""" -from __future__ import annotations - -import collections.abc -import dataclasses -import decimal -import inspect -import json -import re -from collections.abc import Iterable, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Literal, TypeVar, get_args, overload - -from typing_extensions import TypeVar as TypingExtensionsTypeVar - -from reflex import constants -from reflex.constants.base import REFLEX_VAR_OPENING_TAG -from reflex.utils import types -from reflex.utils.exceptions import VarTypeError -from reflex.utils.types import GenericType, get_origin - -from .base import ( - CachedVarOperation, - CustomVarOperationReturn, - LiteralVar, - Var, - VarData, - _global_vars, - cached_property_no_lock, - figure_out_type, - get_unique_variable_name, - unionize, - var_operation, - var_operation_return, -) -from .number import ( - BooleanVar, - LiteralNumberVar, - NumberVar, - raise_unsupported_operand_types, -) - -if TYPE_CHECKING: - from .base import BASE_TYPE, DATACLASS_TYPE, SQLA_TYPE - from .function import FunctionVar - from .object import ObjectVar - -ARRAY_VAR_TYPE = TypeVar("ARRAY_VAR_TYPE", bound=Sequence, covariant=True) -OTHER_ARRAY_VAR_TYPE = TypeVar("OTHER_ARRAY_VAR_TYPE", bound=Sequence, covariant=True) -MAPPING_VAR_TYPE = TypeVar("MAPPING_VAR_TYPE", bound=Mapping, covariant=True) - -OTHER_TUPLE = TypeVar("OTHER_TUPLE") - -INNER_ARRAY_VAR = TypeVar("INNER_ARRAY_VAR") - - -KEY_TYPE = TypeVar("KEY_TYPE") -VALUE_TYPE = TypeVar("VALUE_TYPE") - - -class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(Sequence, set)): - """Base class for immutable array vars.""" - - def join(self, sep: StringVar | str = "") -> StringVar: - """Join the elements of the array. - - Args: - sep: The separator between elements. - - Returns: - The joined elements. - """ - if not isinstance(sep, (StringVar, str)): - raise_unsupported_operand_types("join", (type(self), type(sep))) - if ( - isinstance(self, LiteralArrayVar) - and ( - len( - args := [ - x - for x in self._var_value - if isinstance(x, (LiteralStringVar, str)) - ] - ) - == len(self._var_value) - ) - and isinstance(sep, (LiteralStringVar, str)) - ): - sep_str = sep._var_value if isinstance(sep, LiteralStringVar) else sep - return LiteralStringVar.create( - sep_str.join( - i._var_value if isinstance(i, LiteralStringVar) else i for i in args - ) - ) - return array_join_operation(self, sep) - - def reverse(self) -> ArrayVar[ARRAY_VAR_TYPE]: - """Reverse the array. - - Returns: - The reversed array. - """ - return array_reverse_operation(self) - - def __add__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> ArrayVar[ARRAY_VAR_TYPE]: - """Concatenate two arrays. - - Parameters: - other: The other array to concatenate. - - Returns: - ArrayConcatOperation: The concatenation of the two arrays. - """ - if not isinstance(other, ArrayVar): - raise_unsupported_operand_types("+", (type(self), type(other))) - - return array_concat_operation(self, other) - - @overload - def __getitem__(self, i: slice) -> ArrayVar[ARRAY_VAR_TYPE]: ... - - @overload - def __getitem__( - self: ( - ArrayVar[tuple[int, OTHER_TUPLE]] - | ArrayVar[tuple[float, OTHER_TUPLE]] - | ArrayVar[tuple[int | float, OTHER_TUPLE]] - ), - i: Literal[0, -2], - ) -> NumberVar: ... - - @overload - def __getitem__( - self: ArrayVar[tuple[Any, bool]], i: Literal[1, -1] - ) -> BooleanVar: ... - - @overload - def __getitem__( - self: ( - ArrayVar[tuple[Any, int]] - | ArrayVar[tuple[Any, float]] - | ArrayVar[tuple[Any, int | float]] - ), - i: Literal[1, -1], - ) -> NumberVar: ... - - @overload - def __getitem__( # pyright: ignore [reportOverlappingOverload] - self: ArrayVar[tuple[str, Any]], i: Literal[0, -2] - ) -> StringVar: ... - - @overload - def __getitem__( - self: ArrayVar[tuple[Any, str]], i: Literal[1, -1] - ) -> StringVar: ... - - @overload - def __getitem__( - self: ArrayVar[tuple[bool, Any]], i: Literal[0, -2] - ) -> BooleanVar: ... - - @overload - def __getitem__( - self: ArrayVar[Sequence[bool]], i: int | NumberVar - ) -> BooleanVar: ... - - @overload - def __getitem__( - self: ( - ArrayVar[Sequence[int]] - | ArrayVar[Sequence[float]] - | ArrayVar[Sequence[int | float]] - ), - i: int | NumberVar, - ) -> NumberVar: ... - - @overload - def __getitem__(self: ArrayVar[Sequence[str]], i: int | NumberVar) -> StringVar: ... - - @overload - def __getitem__( - self: ArrayVar[Sequence[OTHER_ARRAY_VAR_TYPE]], - i: int | NumberVar, - ) -> ArrayVar[OTHER_ARRAY_VAR_TYPE]: ... - - @overload - def __getitem__( - self: ArrayVar[Sequence[MAPPING_VAR_TYPE]], - i: int | NumberVar, - ) -> ObjectVar[MAPPING_VAR_TYPE]: ... - - @overload - def __getitem__( - self: ArrayVar[Sequence[BASE_TYPE]], - i: int | NumberVar, - ) -> ObjectVar[BASE_TYPE]: ... - - @overload - def __getitem__( - self: ArrayVar[Sequence[SQLA_TYPE]], - i: int | NumberVar, - ) -> ObjectVar[SQLA_TYPE]: ... - - @overload - def __getitem__( - self: ArrayVar[Sequence[DATACLASS_TYPE]], - i: int | NumberVar, - ) -> ObjectVar[DATACLASS_TYPE]: ... - - @overload - def __getitem__(self, i: int | NumberVar) -> Var: ... - - def __getitem__(self, i: Any) -> ArrayVar[ARRAY_VAR_TYPE] | Var: - """Get a slice of the array. - - Args: - i: The slice. - - Returns: - The array slice operation. - """ - if isinstance(i, slice): - return ArraySliceOperation.create(self, i) - if not isinstance(i, (int, NumberVar)) or ( - isinstance(i, NumberVar) and i._is_strict_float() - ): - raise_unsupported_operand_types("[]", (type(self), type(i))) - return array_item_operation(self, i) - - def length(self) -> NumberVar[int]: - """Get the length of the array. - - Returns: - The length of the array. - """ - return array_length_operation(self) - - @overload - @classmethod - def range(cls, stop: int | NumberVar, /) -> ArrayVar[list[int]]: ... - - @overload - @classmethod - def range( - cls, - start: int | NumberVar, - end: int | NumberVar, - step: int | NumberVar = 1, - /, - ) -> ArrayVar[list[int]]: ... - - @overload - @classmethod - def range( - cls, - first_endpoint: int | NumberVar, - second_endpoint: int | NumberVar | None = None, - step: int | NumberVar | None = None, - ) -> ArrayVar[list[int]]: ... - - @classmethod - def range( - cls, - first_endpoint: int | NumberVar, - second_endpoint: int | NumberVar | None = None, - step: int | NumberVar | None = None, - ) -> ArrayVar[list[int]]: - """Create a range of numbers. - - Args: - first_endpoint: The end of the range if second_endpoint is not provided, otherwise the start of the range. - second_endpoint: The end of the range. - step: The step of the range. - - Returns: - The range of numbers. - """ - if any( - not isinstance(i, (int, NumberVar)) - for i in (first_endpoint, second_endpoint, step) - if i is not None - ): - raise_unsupported_operand_types( - "range", (type(first_endpoint), type(second_endpoint), type(step)) - ) - if second_endpoint is None: - start = 0 - end = first_endpoint - else: - start = first_endpoint - end = second_endpoint - - return array_range_operation(start, end, step or 1) - - @overload - def contains(self, other: Any) -> BooleanVar: ... - - @overload - def contains(self, other: Any, field: StringVar | str) -> BooleanVar: ... - - def contains(self, other: Any, field: Any = None) -> BooleanVar: - """Check if the array contains an element. - - Args: - other: The element to check for. - field: The field to check. - - Returns: - The array contains operation. - """ - if field is not None: - if not isinstance(field, (StringVar, str)): - raise_unsupported_operand_types("contains", (type(self), type(field))) - return array_contains_field_operation(self, other, field) - return array_contains_operation(self, other) - - def pluck(self, field: StringVar | str) -> ArrayVar: - """Pluck a field from the array. - - Args: - field: The field to pluck from the array. - - Returns: - The array pluck operation. - """ - return array_pluck_operation(self, field) - - def __mul__(self, other: NumberVar | int) -> ArrayVar[ARRAY_VAR_TYPE]: - """Multiply the sequence by a number or integer. - - Parameters: - other: The number or integer to multiply the sequence by. - - Returns: - ArrayVar[ARRAY_VAR_TYPE]: The result of multiplying the sequence by the given number or integer. - """ - if not isinstance(other, (NumberVar, int)) or ( - isinstance(other, NumberVar) and other._is_strict_float() - ): - raise_unsupported_operand_types("*", (type(self), type(other))) - - return repeat_array_operation(self, other) - - __rmul__ = __mul__ - - @overload - def __lt__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> BooleanVar: ... - - @overload - def __lt__(self, other: list | tuple) -> BooleanVar: ... - - def __lt__(self, other: Any): - """Check if the array is less than another array. - - Args: - other: The other array. - - Returns: - The array less than operation. - """ - if not isinstance(other, (ArrayVar, list, tuple)): - raise_unsupported_operand_types("<", (type(self), type(other))) - - return array_lt_operation(self, other) - - @overload - def __gt__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> BooleanVar: ... - - @overload - def __gt__(self, other: list | tuple) -> BooleanVar: ... - - def __gt__(self, other: Any): - """Check if the array is greater than another array. - - Args: - other: The other array. - - Returns: - The array greater than operation. - """ - if not isinstance(other, (ArrayVar, list, tuple)): - raise_unsupported_operand_types(">", (type(self), type(other))) - - return array_gt_operation(self, other) - - @overload - def __le__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> BooleanVar: ... - - @overload - def __le__(self, other: list | tuple) -> BooleanVar: ... - - def __le__(self, other: Any): - """Check if the array is less than or equal to another array. - - Args: - other: The other array. - - Returns: - The array less than or equal operation. - """ - if not isinstance(other, (ArrayVar, list, tuple)): - raise_unsupported_operand_types("<=", (type(self), type(other))) - - return array_le_operation(self, other) - - @overload - def __ge__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> BooleanVar: ... - - @overload - def __ge__(self, other: list | tuple) -> BooleanVar: ... - - def __ge__(self, other: Any): - """Check if the array is greater than or equal to another array. - - Args: - other: The other array. - - Returns: - The array greater than or equal operation. - """ - if not isinstance(other, (ArrayVar, list, tuple)): - raise_unsupported_operand_types(">=", (type(self), type(other))) - - return array_ge_operation(self, other) - - def foreach(self, fn: Any): - """Apply a function to each element of the array. - - Args: - fn: The function to apply. - - Returns: - The array after applying the function. - - Raises: - VarTypeError: If the function takes more than one argument. - """ - from .function import ArgsFunctionOperation - - if not callable(fn): - raise_unsupported_operand_types("foreach", (type(self), type(fn))) - # get the number of arguments of the function - num_args = len(inspect.signature(fn).parameters) - if num_args > 1: - msg = "The function passed to foreach should take at most one argument." - raise VarTypeError(msg) - - if num_args == 0: - return_value = fn() - function_var = ArgsFunctionOperation.create((), return_value) - else: - # generic number var - number_var = Var("").to(NumberVar, int) - - first_arg_type = self[number_var]._var_type - - arg_name = get_unique_variable_name() - - # get first argument type - first_arg = Var( - _js_expr=arg_name, - _var_type=first_arg_type, - ).guess_type() - - function_var = ArgsFunctionOperation.create( - (arg_name,), - Var.create(fn(first_arg)), - ) - - return map_array_operation(self, function_var) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralArrayVar(CachedVarOperation, LiteralVar, ArrayVar[ARRAY_VAR_TYPE]): - """Base class for immutable literal array vars.""" - - _var_value: Sequence[Var | Any] = dataclasses.field(default=()) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return ( - "[" - + ", ".join([ - str(LiteralVar.create(element)) for element in self._var_value - ]) - + "]" - ) - - @classmethod - def _get_all_var_data_without_creating_var(cls, value: Iterable) -> VarData | None: - """Get all the VarData associated with the Var without creating a Var. - - Args: - value: The value to get the VarData for. - - Returns: - The VarData associated with the Var. - """ - return VarData.merge(*[ - LiteralVar._get_all_var_data_without_creating_var_dispatch(element) - for element in value - ]) - - @cached_property_no_lock - def _cached_get_all_var_data(self) -> VarData | None: - """Get all the VarData associated with the Var. - - Returns: - The VarData associated with the Var. - """ - return VarData.merge( - *[ - LiteralVar._get_all_var_data_without_creating_var_dispatch(element) - for element in self._var_value - ], - self._var_data, - ) - - def __hash__(self) -> int: - """Get the hash of the var. - - Returns: - The hash of the var. - """ - return hash((self.__class__.__name__, self._js_expr)) - - def json(self) -> str: - """Get the JSON representation of the var. - - Returns: - The JSON representation of the var. - - Raises: - TypeError: If the array elements are not of type LiteralVar. - """ - elements = [] - for element in self._var_value: - element_var = LiteralVar.create(element) - if not isinstance(element_var, LiteralVar): - msg = f"Array elements must be of type LiteralVar, not {type(element_var)}" - raise TypeError(msg) - elements.append(element_var.json()) - - return "[" + ", ".join(elements) + "]" - - @classmethod - def create( - cls, - value: OTHER_ARRAY_VAR_TYPE, - _var_type: type[OTHER_ARRAY_VAR_TYPE] | None = None, - _var_data: VarData | None = None, - ) -> LiteralArrayVar[OTHER_ARRAY_VAR_TYPE]: - """Create a var from a string value. - - Args: - value: The value to create the var from. - _var_type: The type of the var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return LiteralArrayVar( - _js_expr="", - _var_type=figure_out_type(value) if _var_type is None else _var_type, - _var_data=_var_data, - _var_value=value, - ) - - -STRING_TYPE = TypingExtensionsTypeVar("STRING_TYPE", default=str) - - -class StringVar(Var[STRING_TYPE], python_types=str): - """Base class for immutable string vars.""" - - def __add__(self, other: StringVar | str) -> ConcatVarOperation: - """Concatenate two strings. - - Args: - other: The other string. - - Returns: - The string concatenation operation. - """ - if not isinstance(other, (StringVar, str)): - raise_unsupported_operand_types("+", (type(self), type(other))) - - return ConcatVarOperation.create(self, other) - - def __radd__(self, other: StringVar | str) -> ConcatVarOperation: - """Concatenate two strings. - - Args: - other: The other string. - - Returns: - The string concatenation operation. - """ - if not isinstance(other, (StringVar, str)): - raise_unsupported_operand_types("+", (type(other), type(self))) - - return ConcatVarOperation.create(other, self) - - def __mul__(self, other: NumberVar | int) -> StringVar: - """Multiply the sequence by a number or an integer. - - Args: - other: The number or integer to multiply the sequence by. - - Returns: - StringVar: The resulting sequence after multiplication. - """ - if not isinstance(other, (NumberVar, int)): - raise_unsupported_operand_types("*", (type(self), type(other))) - - return (self.split() * other).join() - - def __rmul__(self, other: NumberVar | int) -> StringVar: - """Multiply the sequence by a number or an integer. - - Args: - other: The number or integer to multiply the sequence by. - - Returns: - StringVar: The resulting sequence after multiplication. - """ - if not isinstance(other, (NumberVar, int)): - raise_unsupported_operand_types("*", (type(other), type(self))) - - return (self.split() * other).join() - - @overload - def __getitem__(self, i: slice) -> StringVar: ... - - @overload - def __getitem__(self, i: int | NumberVar) -> StringVar: ... - - def __getitem__(self, i: Any) -> StringVar: - """Get a slice of the string. - - Args: - i: The slice. - - Returns: - The string slice operation. - """ - if isinstance(i, slice): - return self.split()[i].join() - if not isinstance(i, (int, NumberVar)) or ( - isinstance(i, NumberVar) and i._is_strict_float() - ): - raise_unsupported_operand_types("[]", (type(self), type(i))) - return string_item_operation(self, i) - - def length(self) -> NumberVar: - """Get the length of the string. - - Returns: - The string length operation. - """ - return self.split().length() - - def lower(self) -> StringVar: - """Convert the string to lowercase. - - Returns: - The string lower operation. - """ - return string_lower_operation(self) - - def upper(self) -> StringVar: - """Convert the string to uppercase. - - Returns: - The string upper operation. - """ - return string_upper_operation(self) - - def title(self) -> StringVar: - """Convert the string to title case. - - Returns: - The string title operation. - """ - return string_title_operation(self) - - def capitalize(self) -> StringVar: - """Capitalize the string. - - Returns: - The string capitalize operation. - """ - return string_capitalize_operation(self) - - def strip(self) -> StringVar: - """Strip the string. - - Returns: - The string strip operation. - """ - return string_strip_operation(self) - - def reversed(self) -> StringVar: - """Reverse the string. - - Returns: - The string reverse operation. - """ - return self.split().reverse().join() - - def contains( - self, other: StringVar | str, field: StringVar | str | None = None - ) -> BooleanVar: - """Check if the string contains another string. - - Args: - other: The other string. - field: The field to check. - - Returns: - The string contains operation. - """ - if not isinstance(other, (StringVar, str)): - raise_unsupported_operand_types("contains", (type(self), type(other))) - if field is not None: - if not isinstance(field, (StringVar, str)): - raise_unsupported_operand_types("contains", (type(self), type(field))) - return string_contains_field_operation(self, other, field) - return string_contains_operation(self, other) - - def split(self, separator: StringVar | str = "") -> ArrayVar[list[str]]: - """Split the string. - - Args: - separator: The separator. - - Returns: - The string split operation. - """ - if not isinstance(separator, (StringVar, str)): - raise_unsupported_operand_types("split", (type(self), type(separator))) - return string_split_operation(self, separator) - - def startswith(self, prefix: StringVar | str) -> BooleanVar: - """Check if the string starts with a prefix. - - Args: - prefix: The prefix. - - Returns: - The string starts with operation. - """ - if not isinstance(prefix, (StringVar, str)): - raise_unsupported_operand_types("startswith", (type(self), type(prefix))) - return string_starts_with_operation(self, prefix) - - def endswith(self, suffix: StringVar | str) -> BooleanVar: - """Check if the string ends with a suffix. - - Args: - suffix: The suffix. - - Returns: - The string ends with operation. - """ - if not isinstance(suffix, (StringVar, str)): - raise_unsupported_operand_types("endswith", (type(self), type(suffix))) - return string_ends_with_operation(self, suffix) - - def __lt__(self, other: StringVar | str) -> BooleanVar: - """Check if the string is less than another string. - - Args: - other: The other string. - - Returns: - The string less than operation. - """ - if not isinstance(other, (StringVar, str)): - raise_unsupported_operand_types("<", (type(self), type(other))) - - return string_lt_operation(self, other) - - def __gt__(self, other: StringVar | str) -> BooleanVar: - """Check if the string is greater than another string. - - Args: - other: The other string. - - Returns: - The string greater than operation. - """ - if not isinstance(other, (StringVar, str)): - raise_unsupported_operand_types(">", (type(self), type(other))) - - return string_gt_operation(self, other) - - def __le__(self, other: StringVar | str) -> BooleanVar: - """Check if the string is less than or equal to another string. - - Args: - other: The other string. - - Returns: - The string less than or equal operation. - """ - if not isinstance(other, (StringVar, str)): - raise_unsupported_operand_types("<=", (type(self), type(other))) - - return string_le_operation(self, other) - - def __ge__(self, other: StringVar | str) -> BooleanVar: - """Check if the string is greater than or equal to another string. - - Args: - other: The other string. - - Returns: - The string greater than or equal operation. - """ - if not isinstance(other, (StringVar, str)): - raise_unsupported_operand_types(">=", (type(self), type(other))) - - return string_ge_operation(self, other) - - @overload - def replace( # pyright: ignore [reportOverlappingOverload] - self, search_value: StringVar | str, new_value: StringVar | str - ) -> StringVar: ... - - @overload - def replace( - self, search_value: Any, new_value: Any - ) -> CustomVarOperationReturn[StringVar]: ... - - def replace(self, search_value: Any, new_value: Any) -> StringVar: # pyright: ignore [reportInconsistentOverload] - """Replace a string with a value. - - Args: - search_value: The string to search. - new_value: The value to be replaced with. - - Returns: - The string replace operation. - """ - if not isinstance(search_value, (StringVar, str)): - raise_unsupported_operand_types("replace", (type(self), type(search_value))) - if not isinstance(new_value, (StringVar, str)): - raise_unsupported_operand_types("replace", (type(self), type(new_value))) - - return string_replace_operation(self, search_value, new_value) - - -@var_operation -def string_lt_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): - """Check if a string is less than another string. - - Args: - lhs: The left-hand side string. - rhs: The right-hand side string. - - Returns: - The string less than operation. - """ - return var_operation_return(js_expression=f"{lhs} < {rhs}", var_type=bool) - - -@var_operation -def string_gt_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): - """Check if a string is greater than another string. - - Args: - lhs: The left-hand side string. - rhs: The right-hand side string. - - Returns: - The string greater than operation. - """ - return var_operation_return(js_expression=f"{lhs} > {rhs}", var_type=bool) - - -@var_operation -def string_le_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): - """Check if a string is less than or equal to another string. - - Args: - lhs: The left-hand side string. - rhs: The right-hand side string. - - Returns: - The string less than or equal operation. - """ - return var_operation_return(js_expression=f"{lhs} <= {rhs}", var_type=bool) - - -@var_operation -def string_ge_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): - """Check if a string is greater than or equal to another string. - - Args: - lhs: The left-hand side string. - rhs: The right-hand side string. - - Returns: - The string greater than or equal operation. - """ - return var_operation_return(js_expression=f"{lhs} >= {rhs}", var_type=bool) - - -@var_operation -def string_lower_operation(string: StringVar[Any]): - """Convert a string to lowercase. - - Args: - string: The string to convert. - - Returns: - The lowercase string. - """ - return var_operation_return(js_expression=f"{string}.toLowerCase()", var_type=str) - - -@var_operation -def string_upper_operation(string: StringVar[Any]): - """Convert a string to uppercase. - - Args: - string: The string to convert. - - Returns: - The uppercase string. - """ - return var_operation_return(js_expression=f"{string}.toUpperCase()", var_type=str) - - -@var_operation -def string_title_operation(string: StringVar[Any]): - """Convert a string to title case. - - Args: - string: The string to convert. - - Returns: - The title case string. - """ - return var_operation_return( - js_expression=f"{string}.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ')", - var_type=str, - ) - - -@var_operation -def string_capitalize_operation(string: StringVar[Any]): - """Capitalize a string. - - Args: - string: The string to capitalize. - - Returns: - The capitalized string. - """ - return var_operation_return( - js_expression=f"(((s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())({string}))", - var_type=str, - ) - - -@var_operation -def string_strip_operation(string: StringVar[Any]): - """Strip a string. - - Args: - string: The string to strip. - - Returns: - The stripped string. - """ - return var_operation_return(js_expression=f"{string}.trim()", var_type=str) - - -@var_operation -def string_contains_field_operation( - haystack: StringVar[Any], needle: StringVar[Any] | str, field: StringVar[Any] | str -): - """Check if a string contains another string. - - Args: - haystack: The haystack. - needle: The needle. - field: The field to check. - - Returns: - The string contains operation. - """ - return var_operation_return( - js_expression=f"{haystack}.some(obj => obj[{field}] === {needle})", - var_type=bool, - ) - - -@var_operation -def string_contains_operation(haystack: StringVar[Any], needle: StringVar[Any] | str): - """Check if a string contains another string. - - Args: - haystack: The haystack. - needle: The needle. - - Returns: - The string contains operation. - """ - return var_operation_return( - js_expression=f"{haystack}.includes({needle})", var_type=bool - ) - - -@var_operation -def string_starts_with_operation( - full_string: StringVar[Any], prefix: StringVar[Any] | str -): - """Check if a string starts with a prefix. - - Args: - full_string: The full string. - prefix: The prefix. - - Returns: - Whether the string starts with the prefix. - """ - return var_operation_return( - js_expression=f"{full_string}.startsWith({prefix})", var_type=bool - ) - - -@var_operation -def string_ends_with_operation( - full_string: StringVar[Any], suffix: StringVar[Any] | str -): - """Check if a string ends with a suffix. - - Args: - full_string: The full string. - suffix: The suffix. - - Returns: - Whether the string ends with the suffix. - """ - return var_operation_return( - js_expression=f"{full_string}.endsWith({suffix})", var_type=bool - ) - - -@var_operation -def string_item_operation(string: StringVar[Any], index: NumberVar | int): - """Get an item from a string. - - Args: - string: The string. - index: The index of the item. - - Returns: - The item from the string. - """ - return var_operation_return(js_expression=f"{string}?.at?.({index})", var_type=str) - - -@var_operation -def array_join_operation(array: ArrayVar, sep: StringVar[Any] | str = ""): - """Join the elements of an array. - - Args: - array: The array. - sep: The separator. - - Returns: - The joined elements. - """ - return var_operation_return(js_expression=f"{array}.join({sep})", var_type=str) - - -@var_operation -def string_replace_operation( - string: StringVar[Any], search_value: StringVar | str, new_value: StringVar | str -): - """Replace a string with a value. - - Args: - string: The string. - search_value: The string to search. - new_value: The value to be replaced with. - - Returns: - The string replace operation. - """ - return var_operation_return( - js_expression=f"{string}.replaceAll({search_value}, {new_value})", - var_type=str, - ) - - -@var_operation -def get_decimal_string_separator_operation(value: NumberVar, separator: StringVar): - """Get the decimal string separator. - - Args: - value: The number. - separator: The separator. - - Returns: - The decimal string separator. - """ - return var_operation_return( - js_expression=f"({value}.toLocaleString('en-US').replaceAll(',', {separator}))", - var_type=str, - ) - - -@var_operation -def get_decimal_string_operation( - value: NumberVar, decimals: NumberVar, separator: StringVar -): - """Get the decimal string of the number. - - Args: - value: The number. - decimals: The number of decimals. - separator: The separator. - - Returns: - The decimal string of the number. - """ - return var_operation_return( - js_expression=f"({value}.toLocaleString('en-US', ((decimals) => ({{minimumFractionDigits: decimals, maximumFractionDigits: decimals}}))({decimals})).replaceAll(',', {separator}))", - var_type=str, - ) - - -# Compile regex for finding reflex var tags. -_decode_var_pattern_re = ( - rf"{constants.REFLEX_VAR_OPENING_TAG}(.*?){constants.REFLEX_VAR_CLOSING_TAG}" -) -_decode_var_pattern = re.compile(_decode_var_pattern_re, flags=re.DOTALL) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralStringVar(LiteralVar, StringVar[str]): - """Base class for immutable literal string vars.""" - - _var_value: str = dataclasses.field(default="") - - @classmethod - def _get_all_var_data_without_creating_var(cls, value: str) -> VarData | None: - """Get all the VarData associated with the Var without creating a Var. - - Args: - value: The value to get the VarData for. - - Returns: - The VarData associated with the Var. - """ - if REFLEX_VAR_OPENING_TAG not in value: - return None - return cls.create(value)._get_all_var_data() - - @classmethod - def create( - cls, - value: str, - _var_type: GenericType | None = None, - _var_data: VarData | None = None, - ) -> StringVar: - """Create a var from a string value. - - Args: - value: The value to create the var from. - _var_type: The type of the var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - # Determine var type in case the value is inherited from str. - _var_type = _var_type or type(value) or str - - if REFLEX_VAR_OPENING_TAG in value: - strings_and_vals: list[Var | str] = [] - offset = 0 - - # Find all tags - while m := _decode_var_pattern.search(value): - start, end = m.span() - - strings_and_vals.append(value[:start]) - - serialized_data = m.group(1) - - if serialized_data.isnumeric() or ( - serialized_data[0] == "-" and serialized_data[1:].isnumeric() - ): - # This is a global immutable var. - var = _global_vars[int(serialized_data)] - strings_and_vals.append(var) - value = value[(end + len(var._js_expr)) :] - - offset += end - start - - strings_and_vals.append(value) - - filtered_strings_and_vals = [ - s for s in strings_and_vals if isinstance(s, Var) or s - ] - if len(filtered_strings_and_vals) == 1: - only_string = filtered_strings_and_vals[0] - if isinstance(only_string, str): - return LiteralVar.create(only_string).to(StringVar, _var_type) - return only_string.to(StringVar, only_string._var_type) - - if len( - literal_strings := [ - s - for s in filtered_strings_and_vals - if isinstance(s, (str, LiteralStringVar)) - ] - ) == len(filtered_strings_and_vals): - return LiteralStringVar.create( - "".join( - s._var_value if isinstance(s, LiteralStringVar) else s - for s in literal_strings - ), - _var_type=_var_type, - _var_data=VarData.merge( - _var_data, - *( - s._get_all_var_data() - for s in filtered_strings_and_vals - if isinstance(s, Var) - ), - ), - ) - - concat_result = ConcatVarOperation.create( - *filtered_strings_and_vals, - _var_data=_var_data, - ) - - return ( - concat_result - if _var_type is str - else concat_result.to(StringVar, _var_type) - ) - - return LiteralStringVar( - _js_expr=json.dumps(value), - _var_type=_var_type, - _var_data=_var_data, - _var_value=value, - ) - - def __hash__(self) -> int: - """Get the hash of the var. - - Returns: - The hash of the var. - """ - return hash((type(self).__name__, self._var_value)) - - def json(self) -> str: - """Get the JSON representation of the var. - - Returns: - The JSON representation of the var. - """ - return json.dumps(self._var_value) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class ConcatVarOperation(CachedVarOperation, StringVar[str]): - """Representing a concatenation of literal string vars.""" - - _var_value: tuple[Var, ...] = dataclasses.field(default_factory=tuple) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - list_of_strs: list[str | Var] = [] - last_string = "" - for var in self._var_value: - if isinstance(var, LiteralStringVar): - last_string += var._var_value - else: - if last_string: - list_of_strs.append(last_string) - last_string = "" - list_of_strs.append(var) - - if last_string: - list_of_strs.append(last_string) - - list_of_strs_filtered = [ - str(LiteralVar.create(s)) for s in list_of_strs if isinstance(s, Var) or s - ] - - if len(list_of_strs_filtered) == 1: - return list_of_strs_filtered[0] - - return "(" + "+".join(list_of_strs_filtered) + ")" - - @cached_property_no_lock - def _cached_get_all_var_data(self) -> VarData | None: - """Get all the VarData asVarDatae Var. - - Returns: - The VarData associated with the Var. - """ - return VarData.merge( - *[ - var._get_all_var_data() - for var in self._var_value - if isinstance(var, Var) - ], - self._var_data, - ) - - @classmethod - def create( - cls, - *value: Var | str, - _var_data: VarData | None = None, - ) -> ConcatVarOperation: - """Create a var from a string value. - - Args: - *value: The values to concatenate. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _js_expr="", - _var_type=str, - _var_data=_var_data, - _var_value=tuple(map(LiteralVar.create, value)), - ) - - -@var_operation -def string_split_operation(string: StringVar[Any], sep: StringVar | str = ""): - """Split a string. - - Args: - string: The string to split. - sep: The separator. - - Returns: - The split string. - """ - return var_operation_return( - js_expression=f"{string}.split({sep})", var_type=list[str] - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class ArraySliceOperation(CachedVarOperation, ArrayVar): - """Base class for immutable string vars that are the result of a string slice operation.""" - - _array: ArrayVar = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]) - ) - _start: NumberVar | int = dataclasses.field(default_factory=lambda: 0) - _stop: NumberVar | int = dataclasses.field(default_factory=lambda: 0) - _step: NumberVar | int = dataclasses.field(default_factory=lambda: 1) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - - Raises: - ValueError: If the slice step is zero. - """ - start, end, step = self._start, self._stop, self._step - - normalized_start = ( - LiteralVar.create(start) if start is not None else Var(_js_expr="undefined") - ) - normalized_end = ( - LiteralVar.create(end) if end is not None else Var(_js_expr="undefined") - ) - if step is None: - return f"{self._array!s}.slice({normalized_start!s}, {normalized_end!s})" - if not isinstance(step, Var): - if step < 0: - actual_start = end + 1 if end is not None else 0 - actual_end = start + 1 if start is not None else self._array.length() - return str(self._array[actual_start:actual_end].reverse()[::-step]) - if step == 0: - msg = "slice step cannot be zero" - raise ValueError(msg) - return f"{self._array!s}.slice({normalized_start!s}, {normalized_end!s}).filter((_, i) => i % {step!s} === 0)" - - actual_start_reverse = end + 1 if end is not None else 0 - actual_end_reverse = start + 1 if start is not None else self._array.length() - - return f"{self.step!s} > 0 ? {self._array!s}.slice({normalized_start!s}, {normalized_end!s}).filter((_, i) => i % {step!s} === 0) : {self._array!s}.slice({actual_start_reverse!s}, {actual_end_reverse!s}).reverse().filter((_, i) => i % {-step!s} === 0)" - - @classmethod - def create( - cls, - array: ArrayVar, - slice: slice, - _var_data: VarData | None = None, - ) -> ArraySliceOperation: - """Create a var from a string value. - - Args: - array: The array. - slice: The slice. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _js_expr="", - _var_type=array._var_type, - _var_data=_var_data, - _array=array, - _start=slice.start, - _stop=slice.stop, - _step=slice.step, - ) - - -@var_operation -def array_pluck_operation( - array: ArrayVar[ARRAY_VAR_TYPE], - field: StringVar | str, -) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: - """Pluck a field from an array of objects. - - Args: - array: The array to pluck from. - field: The field to pluck from the objects in the array. - - Returns: - The reversed array. - """ - return var_operation_return( - js_expression=f"{array}.map(e=>e?.[{field}])", - var_type=array._var_type, - ) - - -@var_operation -def array_reverse_operation( - array: ArrayVar[ARRAY_VAR_TYPE], -) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: - """Reverse an array. - - Args: - array: The array to reverse. - - Returns: - The reversed array. - """ - return var_operation_return( - js_expression=f"{array}.slice().reverse()", - var_type=array._var_type, - ) - - -@var_operation -def array_lt_operation(lhs: ArrayVar | list | tuple, rhs: ArrayVar | list | tuple): - """Check if an array is less than another array. - - Args: - lhs: The left-hand side array. - rhs: The right-hand side array. - - Returns: - The array less than operation. - """ - return var_operation_return(js_expression=f"{lhs} < {rhs}", var_type=bool) - - -@var_operation -def array_gt_operation(lhs: ArrayVar | list | tuple, rhs: ArrayVar | list | tuple): - """Check if an array is greater than another array. - - Args: - lhs: The left-hand side array. - rhs: The right-hand side array. - - Returns: - The array greater than operation. - """ - return var_operation_return(js_expression=f"{lhs} > {rhs}", var_type=bool) - - -@var_operation -def array_le_operation(lhs: ArrayVar | list | tuple, rhs: ArrayVar | list | tuple): - """Check if an array is less than or equal to another array. - - Args: - lhs: The left-hand side array. - rhs: The right-hand side array. - - Returns: - The array less than or equal operation. - """ - return var_operation_return(js_expression=f"{lhs} <= {rhs}", var_type=bool) - - -@var_operation -def array_ge_operation(lhs: ArrayVar | list | tuple, rhs: ArrayVar | list | tuple): - """Check if an array is greater than or equal to another array. - - Args: - lhs: The left-hand side array. - rhs: The right-hand side array. - - Returns: - The array greater than or equal operation. - """ - return var_operation_return(js_expression=f"{lhs} >= {rhs}", var_type=bool) - - -@var_operation -def array_length_operation(array: ArrayVar): - """Get the length of an array. - - Args: - array: The array. - - Returns: - The length of the array. - """ - return var_operation_return( - js_expression=f"{array}.length", - var_type=int, - ) - - -def is_tuple_type(t: GenericType) -> bool: - """Check if a type is a tuple type. - - Args: - t: The type to check. - - Returns: - Whether the type is a tuple type. - """ - return get_origin(t) is tuple - - -def _determine_value_of_array_index( - var_type: GenericType, index: int | float | decimal.Decimal | None = None -): - """Determine the value of an array index. - - Args: - var_type: The type of the array. - index: The index of the array. - - Returns: - The value of the array index. - """ - origin_var_type = get_origin(var_type) or var_type - if origin_var_type in types.UnionTypes: - return unionize(*[ - _determine_value_of_array_index(t, index) - for t in get_args(var_type) - if t is not type(None) - ]) - if origin_var_type is range: - return int - if origin_var_type in [ - Sequence, - Iterable, - list, - set, - collections.abc.Sequence, - collections.abc.Iterable, - ]: - args = get_args(var_type) - return args[0] if args else Any - if origin_var_type is tuple: - args = get_args(var_type) - if len(args) == 2 and args[1] is ...: - return args[0] - return ( - args[int(index) % len(args)] - if args and index is not None - else (unionize(*args) if args else Any) - ) - return Any - - -@var_operation -def array_item_operation(array: ArrayVar, index: NumberVar | int): - """Get an item from an array. - - Args: - array: The array. - index: The index of the item. - - Returns: - The item from the array. - """ - element_type = _determine_value_of_array_index( - array._var_type, - ( - index - if isinstance(index, int) - else (index._var_value if isinstance(index, LiteralNumberVar) else None) - ), - ) - - return var_operation_return( - js_expression=f"{array!s}?.at?.({index!s})", - var_type=element_type, - ) - - -@var_operation -def array_range_operation( - start: NumberVar | int, stop: NumberVar | int, step: NumberVar | int -): - """Create a range of numbers. - - Args: - start: The start of the range. - stop: The end of the range. - step: The step of the range. - - Returns: - The range of numbers. - """ - return var_operation_return( - js_expression=f"Array.from({{ length: Math.ceil(({stop!s} - {start!s}) / {step!s}) }}, (_, i) => {start!s} + i * {step!s})", - var_type=list[int], - ) - - -@var_operation -def array_contains_field_operation( - haystack: ArrayVar, needle: Any | Var, field: StringVar | str -): - """Check if an array contains an element. - - Args: - haystack: The array to check. - needle: The element to check for. - field: The field to check. - - Returns: - The array contains operation. - """ - return var_operation_return( - js_expression=f"{haystack}.some(obj => obj[{field}] === {needle})", - var_type=bool, - ) - - -@var_operation -def array_contains_operation( - haystack: ArrayVar, needle: Any | Var -) -> CustomVarOperationReturn[bool]: - """Check if an array contains an element. - - Args: - haystack: The array to check. - needle: The element to check for. - - Returns: - The array contains operation. - """ - return var_operation_return( - js_expression=f"{haystack}.includes({needle})", - var_type=bool, - ) - - -@var_operation -def repeat_array_operation( - array: ArrayVar[ARRAY_VAR_TYPE], count: NumberVar | int -) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: - """Repeat an array a number of times. - - Args: - array: The array to repeat. - count: The number of times to repeat the array. - - Returns: - The repeated array. - """ - return var_operation_return( - js_expression=f"Array.from({{ length: {count} }}).flatMap(() => {array})", - var_type=array._var_type, - ) - - -@var_operation -def map_array_operation( - array: ArrayVar[ARRAY_VAR_TYPE], - function: FunctionVar, -) -> CustomVarOperationReturn[list[Any]]: - """Map a function over an array. - - Args: - array: The array. - function: The function to map. - - Returns: - The mapped array. - """ - return var_operation_return( - js_expression=f"{array}.map({function})", var_type=list[Any] - ) - - -@var_operation -def array_concat_operation( - lhs: ArrayVar[ARRAY_VAR_TYPE], rhs: ArrayVar[ARRAY_VAR_TYPE] -) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: - """Concatenate two arrays. - - Args: - lhs: The left-hand side array. - rhs: The right-hand side array. - - Returns: - The concatenated array. - """ - return var_operation_return( - js_expression=f"[...{lhs}, ...{rhs}]", - var_type=lhs._var_type | rhs._var_type, - ) - - -class RangeVar(ArrayVar[Sequence[int]], python_types=range): - """Base class for immutable range vars.""" - - -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class LiteralRangeVar(CachedVarOperation, LiteralVar, RangeVar): - """Base class for immutable literal range vars.""" - - _var_value: range = dataclasses.field(default_factory=lambda: range(0)) - - @classmethod - def create( - cls, - value: range, - _var_type: type[range] | None = None, - _var_data: VarData | None = None, - ) -> RangeVar: - """Create a var from a string value. - - Args: - value: The value to create the var from. - _var_type: The type of the var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _js_expr="", - _var_type=_var_type or range, - _var_data=_var_data, - _var_value=value, - ) - - def __hash__(self) -> int: - """Get the hash of the var. - - Returns: - The hash of the var. - """ - return hash(( - self.__class__.__name__, - self._var_value.start, - self._var_value.stop, - self._var_value.step, - )) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"Array.from({{ length: Math.ceil(({self._var_value.stop!s} - {self._var_value.start!s}) / {self._var_value.step!s}) }}, (_, i) => {self._var_value.start!s} + i * {self._var_value.step!s})" - - @cached_property_no_lock - def _cached_get_all_var_data(self) -> VarData | None: - """Get all the var data. - - Returns: - The var data. - """ - return self._var_data - - def json(self) -> str: - """Get the JSON representation of the var. - - Returns: - The JSON representation of the var. - """ - return json.dumps( - list(self._var_value), - ) +from reflex_core.vars.sequence import * diff --git a/scripts/hatch_build.py b/scripts/hatch_build.py index 088946c7393..5ff53bae90a 100644 --- a/scripts/hatch_build.py +++ b/scripts/hatch_build.py @@ -58,7 +58,7 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None: file.unlink(missing_ok=True) subprocess.run( - [sys.executable, "-m", "reflex.utils.pyi_generator"], + [sys.executable, "-m", "reflex_core.utils.pyi_generator"], check=True, ) self.marker().touch() diff --git a/scripts/integration.sh b/scripts/integration.sh index d01b2816588..b20151b5566 100755 --- a/scripts/integration.sh +++ b/scripts/integration.sh @@ -21,7 +21,6 @@ reflex run --loglevel debug --env "$env_mode" "$@" & pid=$! # Within the context of this bash, $pid_in_bash is what we need to pass to "kill" on exit # This is true on all platforms. pid_in_bash=$pid -trap "kill -INT $pid_in_bash ||:" EXIT echo "Started server with PID $pid" @@ -29,9 +28,23 @@ echo "Started server with PID $pid" popd # In Windows, our Python script below needs to work with the WINPID +is_windows=false if [ -f /proc/$pid/winpid ]; then - pid=$(cat /proc/$pid/winpid) - echo "Windows detected, passing winpid $pid to port waiter" + winpid=$(cat /proc/$pid/winpid) + is_windows=true + echo "Windows detected, passing winpid $winpid to port waiter" + pid=$winpid fi +cleanup() { + if [ "$is_windows" = true ]; then + # Use taskkill to kill the entire process tree on Windows, + # so that reflex.exe and child processes release their file locks. + taskkill //F //T //PID $winpid 2>/dev/null ||: + else + kill -INT $pid_in_bash ||: + fi +} +trap cleanup EXIT + python scripts/wait_for_listening_port.py $check_ports --timeout=900 --server-pid "$pid" diff --git a/scripts/make_pyi.py b/scripts/make_pyi.py index f4121d755be..5410d45302c 100644 --- a/scripts/make_pyi.py +++ b/scripts/make_pyi.py @@ -5,14 +5,30 @@ import sys from pathlib import Path -from reflex.utils.pyi_generator import PyiGenerator, _relative_to_pwd +from reflex_core.utils.pyi_generator import PyiGenerator, _relative_to_pwd logger = logging.getLogger("pyi_generator") LAST_RUN_COMMIT_SHA_FILE = Path(".pyi_generator_last_run").resolve() GENERATOR_FILE = Path(__file__).resolve() GENERATOR_DIFF_FILE = Path(".pyi_generator_diff").resolve() -DEFAULT_TARGETS = ["reflex/components", "reflex/experimental", "reflex/__init__.py"] +DEFAULT_TARGETS = [ + "reflex/components", + "reflex/experimental", + "reflex/__init__.py", + "packages/reflex-components-code/src/reflex_components_code", + "packages/reflex-components-core/src/reflex_components_core", + "packages/reflex-components-dataeditor/src/reflex_components_dataeditor", + "packages/reflex-components-gridjs/src/reflex_components_gridjs", + "packages/reflex-components-lucide/src/reflex_components_lucide", + "packages/reflex-components-markdown/src/reflex_components_markdown", + "packages/reflex-components-moment/src/reflex_components_moment", + "packages/reflex-components-plotly/src/reflex_components_plotly", + "packages/reflex-components-radix/src/reflex_components_radix", + "packages/reflex-components-react-player/src/reflex_components_react_player", + "packages/reflex-components-recharts/src/reflex_components_recharts", + "packages/reflex-components-sonner/src/reflex_components_sonner", +] def _git_diff(args: list[str]) -> str: diff --git a/tests/__init__.py b/tests/__init__.py index d0196603cf2..e9d07476c29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,4 +2,4 @@ import os -from reflex import constants +from reflex_core import constants diff --git a/tests/benchmarks/test_compilation.py b/tests/benchmarks/test_compilation.py index 96fbf7e9d2a..e1188ed081b 100644 --- a/tests/benchmarks/test_compilation.py +++ b/tests/benchmarks/test_compilation.py @@ -1,7 +1,7 @@ from pytest_codspeed import BenchmarkFixture +from reflex_core.components.component import Component from reflex.compiler.compiler import _compile_page, _compile_stateful_components -from reflex.components.component import Component def import_templates(): diff --git a/tests/benchmarks/test_evaluate.py b/tests/benchmarks/test_evaluate.py index 4d348afb8b0..d12f9facf79 100644 --- a/tests/benchmarks/test_evaluate.py +++ b/tests/benchmarks/test_evaluate.py @@ -1,8 +1,7 @@ from collections.abc import Callable from pytest_codspeed import BenchmarkFixture - -from reflex.components.component import Component +from reflex_core.components.component import Component def test_evaluate_page( diff --git a/tests/integration/init-test/in_docker_test_script.sh b/tests/integration/init-test/in_docker_test_script.sh index f17fc3e95fd..78589fae46c 100755 --- a/tests/integration/init-test/in_docker_test_script.sh +++ b/tests/integration/init-test/in_docker_test_script.sh @@ -24,14 +24,15 @@ function do_export () { } echo "Preparing test project dir" -python3 -m venv ~/venv -source ~/venv/bin/activate -pip install -U pip +curl -LsSf https://astral.sh/uv/install.sh | sh +source "$HOME/.local/bin/env" echo "Installing reflex from local repo code" cp -r /reflex-repo ~/reflex-repo -pip install ~/reflex-repo -pip install psutil +uv venv ~/venv +source ~/venv/bin/activate +uv pip install ~/reflex-repo +uv pip install psutil redis-server & diff --git a/tests/integration/test_client_storage.py b/tests/integration/test_client_storage.py index 80766407d31..e38edd55ad6 100644 --- a/tests/integration/test_client_storage.py +++ b/tests/integration/test_client_storage.py @@ -6,11 +6,11 @@ from collections.abc import Generator import pytest +from reflex_core.constants.state import FIELD_MARKER from selenium.webdriver.common.by import By from selenium.webdriver.firefox.webdriver import WebDriver as Firefox from selenium.webdriver.remote.webdriver import WebDriver -from reflex.constants.state import FIELD_MARKER from reflex.istate.manager.disk import StateManagerDisk from reflex.istate.manager.memory import StateManagerMemory from reflex.istate.manager.redis import StateManagerRedis diff --git a/tests/integration/test_connection_banner.py b/tests/integration/test_connection_banner.py index b6dbb5e1bf5..8ff1516005d 100644 --- a/tests/integration/test_connection_banner.py +++ b/tests/integration/test_connection_banner.py @@ -4,10 +4,10 @@ from collections.abc import Generator import pytest +from reflex_core import constants from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.common.by import By -from reflex import constants from reflex.environment import environment from reflex.istate.manager.redis import StateManagerRedis from reflex.testing import AppHarness, WebDriver diff --git a/tests/integration/test_extra_overlay_function.py b/tests/integration/test_extra_overlay_function.py index 95dcae3e2b4..e766360f8ba 100644 --- a/tests/integration/test_extra_overlay_function.py +++ b/tests/integration/test_extra_overlay_function.py @@ -25,7 +25,7 @@ def index(): app = rx.App() rx.config.get_config().extra_overlay_function = ( - "reflex.components.radix.themes.components.button" + "reflex_components_radix.themes.components.button" ) app.add_page(index) diff --git a/tests/integration/test_form_submit.py b/tests/integration/test_form_submit.py index a064c1842fb..efe5708758e 100644 --- a/tests/integration/test_form_submit.py +++ b/tests/integration/test_form_submit.py @@ -5,11 +5,11 @@ from collections.abc import Generator import pytest +from reflex_core.utils import format from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from reflex.testing import AppHarness -from reflex.utils import format def FormSubmit(form_component): diff --git a/tests/integration/test_icon.py b/tests/integration/test_icon.py index 43313532a62..bcc59f026ef 100644 --- a/tests/integration/test_icon.py +++ b/tests/integration/test_icon.py @@ -3,15 +3,16 @@ from collections.abc import Generator import pytest +from reflex_components_lucide.icon import LUCIDE_ICON_LIST from selenium.webdriver.common.by import By -from reflex.components.lucide.icon import LUCIDE_ICON_LIST from reflex.testing import AppHarness, WebDriver def Icons(): + from reflex_components_lucide.icon import LUCIDE_ICON_LIST + import reflex as rx - from reflex.components.lucide.icon import LUCIDE_ICON_LIST app = rx.App() diff --git a/tests/integration/test_login_flow.py b/tests/integration/test_login_flow.py index e06008ecfee..7814617a544 100644 --- a/tests/integration/test_login_flow.py +++ b/tests/integration/test_login_flow.py @@ -5,11 +5,11 @@ from collections.abc import Generator import pytest +from reflex_core.constants.state import FIELD_MARKER from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver -from reflex.constants.state import FIELD_MARKER from reflex.testing import AppHarness from . import utils diff --git a/tests/integration/test_upload.py b/tests/integration/test_upload.py index 9af85805a1f..c698efb2b0c 100644 --- a/tests/integration/test_upload.py +++ b/tests/integration/test_upload.py @@ -10,10 +10,10 @@ from urllib.parse import urlsplit import pytest +from reflex_core.constants.event import Endpoint from selenium.webdriver.common.by import By import reflex as rx -from reflex.constants.event import Endpoint from reflex.testing import AppHarness, WebDriver from .utils import poll_for_navigation diff --git a/tests/integration/test_var_operations.py b/tests/integration/test_var_operations.py index f07ddc75295..7de76586e97 100644 --- a/tests/integration/test_var_operations.py +++ b/tests/integration/test_var_operations.py @@ -12,11 +12,13 @@ def VarOperations(): """App with var operations.""" from typing import TypedDict + import pydantic + from reflex_core.vars.base import LiteralVar + from reflex_core.vars.sequence import ArrayVar + import reflex as rx - from reflex.vars.base import LiteralVar - from reflex.vars.sequence import ArrayVar - class Object(rx.Base): + class Object(pydantic.BaseModel): name: str = "hello" optional_none: str | None = None optional_str: str | None = "hello" diff --git a/tests/integration/tests_playwright/test_appearance.py b/tests/integration/tests_playwright/test_appearance.py index 9af5fa38b73..6c847778494 100644 --- a/tests/integration/tests_playwright/test_appearance.py +++ b/tests/integration/tests_playwright/test_appearance.py @@ -7,8 +7,9 @@ def DefaultLightModeApp(): + from reflex_core.style import color_mode + import reflex as rx - from reflex.style import color_mode app = rx.App(theme=rx.theme(appearance="light")) @@ -18,8 +19,9 @@ def index(): def DefaultDarkModeApp(): + from reflex_core.style import color_mode + import reflex as rx - from reflex.style import color_mode app = rx.App(theme=rx.theme(appearance="dark")) @@ -29,8 +31,9 @@ def index(): def DefaultSystemModeApp(): + from reflex_core.style import color_mode + import reflex as rx - from reflex.style import color_mode app = rx.App() @@ -40,8 +43,9 @@ def index(): def ColorToggleApp(): + from reflex_core.style import color_mode, resolved_color_mode, set_color_mode + import reflex as rx - from reflex.style import color_mode, resolved_color_mode, set_color_mode app = rx.App(theme=rx.theme(appearance="light")) diff --git a/tests/units/__init__.py b/tests/units/__init__.py index d0196603cf2..e9d07476c29 100644 --- a/tests/units/__init__.py +++ b/tests/units/__init__.py @@ -2,4 +2,4 @@ import os -from reflex import constants +from reflex_core import constants diff --git a/tests/units/compiler/test_compiler.py b/tests/units/compiler/test_compiler.py index 1a0f87b89a3..b2ce38ad8ca 100644 --- a/tests/units/compiler/test_compiler.py +++ b/tests/units/compiler/test_compiler.py @@ -4,15 +4,15 @@ import pytest from pytest_mock import MockerFixture +from reflex_components_core.base import document +from reflex_components_core.el.elements.metadata import Link +from reflex_core import constants +from reflex_core.constants.compiler import PageNames +from reflex_core.utils.imports import ImportVar, ParsedImportDict +from reflex_core.vars.base import Var +from reflex_core.vars.sequence import LiteralStringVar -from reflex import constants from reflex.compiler import compiler, utils -from reflex.components.base import document -from reflex.components.el.elements.metadata import Link -from reflex.constants.compiler import PageNames -from reflex.utils.imports import ImportVar, ParsedImportDict -from reflex.vars.base import Var -from reflex.vars.sequence import LiteralStringVar @pytest.mark.parametrize( diff --git a/tests/units/components/base/test_bare.py b/tests/units/components/base/test_bare.py index d36813badf2..52fd2b358e4 100644 --- a/tests/units/components/base/test_bare.py +++ b/tests/units/components/base/test_bare.py @@ -1,7 +1,6 @@ import pytest - -from reflex.components.base.bare import Bare -from reflex.vars.base import Var +from reflex_components_core.base.bare import Bare +from reflex_core.vars.base import Var STATE_VAR = Var(_js_expr="default_state.name") diff --git a/tests/units/components/base/test_link.py b/tests/units/components/base/test_link.py index 3c1260e1293..9c78f4350f6 100644 --- a/tests/units/components/base/test_link.py +++ b/tests/units/components/base/test_link.py @@ -1,4 +1,4 @@ -from reflex.components.base.link import RawLink, ScriptTag +from reflex_components_core.base.link import RawLink, ScriptTag def test_raw_link(): diff --git a/tests/units/components/base/test_script.py b/tests/units/components/base/test_script.py index a279dfefc1e..0f25b8d862d 100644 --- a/tests/units/components/base/test_script.py +++ b/tests/units/components/base/test_script.py @@ -1,8 +1,7 @@ """Test that element script renders correctly.""" import pytest - -from reflex.components.base.script import Script +from reflex_components_core.base.script import Script def test_script_inline(): diff --git a/tests/units/components/core/test_banner.py b/tests/units/components/core/test_banner.py index 78646447d6d..1df213f5936 100644 --- a/tests/units/components/core/test_banner.py +++ b/tests/units/components/core/test_banner.py @@ -1,11 +1,10 @@ -from reflex.components.core.banner import ( +from reflex_components_core.core.banner import ( ConnectionBanner, ConnectionModal, ConnectionPulser, WebsocketTargetURL, ) -from reflex.components.radix.themes.base import RadixThemesComponent -from reflex.components.radix.themes.typography.text import Text +from reflex_components_radix.themes.typography.text import Text def test_websocket_target_url(): @@ -25,7 +24,6 @@ def test_connection_banner(): "react", "$/utils/context", "$/utils/state", - RadixThemesComponent.create().library or "", "$/env.json", )) @@ -41,7 +39,6 @@ def test_connection_modal(): "react", "$/utils/context", "$/utils/state", - RadixThemesComponent.create().library or "", "$/env.json", )) diff --git a/tests/units/components/core/test_colors.py b/tests/units/components/core/test_colors.py index aaa82959c92..931385f93ef 100644 --- a/tests/units/components/core/test_colors.py +++ b/tests/units/components/core/test_colors.py @@ -1,10 +1,10 @@ import pytest +from reflex_components_code.code import CodeBlock +from reflex_core.constants.colors import Color +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.vars.base import LiteralVar import reflex as rx -from reflex.components.datadisplay.code import CodeBlock -from reflex.constants.colors import Color -from reflex.constants.state import FIELD_MARKER -from reflex.vars.base import LiteralVar class ColorState(rx.State): diff --git a/tests/units/components/core/test_cond.py b/tests/units/components/core/test_cond.py index 0e1df51d067..b1f7d82d56c 100644 --- a/tests/units/components/core/test_cond.py +++ b/tests/units/components/core/test_cond.py @@ -2,14 +2,14 @@ from typing import Any import pytest +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core.cond import Cond, cond +from reflex_components_radix.themes.typography.text import Text +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.utils.format import format_state_name +from reflex_core.vars.base import LiteralVar, Var, computed_var -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.constants.state import FIELD_MARKER from reflex.state import BaseState -from reflex.utils.format import format_state_name -from reflex.vars.base import LiteralVar, Var, computed_var @pytest.fixture diff --git a/tests/units/components/core/test_debounce.py b/tests/units/components/core/test_debounce.py index 43cca255c70..57e91763fad 100644 --- a/tests/units/components/core/test_debounce.py +++ b/tests/units/components/core/test_debounce.py @@ -1,11 +1,11 @@ """Test that DebounceInput collapses nested forms.""" import pytest +from reflex_components_core.core.debounce import DEFAULT_DEBOUNCE_TIMEOUT +from reflex_core.vars.base import LiteralVar, Var import reflex as rx -from reflex.components.core.debounce import DEFAULT_DEBOUNCE_TIMEOUT from reflex.state import BaseState -from reflex.vars.base import LiteralVar, Var def test_create_no_child(): diff --git a/tests/units/components/core/test_foreach.py b/tests/units/components/core/test_foreach.py index 01b84bdaef6..71ea1efe494 100644 --- a/tests/units/components/core/test_foreach.py +++ b/tests/units/components/core/test_foreach.py @@ -1,24 +1,24 @@ +import pydantic import pytest - -import reflex as rx -from reflex import el -from reflex.base import Base -from reflex.components.component import Component -from reflex.components.core.foreach import ( +from reflex_components_core.core.foreach import ( Foreach, ForeachRenderError, ForeachVarError, foreach, ) -from reflex.components.radix.themes.layout.box import box -from reflex.components.radix.themes.typography.text import text -from reflex.constants.state import FIELD_MARKER +from reflex_components_radix.themes.layout.box import box +from reflex_components_radix.themes.typography.text import text +from reflex_core.components.component import Component +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.vars.number import NumberVar +from reflex_core.vars.sequence import ArrayVar + +import reflex as rx +from reflex import el from reflex.state import BaseState, ComponentState -from reflex.vars.number import NumberVar -from reflex.vars.sequence import ArrayVar -class ForEachTag(Base): +class ForEachTag(pydantic.BaseModel): """A tag for testing the ForEach component.""" name: str = "" diff --git a/tests/units/components/core/test_html.py b/tests/units/components/core/test_html.py index 8ab466a4e33..4241c9344ab 100644 --- a/tests/units/components/core/test_html.py +++ b/tests/units/components/core/test_html.py @@ -1,6 +1,6 @@ import pytest +from reflex_components_core.core.html import Html -from reflex.components.core.html import Html from reflex.state import State diff --git a/tests/units/components/core/test_match.py b/tests/units/components/core/test_match.py index 8fd1865fde6..b8877523888 100644 --- a/tests/units/components/core/test_match.py +++ b/tests/units/components/core/test_match.py @@ -1,14 +1,14 @@ import re import pytest +from reflex_components_core.core.match import Match +from reflex_core.components.component import Component +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.utils.exceptions import MatchTypeError +from reflex_core.vars.base import Var import reflex as rx -from reflex.components.component import Component -from reflex.components.core.match import Match -from reflex.constants.state import FIELD_MARKER from reflex.state import BaseState -from reflex.utils.exceptions import MatchTypeError -from reflex.vars.base import Var class MatchState(BaseState): @@ -141,7 +141,7 @@ def test_match_on_component_without_default(): """Test that matching cases with return values as components returns a Fragment as the default case if not provided. """ - from reflex.components.base.fragment import Fragment + from reflex_components_core.base.fragment import Fragment match_case_tuples = ( (1, rx.text("first value")), @@ -249,7 +249,7 @@ def test_match_case_tuple_elements(match_case): ), ( 'Match cases should have the same return types. Case 3 with return value `"red"` of type ' - " is not " + " is not " ), ), ( @@ -264,7 +264,7 @@ def test_match_case_tuple_elements(match_case): ), ( 'Match cases should have the same return types. Case 3 with return value `jsx(RadixThemesText,{as:"p"},"first value")` ' - "of type is not " + "of type is not " ), ), ], diff --git a/tests/units/components/core/test_responsive.py b/tests/units/components/core/test_responsive.py index 6424ed3c3d9..b838bcf270b 100644 --- a/tests/units/components/core/test_responsive.py +++ b/tests/units/components/core/test_responsive.py @@ -1,38 +1,38 @@ -from reflex.components.core.responsive import ( +from reflex_components_core.core.responsive import ( desktop_only, mobile_and_tablet, mobile_only, tablet_and_desktop, tablet_only, ) -from reflex.components.radix.themes.layout.box import Box +from reflex_components_core.el.elements.typography import Div def test_mobile_only(): """Test the mobile_only responsive component.""" component = mobile_only("Content") - assert isinstance(component, Box) + assert isinstance(component, Div) def test_tablet_only(): """Test the tablet_only responsive component.""" component = tablet_only("Content") - assert isinstance(component, Box) + assert isinstance(component, Div) def test_desktop_only(): """Test the desktop_only responsive component.""" component = desktop_only("Content") - assert isinstance(component, Box) + assert isinstance(component, Div) def test_tablet_and_desktop(): """Test the tablet_and_desktop responsive component.""" component = tablet_and_desktop("Content") - assert isinstance(component, Box) + assert isinstance(component, Div) def test_mobile_and_tablet(): """Test the mobile_and_tablet responsive component.""" component = mobile_and_tablet("Content") - assert isinstance(component, Box) + assert isinstance(component, Div) diff --git a/tests/units/components/core/test_upload.py b/tests/units/components/core/test_upload.py index ddeed335c59..f7ce6b6c0c5 100644 --- a/tests/units/components/core/test_upload.py +++ b/tests/units/components/core/test_upload.py @@ -1,10 +1,7 @@ from typing import Any, cast import pytest - -import reflex as rx -from reflex import event -from reflex.components.core.upload import ( +from reflex_components_core.core.upload import ( StyledUpload, Upload, UploadNamespace, @@ -12,9 +9,12 @@ cancel_upload, get_upload_url, ) -from reflex.event import EventChain, EventHandler, EventSpec +from reflex_core.event import EventChain, EventHandler, EventSpec +from reflex_core.vars.base import LiteralVar, Var + +import reflex as rx +from reflex import event from reflex.state import State -from reflex.vars.base import LiteralVar, Var class UploadStateTest(State): diff --git a/tests/units/components/datadisplay/test_code.py b/tests/units/components/datadisplay/test_code.py index 85d60cf60b6..5b20c0584ea 100644 --- a/tests/units/components/datadisplay/test_code.py +++ b/tests/units/components/datadisplay/test_code.py @@ -1,6 +1,5 @@ import pytest - -from reflex.components.datadisplay.code import CodeBlock, Theme +from reflex_components_code.code import CodeBlock, Theme @pytest.mark.parametrize( diff --git a/tests/units/components/datadisplay/test_dataeditor.py b/tests/units/components/datadisplay/test_dataeditor.py index 6b0d3ac6103..301149710ae 100644 --- a/tests/units/components/datadisplay/test_dataeditor.py +++ b/tests/units/components/datadisplay/test_dataeditor.py @@ -1,4 +1,4 @@ -from reflex.components.datadisplay.dataeditor import DataEditor +from reflex_components_dataeditor.dataeditor import DataEditor def test_dataeditor(): diff --git a/tests/units/components/datadisplay/test_datatable.py b/tests/units/components/datadisplay/test_datatable.py index 8142870e465..84b6c3a0f8a 100644 --- a/tests/units/components/datadisplay/test_datatable.py +++ b/tests/units/components/datadisplay/test_datatable.py @@ -1,12 +1,12 @@ import pandas as pd import pytest +from reflex_components_gridjs.datatable import DataTable +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.utils.exceptions import UntypedComputedVarError +from reflex_core.utils.serializers import serialize, serialize_dataframe import reflex as rx -from reflex.components.gridjs.datatable import DataTable -from reflex.constants.state import FIELD_MARKER from reflex.utils import types -from reflex.utils.exceptions import UntypedComputedVarError -from reflex.utils.serializers import serialize, serialize_dataframe @pytest.mark.parametrize( diff --git a/tests/units/components/datadisplay/test_shiki_code.py b/tests/units/components/datadisplay/test_shiki_code.py index 05553815409..690765ea3e7 100644 --- a/tests/units/components/datadisplay/test_shiki_code.py +++ b/tests/units/components/datadisplay/test_shiki_code.py @@ -1,17 +1,16 @@ import pytest - -from reflex.components.datadisplay.shiki_code_block import ( +from reflex_components_code.shiki_code_block import ( ShikiBaseTransformers, ShikiCodeBlock, ShikiHighLevelCodeBlock, ShikiJsTransformer, ) -from reflex.components.el.elements.forms import Button -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.layout.box import Box -from reflex.style import Style -from reflex.vars import Var -from reflex.vars.base import LiteralVar +from reflex_components_core.el.elements.forms import Button +from reflex_components_lucide.icon import Icon +from reflex_components_radix.themes.layout.box import Box +from reflex_core.style import Style +from reflex_core.vars import Var +from reflex_core.vars.base import LiteralVar @pytest.mark.parametrize( diff --git a/tests/units/components/el/test_svg.py b/tests/units/components/el/test_svg.py index d7d46898508..951b4a3afaa 100644 --- a/tests/units/components/el/test_svg.py +++ b/tests/units/components/el/test_svg.py @@ -1,4 +1,4 @@ -from reflex.components.el.elements.media import ( +from reflex_components_core.el.elements.media import ( Circle, Defs, Ellipse, diff --git a/tests/units/components/forms/test_form.py b/tests/units/components/forms/test_form.py index 6b3d4a06001..d5b636711e1 100644 --- a/tests/units/components/forms/test_form.py +++ b/tests/units/components/forms/test_form.py @@ -1,6 +1,6 @@ -from reflex.components.radix.primitives.form import Form -from reflex.event import EventChain, prevent_default -from reflex.vars.base import Var +from reflex_components_radix.primitives.form import Form +from reflex_core.event import EventChain, prevent_default +from reflex_core.vars.base import Var def test_render_on_submit(): diff --git a/tests/units/components/graphing/test_plotly.py b/tests/units/components/graphing/test_plotly.py index d060a0fd3d3..46d70a20e9c 100644 --- a/tests/units/components/graphing/test_plotly.py +++ b/tests/units/components/graphing/test_plotly.py @@ -1,9 +1,9 @@ import numpy as np import plotly.graph_objects as go import pytest +from reflex_core.utils.serializers import serialize, serialize_figure import reflex as rx -from reflex.utils.serializers import serialize, serialize_figure @pytest.fixture diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index c1b986dfda5..3e4268eb891 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -1,4 +1,4 @@ -from reflex.components.recharts.charts import ( +from reflex_components_recharts.charts import ( AreaChart, BarChart, LineChart, @@ -7,7 +7,7 @@ RadialBarChart, ScatterChart, ) -from reflex.components.recharts.general import ResponsiveContainer +from reflex_components_recharts.general import ResponsiveContainer def test_area_chart(): diff --git a/tests/units/components/lucide/test_icon.py b/tests/units/components/lucide/test_icon.py index 0f183abc7ae..5412f05c17f 100644 --- a/tests/units/components/lucide/test_icon.py +++ b/tests/units/components/lucide/test_icon.py @@ -1,11 +1,10 @@ import pytest - -from reflex.components.lucide.icon import ( +from reflex_components_lucide.icon import ( LUCIDE_ICON_LIST, LUCIDE_ICON_MAPPING_OVERRIDE, Icon, ) -from reflex.utils import format +from reflex_core.utils import format @pytest.mark.parametrize("tag", LUCIDE_ICON_LIST) diff --git a/tests/units/components/markdown/test_markdown.py b/tests/units/components/markdown/test_markdown.py index b91084a09fa..c56a8e67457 100644 --- a/tests/units/components/markdown/test_markdown.py +++ b/tests/units/components/markdown/test_markdown.py @@ -1,12 +1,12 @@ import pytest - -from reflex.components.component import Component, memo -from reflex.components.datadisplay.code import CodeBlock -from reflex.components.datadisplay.shiki_code_block import ShikiHighLevelCodeBlock -from reflex.components.markdown.markdown import Markdown, MarkdownComponentMap -from reflex.components.radix.themes.layout.box import Box -from reflex.components.radix.themes.typography.heading import Heading -from reflex.vars.base import Var +from reflex_components_code.code import CodeBlock +from reflex_components_code.shiki_code_block import ShikiHighLevelCodeBlock +from reflex_components_core.core.markdown_component_map import MarkdownComponentMap +from reflex_components_markdown.markdown import Markdown +from reflex_components_radix.themes.layout.box import Box +from reflex_components_radix.themes.typography.heading import Heading +from reflex_core.components.component import Component, memo +from reflex_core.vars.base import Var class CustomMarkdownComponent(Component, MarkdownComponentMap): diff --git a/tests/units/components/media/test_image.py b/tests/units/components/media/test_image.py index 3ab74eb688f..103b6fb135c 100644 --- a/tests/units/components/media/test_image.py +++ b/tests/units/components/media/test_image.py @@ -2,9 +2,9 @@ import PIL import pytest from PIL.Image import Image as Img +from reflex_core.utils.serializers import serialize, serialize_image import reflex as rx -from reflex.utils.serializers import serialize, serialize_image @pytest.fixture diff --git a/tests/units/components/radix/test_icon_button.py b/tests/units/components/radix/test_icon_button.py index 8047cf7b2d9..5622b6d1dc7 100644 --- a/tests/units/components/radix/test_icon_button.py +++ b/tests/units/components/radix/test_icon_button.py @@ -1,9 +1,8 @@ import pytest - -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.components.icon_button import IconButton -from reflex.style import Style -from reflex.vars.base import LiteralVar +from reflex_components_lucide.icon import Icon +from reflex_components_radix.themes.components.icon_button import IconButton +from reflex_core.style import Style +from reflex_core.vars.base import LiteralVar def test_icon_button(): diff --git a/tests/units/components/radix/test_layout.py b/tests/units/components/radix/test_layout.py index 73fcde2a87a..f84b27fda2f 100644 --- a/tests/units/components/radix/test_layout.py +++ b/tests/units/components/radix/test_layout.py @@ -1,4 +1,4 @@ -from reflex.components.radix.themes.layout.base import LayoutComponent +from reflex_components_radix.themes.layout.base import LayoutComponent def test_layout_component(): diff --git a/tests/units/components/recharts/test_cartesian.py b/tests/units/components/recharts/test_cartesian.py index 3337427bd13..078455f4411 100644 --- a/tests/units/components/recharts/test_cartesian.py +++ b/tests/units/components/recharts/test_cartesian.py @@ -1,4 +1,4 @@ -from reflex.components.recharts import ( +from reflex_components_recharts import ( Area, Bar, Brush, diff --git a/tests/units/components/recharts/test_polar.py b/tests/units/components/recharts/test_polar.py index 4e4af0f498c..27c3561a44c 100644 --- a/tests/units/components/recharts/test_polar.py +++ b/tests/units/components/recharts/test_polar.py @@ -1,4 +1,4 @@ -from reflex.components.recharts import ( +from reflex_components_recharts import ( Pie, PolarAngleAxis, PolarGrid, diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 925873703a8..98df3eb85a7 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -1,24 +1,22 @@ from contextlib import nullcontext from typing import Any, ClassVar +import pydantic import pytest - -import reflex as rx -from reflex.base import Base -from reflex.compiler.utils import compile_custom_component -from reflex.components.base.bare import Bare -from reflex.components.base.fragment import Fragment -from reflex.components.component import ( +from reflex_components_core.base.bare import Bare +from reflex_components_core.base.fragment import Fragment +from reflex_components_radix.mappings import RADIX_MAPPING +from reflex_components_radix.themes.layout.box import Box +from reflex_core.components.component import ( CUSTOM_COMPONENTS, Component, CustomComponent, StatefulComponent, custom_component, ) -from reflex.components.radix.themes.layout.box import Box -from reflex.constants import EventTriggers -from reflex.constants.state import FIELD_MARKER -from reflex.event import ( +from reflex_core.constants import EventTriggers +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.event import ( EventChain, EventHandler, JavascriptInputEvent, @@ -27,18 +25,30 @@ parse_args_spec, passthrough_event_spec, ) -from reflex.state import BaseState -from reflex.style import Style -from reflex.utils import imports -from reflex.utils.exceptions import ( +from reflex_core.style import Style +from reflex_core.utils.exceptions import ( ChildrenTypeError, EventFnArgMismatchError, EventHandlerArgTypeMismatchError, ) -from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var -from reflex.vars.object import ObjectVar +from reflex_core.utils.imports import ( + ImportDict, + ImportVar, + ParsedImportDict, + parse_imports, +) +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.object import ObjectVar + +import reflex as rx +from reflex import ( + _COMPONENTS_BASE_MAPPING, # pyright: ignore[reportAttributeAccessIssue] + _COMPONENTS_CORE_MAPPING, # pyright: ignore[reportAttributeAccessIssue] +) +from reflex.compiler.utils import compile_custom_component +from reflex.state import BaseState +from reflex.utils import imports class TestState(BaseState): @@ -792,7 +802,7 @@ def test_component_create_unpack_tuple_child(test_component, element, expected): assert fragment_wrapper.render() == expected -class _Obj(Base): +class _Obj(pydantic.BaseModel): custom: int = 0 @@ -861,7 +871,7 @@ def my_component(width: Var[int], color: Var[str]): color=color, ) - from reflex.components.radix.themes.typography.text import Text + from reflex_components_radix.themes.typography.text import Text ccomponent = my_component( rx.text("child"), width=LiteralVar.create(1), color=LiteralVar.create("red") @@ -1468,9 +1478,9 @@ def test_instantiate_all_components(): "Thead", } component_nested_list = [ - *rx.RADIX_MAPPING.values(), - *rx.COMPONENTS_BASE_MAPPING.values(), - *rx.COMPONENTS_CORE_MAPPING.values(), + *RADIX_MAPPING.values(), + *_COMPONENTS_BASE_MAPPING.values(), + *_COMPONENTS_CORE_MAPPING.values(), ] for component_name in [ comp_name diff --git a/tests/units/components/test_component_future_annotations.py b/tests/units/components/test_component_future_annotations.py index 29f6e821cad..56d7565b1c1 100644 --- a/tests/units/components/test_component_future_annotations.py +++ b/tests/units/components/test_component_future_annotations.py @@ -2,8 +2,8 @@ from typing import Any -from reflex.components.component import Component -from reflex.event import EventHandler, input_event, no_args_event_spec +from reflex_core.components.component import Component +from reflex_core.event import EventHandler, input_event, no_args_event_spec # This is a repeat of its namesake in test_component.py. diff --git a/tests/units/components/test_component_state.py b/tests/units/components/test_component_state.py index 1b62e35c81d..3dcd7a01694 100644 --- a/tests/units/components/test_component_state.py +++ b/tests/units/components/test_component_state.py @@ -1,10 +1,10 @@ """Ensure that Components returned by ComponentState.create have independent State classes.""" import pytest +from reflex_components_core.base.bare import Bare +from reflex_core.utils.exceptions import ReflexRuntimeError import reflex as rx -from reflex.components.base.bare import Bare -from reflex.utils.exceptions import ReflexRuntimeError def test_component_state(): diff --git a/tests/units/components/test_props.py b/tests/units/components/test_props.py index cbe7d3e52b8..cc437d7476d 100644 --- a/tests/units/components/test_props.py +++ b/tests/units/components/test_props.py @@ -1,17 +1,17 @@ from __future__ import annotations import pytest - -from reflex.components.props import NoExtrasAllowedProps, PropsBase -from reflex.event import ( +from reflex_core.components.props import NoExtrasAllowedProps, PropsBase +from reflex_core.event import ( EventChain, EventHandler, event, no_args_event_spec, passthrough_event_spec, ) +from reflex_core.utils.exceptions import InvalidPropValueError + from reflex.state import State -from reflex.utils.exceptions import InvalidPropValueError class PropA(NoExtrasAllowedProps): diff --git a/tests/units/components/test_tag.py b/tests/units/components/test_tag.py index 32ad2d7068f..822c8c951b0 100644 --- a/tests/units/components/test_tag.py +++ b/tests/units/components/test_tag.py @@ -1,7 +1,6 @@ import pytest - -from reflex.components.tags import CondTag, Tag, tagless -from reflex.vars.base import LiteralVar, Var +from reflex_core.components.tags import CondTag, Tag, tagless +from reflex_core.vars.base import LiteralVar, Var @pytest.mark.parametrize( diff --git a/tests/units/components/typography/test_markdown.py b/tests/units/components/typography/test_markdown.py index d104d4c3812..2ddc591252b 100644 --- a/tests/units/components/typography/test_markdown.py +++ b/tests/units/components/typography/test_markdown.py @@ -1,7 +1,7 @@ import pytest +from reflex_components_markdown.markdown import Markdown import reflex as rx -from reflex.components.markdown.markdown import Markdown @pytest.mark.parametrize( diff --git a/tests/units/conftest.py b/tests/units/conftest.py index 1399832d8be..d330d50d19c 100644 --- a/tests/units/conftest.py +++ b/tests/units/conftest.py @@ -6,10 +6,10 @@ from unittest import mock import pytest +from reflex_core.components.component import CUSTOM_COMPONENTS +from reflex_core.event import EventSpec from reflex.app import App -from reflex.components.component import CUSTOM_COMPONENTS -from reflex.event import EventSpec from reflex.experimental.memo import EXPERIMENTAL_MEMOS from reflex.model import ModelRegistry from reflex.testing import chdir diff --git a/tests/units/docgen/__init__.py b/tests/units/docgen/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/units/test_docgen.py b/tests/units/docgen/test_class_and_component.py similarity index 96% rename from tests/units/test_docgen.py rename to tests/units/docgen/test_class_and_component.py index 3d2adbf38bc..3a2f67067a4 100644 --- a/tests/units/test_docgen.py +++ b/tests/units/docgen/test_class_and_component.py @@ -5,20 +5,19 @@ import sys import pytest -from reflex_docgen import ( - generate_class_documentation, - generate_documentation, - get_component_event_handlers, -) - -from reflex.components.component import ( +from reflex_core.components.component import ( DEFAULT_TRIGGERS_AND_DESC, Component, TriggerDefinition, field, ) -from reflex.constants import EventTriggers -from reflex.event import EventHandler, no_args_event_spec +from reflex_core.constants import EventTriggers +from reflex_core.event import EventHandler, no_args_event_spec +from reflex_docgen import ( + generate_class_documentation, + generate_documentation, + get_component_event_handlers, +) def test_default_triggers_have_descriptions(): @@ -262,11 +261,11 @@ def test_rx_state_fields(): def test_class_with_class_vars(): """Class variables are extracted from __class_vars__.""" - from reflex.base import Base + from pydantic import BaseModel - # Base has __class_vars__ (from pydantic v1) - doc = generate_class_documentation(Base) - # Just verify the attribute is populated (may be empty if Base has no class vars) + # BaseModel has __class_vars__ (from pydantic) + doc = generate_class_documentation(BaseModel) + # Just verify the attribute is populated (may be empty if BaseModel has no class vars) assert isinstance(doc.class_fields, tuple) diff --git a/tests/units/docgen/test_markdown.py b/tests/units/docgen/test_markdown.py new file mode 100644 index 00000000000..0f589406d78 --- /dev/null +++ b/tests/units/docgen/test_markdown.py @@ -0,0 +1,735 @@ +"""Tests for reflex-docgen markdown parsing.""" + +from pathlib import Path + +import pytest +from reflex_docgen.markdown import ( + BoldSpan, + CodeSpan, + ComponentPreview, + FrontMatter, + ImageSpan, + ItalicSpan, + LinkSpan, + StrikethroughSpan, + TextBlock, + TextSpan, + parse_document, +) + +_DOCS_DIR = Path(__file__).resolve().parents[3] / "docs" + + +def test_no_frontmatter(): + """Parsing a document without frontmatter returns None.""" + doc = parse_document("# Hello\n\nWorld\n") + assert doc.frontmatter is None + + +def test_basic_frontmatter(): + """A simple YAML frontmatter block is extracted.""" + source = "---\ntitle: Test\n---\n# Hello\n" + doc = parse_document(source) + assert doc.frontmatter is not None + assert doc.frontmatter.title == "Test" + + +def test_multiline_frontmatter(): + """Multi-line YAML frontmatter is preserved.""" + source = "---\ncomponents:\n - rx.button\n\nButton: |\n lambda **props: rx.button(**props)\n---\n# Button\n" + doc = parse_document(source) + assert doc.frontmatter is not None + assert doc.frontmatter.components == ("rx.button",) + assert len(doc.frontmatter.component_previews) == 1 + assert doc.frontmatter.component_previews[0].name == "Button" + + +def test_frontmatter_not_in_blocks(): + """Frontmatter should not appear in the blocks list.""" + source = "---\ntitle: Test\n---\n# Hello\n" + doc = parse_document(source) + assert not any(isinstance(b, FrontMatter) for b in doc.blocks) + + +def test_empty_frontmatter(): + """Empty frontmatter (no content between ---) is not recognized.""" + doc = parse_document("---\n---\n# Hello\n") + assert doc.frontmatter is None + + +def test_only_low_level_list_true(): + """only_low_level as a YAML list with True is parsed.""" + source = "---\nonly_low_level:\n - True\n---\n# Dialog\n" + fm = parse_document(source).frontmatter + assert fm is not None + assert fm.only_low_level is True + + +def test_only_low_level_scalar(): + """only_low_level as a scalar boolean is parsed.""" + source = "---\nonly_low_level: true\n---\n# Dialog\n" + fm = parse_document(source).frontmatter + assert fm is not None + assert fm.only_low_level is True + + +def test_only_low_level_default_false(): + """only_low_level defaults to False when absent.""" + source = "---\ncomponents:\n - rx.box\n---\n# Box\n" + fm = parse_document(source).frontmatter + assert fm is not None + assert fm.only_low_level is False + + +def test_component_previews(): + """A component preview lambda is extracted from frontmatter.""" + source = '---\ncomponents:\n - rx.button\n\nButton: |\n lambda **props: rx.button("Click me", **props)\n---\n# Button\n' + fm = parse_document(source).frontmatter + assert fm is not None + assert len(fm.component_previews) == 1 + preview = fm.component_previews[0] + assert preview.name == "Button" + assert "rx.button" in preview.source + assert preview.source.startswith("lambda") + + +def test_multiple_previews(): + """Multiple component preview lambdas are extracted.""" + source = "---\ncomponents:\n - rx.input\n\nInput: |\n lambda **props: rx.input(**props)\n\nInputSlot: |\n lambda **props: rx.input(rx.input.slot(**props))\n---\n# Input\n" + fm = parse_document(source).frontmatter + assert fm is not None + assert len(fm.component_previews) == 2 + names = [p.name for p in fm.component_previews] + assert "Input" in names + assert "InputSlot" in names + + +def test_as_markdown_frontmatter_with_previews(): + """FrontMatter with component previews renders correctly.""" + fm = FrontMatter( + components=("rx.button",), + only_low_level=False, + title=None, + component_previews=( + ComponentPreview( + name="Button", + source='lambda **props: rx.button("Click", **props)', + ), + ), + ) + md = fm.as_markdown() + assert md.startswith("---\n") + assert md.endswith("\n---") + assert "rx.button" in md + assert "Button:" in md + + +def test_as_markdown_frontmatter_with_only_low_level(): + """FrontMatter with only_low_level renders the field.""" + fm = FrontMatter( + components=(), + only_low_level=True, + title=None, + component_previews=(), + ) + assert "only_low_level" in fm.as_markdown() + + +def test_h1(): + """A level-1 heading is parsed correctly.""" + doc = parse_document("# Title\n") + assert len(doc.headings) == 1 + assert doc.headings[0].level == 1 + assert doc.headings[0].children == (TextSpan(text="Title"),) + + +def test_multiple_heading_levels(): + """Headings at different levels are all captured.""" + source = "# H1\n\n## H2\n\n### H3\n" + doc = parse_document(source) + assert len(doc.headings) == 3 + assert [h.level for h in doc.headings] == [1, 2, 3] + assert [h.children for h in doc.headings] == [ + (TextSpan(text="H1"),), + (TextSpan(text="H2"),), + (TextSpan(text="H3"),), + ] + + +def test_heading_with_inline_code(): + """Inline code in headings is preserved as a CodeSpan.""" + doc = parse_document("# The `rx.button` Component\n") + children = doc.headings[0].children + assert children == ( + TextSpan(text="The "), + CodeSpan(code="rx.button"), + TextSpan(text=" Component"), + ) + + +def test_plain_code_block(): + """A fenced code block with only a language is parsed.""" + source = "```python\nx = 1\n```\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 1 + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == () + assert cb.content == "x = 1" + + +def test_code_block_with_flags(): + """A fenced code block with demo and exec flags is parsed.""" + source = "```python demo exec\nclass Foo:\n pass\n```\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 1 + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("demo", "exec") + + +def test_code_block_demo_only(): + """A fenced code block with only the demo flag is parsed.""" + source = "```python demo\nrx.button('Click')\n```\n" + doc = parse_document(source) + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("demo",) + + +def test_code_block_exec_only(): + """A fenced code block with only the exec flag is parsed.""" + source = "```python exec\nimport reflex as rx\n```\n" + doc = parse_document(source) + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("exec",) + + +def test_code_block_eval(): + """A fenced code block with the eval flag is parsed.""" + source = "```python eval\nrx.text('hello')\n```\n" + doc = parse_document(source) + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("eval",) + + +def test_code_block_no_language(): + """A fenced code block without a language is parsed.""" + source = "```\nplain text\n```\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 1 + cb = doc.code_blocks[0] + assert cb.language is None + assert cb.flags == () + + +def test_directive_alert(): + """An md alert directive is parsed as a DirectiveBlock.""" + source = "```md alert\n# Warning Title\n\nThis is the body.\n```\n" + doc = parse_document(source) + assert len(doc.directives) == 1 + d = doc.directives[0] + assert d.name == "alert" + assert d.args == () + + +def test_directive_alert_with_variant(): + """An md alert with a variant like 'info' preserves the variant as an arg.""" + source = "```md alert info\n# Note\nSome info.\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "alert" + assert d.args == ("info",) + + +def test_directive_alert_warning(): + """An md alert warning directive preserves the warning arg.""" + source = "```md alert warning\nDo not do this.\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "alert" + assert d.args == ("warning",) + + +def test_directive_video(): + """An md video directive captures the URL as an arg.""" + source = "```md video https://youtube.com/embed/abc123\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "video" + assert d.args == ("https://youtube.com/embed/abc123",) + + +def test_directive_definition(): + """An md definition directive is parsed.""" + source = "```md definition\nSome definition content.\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "definition" + assert d.args == () + assert d.content == "Some definition content." + + +def test_directive_section(): + """An md section directive is parsed.""" + source = "```md section\nSection content here.\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "section" + assert d.args == () + + +def test_directive_not_in_code_blocks(): + """Directive blocks should not appear in the code_blocks list.""" + source = "```md alert\nBody\n```\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 0 + assert len(doc.directives) == 1 + + +def test_simple_paragraph(): + """A plain paragraph is captured as a TextBlock with a TextSpan.""" + doc = parse_document("Hello world.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert len(text_blocks) == 1 + assert text_blocks[0].children == (TextSpan(text="Hello world."),) + + +def test_paragraph_with_inline_code(): + """Inline code in paragraphs is preserved as a CodeSpan.""" + doc = parse_document("Use `rx.button` for buttons.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="Use "), + CodeSpan(code="rx.button"), + TextSpan(text=" for buttons."), + ) + + +def test_bold_text(): + """Bold text is parsed into a BoldSpan.""" + doc = parse_document("This is **bold** text.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="This is "), + BoldSpan(children=(TextSpan(text="bold"),)), + TextSpan(text=" text."), + ) + + +def test_italic_text(): + """Italic text is parsed into an ItalicSpan.""" + doc = parse_document("This is *italic* text.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="This is "), + ItalicSpan(children=(TextSpan(text="italic"),)), + TextSpan(text=" text."), + ) + + +def test_strikethrough_text(): + """Strikethrough text is parsed into a StrikethroughSpan.""" + doc = parse_document("This is ~~struck~~ text.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="This is "), + StrikethroughSpan(children=(TextSpan(text="struck"),)), + TextSpan(text=" text."), + ) + + +def test_link(): + """Links are parsed into LinkSpans.""" + doc = parse_document("Click [here](http://example.com) now.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="Click "), + LinkSpan(children=(TextSpan(text="here"),), target="http://example.com"), + TextSpan(text=" now."), + ) + + +def test_image(): + """Images are parsed into ImageSpans.""" + doc = parse_document("See ![alt text](image.png) here.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="See "), + ImageSpan(children=(TextSpan(text="alt text"),), src="image.png"), + TextSpan(text=" here."), + ) + + +def test_nested_spans(): + """Bold containing code is parsed as nested spans.""" + doc = parse_document("Use **`rx.button`** here.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="Use "), + BoldSpan(children=(CodeSpan(code="rx.button"),)), + TextSpan(text=" here."), + ) + + +def test_mixed_document(): + """A document with frontmatter, headings, code, and directives is fully parsed.""" + source = ( + "---\ntitle: Test\n---\n" + "# Title\n\n" + "Some text.\n\n" + "```python demo\ncode()\n```\n\n" + "```md alert\nAlert body.\n```\n\n" + "## Section\n" + ) + doc = parse_document(source) + assert doc.frontmatter is not None + assert len(doc.headings) == 2 + assert len(doc.code_blocks) == 1 + assert len(doc.directives) == 1 + + +def test_empty_document(): + """An empty string produces an empty Document.""" + doc = parse_document("") + assert doc.frontmatter is None + assert doc.blocks == () + + +def test_realistic_doc_structure(): + """Verify parsing of a realistic Reflex doc structure.""" + source = """\ +--- +components: + - rx.button +--- + +```python exec +import reflex as rx +``` + +# Button + +Buttons trigger events. + +```python demo exec +class State(rx.State): + count: int = 0 + +def button_demo(): + return rx.button("Click", on_click=State.increment) +``` + +```md alert info +# Important + +Use `on_click` for click events. +``` + +```md video https://youtube.com/embed/abc123 + +``` + +## Variants +""" + doc = parse_document(source) + assert doc.frontmatter is not None + assert doc.frontmatter.components == ("rx.button",) + assert len(doc.headings) == 2 + assert doc.headings[0].children == (TextSpan(text="Button"),) + assert doc.headings[1].children == (TextSpan(text="Variants"),) + assert len(doc.code_blocks) == 2 + assert doc.code_blocks[0].flags == ("exec",) + assert doc.code_blocks[1].flags == ("demo", "exec") + assert len(doc.directives) == 2 + assert doc.directives[0].name == "alert" + assert doc.directives[0].args == ("info",) + assert doc.directives[1].name == "video" + + +_ALL_MD_FILES = sorted(_DOCS_DIR.rglob("*.md")) + + +@pytest.mark.parametrize( + "md_file", _ALL_MD_FILES, ids=lambda p: str(p.relative_to(_DOCS_DIR)) +) +def test_parse_all_doc_files(md_file: Path): + """Every markdown file in docs/ should parse without errors.""" + source = md_file.read_text(encoding="utf-8") + doc = parse_document(source) + # Sanity check: a non-empty file should produce at least one block. + if source.strip(): + assert len(doc.blocks) > 0 + # Verify as_markdown doesn't crash on any doc file. + doc.as_markdown() + + +# --------------------------------------------------------------------------- +# as_markdown round-trip tests +# --------------------------------------------------------------------------- + + +def test_as_markdown_text_span(): + """TextSpan renders back to plain text.""" + assert TextSpan(text="hello").as_markdown() == "hello" + + +def test_as_markdown_code_span(): + """CodeSpan renders back with backticks.""" + assert CodeSpan(code="rx.button").as_markdown() == "`rx.button`" + + +def test_as_markdown_bold_span(): + """BoldSpan renders back with double asterisks.""" + span = BoldSpan(children=(TextSpan(text="bold"),)) + assert span.as_markdown() == "**bold**" + + +def test_as_markdown_italic_span(): + """ItalicSpan renders back with single asterisks.""" + span = ItalicSpan(children=(TextSpan(text="italic"),)) + assert span.as_markdown() == "*italic*" + + +def test_as_markdown_strikethrough_span(): + """StrikethroughSpan renders back with tildes.""" + span = StrikethroughSpan(children=(TextSpan(text="struck"),)) + assert span.as_markdown() == "~~struck~~" + + +def test_as_markdown_link_span(): + """LinkSpan renders back as a markdown link.""" + span = LinkSpan(children=(TextSpan(text="click"),), target="http://x.com") + assert span.as_markdown() == "[click](http://x.com)" + + +def test_as_markdown_image_span(): + """ImageSpan renders back as a markdown image.""" + span = ImageSpan(children=(TextSpan(text="alt"),), src="img.png") + assert span.as_markdown() == "![alt](img.png)" + + +def test_as_markdown_line_break_soft(): + """Soft LineBreakSpan renders as a newline.""" + from reflex_docgen.markdown import LineBreakSpan + + assert LineBreakSpan(soft=True).as_markdown() == "\n" + + +def test_as_markdown_line_break_hard(): + """Hard LineBreakSpan renders as two spaces + newline.""" + from reflex_docgen.markdown import LineBreakSpan + + assert LineBreakSpan(soft=False).as_markdown() == " \n" + + +def test_as_markdown_nested_spans(): + """Nested spans render correctly.""" + span = BoldSpan(children=(CodeSpan(code="x"), TextSpan(text=" = 1"))) + assert span.as_markdown() == "**`x` = 1**" + + +def test_as_markdown_heading(): + """HeadingBlock renders with the correct number of hashes.""" + from reflex_docgen.markdown import HeadingBlock + + h1 = HeadingBlock(level=1, children=(TextSpan(text="Title"),)) + assert h1.as_markdown() == "# Title" + h3 = HeadingBlock(level=3, children=(TextSpan(text="Sub"),)) + assert h3.as_markdown() == "### Sub" + + +def test_as_markdown_heading_with_inline(): + """HeadingBlock with mixed spans renders correctly.""" + from reflex_docgen.markdown import HeadingBlock + + h = HeadingBlock( + level=2, + children=( + TextSpan(text="The "), + CodeSpan(code="rx.button"), + TextSpan(text=" API"), + ), + ) + assert h.as_markdown() == "## The `rx.button` API" + + +def test_as_markdown_text_block(): + """TextBlock renders its children as a paragraph.""" + block = TextBlock( + children=(TextSpan(text="Hello "), BoldSpan(children=(TextSpan(text="world"),))) + ) + assert block.as_markdown() == "Hello **world**" + + +def test_as_markdown_code_block(): + """CodeBlock renders as a fenced code block.""" + from reflex_docgen.markdown import CodeBlock + + cb = CodeBlock(language="python", flags=("demo", "exec"), content="x = 1") + assert cb.as_markdown() == "```python demo exec\nx = 1\n```" + + +def test_as_markdown_code_block_no_language(): + """CodeBlock without language renders with empty info string.""" + from reflex_docgen.markdown import CodeBlock + + cb = CodeBlock(language=None, flags=(), content="plain") + assert cb.as_markdown() == "```\nplain\n```" + + +def test_as_markdown_directive(): + """DirectiveBlock renders as a fenced md block.""" + from reflex_docgen.markdown import DirectiveBlock + + d = DirectiveBlock(name="alert", args=("info",), content="Be careful.") + assert d.as_markdown() == "```md alert info\nBe careful.\n```" + + +def test_as_markdown_list_unordered(): + """Unordered ListBlock renders with dashes.""" + from reflex_docgen.markdown import ListBlock, ListItem + + lb = ListBlock( + ordered=False, + start=None, + items=( + ListItem(children=(TextBlock(children=(TextSpan(text="one"),)),)), + ListItem(children=(TextBlock(children=(TextSpan(text="two"),)),)), + ), + ) + assert lb.as_markdown() == "- one\n- two" + + +def test_as_markdown_list_ordered(): + """Ordered ListBlock renders with numbers.""" + from reflex_docgen.markdown import ListBlock, ListItem + + lb = ListBlock( + ordered=True, + start=1, + items=( + ListItem(children=(TextBlock(children=(TextSpan(text="first"),)),)), + ListItem(children=(TextBlock(children=(TextSpan(text="second"),)),)), + ), + ) + assert lb.as_markdown() == "1. first\n2. second" + + +def test_as_markdown_quote(): + """QuoteBlock renders with > prefix.""" + from reflex_docgen.markdown import QuoteBlock + + q = QuoteBlock(children=(TextBlock(children=(TextSpan(text="wise words"),)),)) + assert q.as_markdown() == "> wise words" + + +def test_as_markdown_table(): + """TableBlock renders as a markdown table.""" + from reflex_docgen.markdown import TableBlock, TableCell, TableRow + + table = TableBlock( + header=TableRow( + cells=( + TableCell(children=(TextSpan(text="Name"),), align=None), + TableCell(children=(TextSpan(text="Value"),), align="right"), + ) + ), + rows=( + TableRow( + cells=( + TableCell(children=(TextSpan(text="a"),), align=None), + TableCell(children=(TextSpan(text="1"),), align="right"), + ) + ), + ), + ) + expected = "| Name | Value |\n| --- | ---: |\n| a | 1 |" + assert table.as_markdown() == expected + + +def test_as_markdown_thematic_break(): + """ThematicBreakBlock renders as ---.""" + from reflex_docgen.markdown import ThematicBreakBlock + + assert ThematicBreakBlock().as_markdown() == "---" + + +def test_as_markdown_frontmatter(): + """FrontMatter renders with --- delimiters.""" + fm = FrontMatter( + components=(), + only_low_level=False, + title="Test", + component_previews=(), + ) + md = fm.as_markdown() + assert md.startswith("---\n") + assert md.endswith("\n---") + assert "title: Test" in md + + +def test_as_markdown_document_roundtrip(): + """Document.as_markdown produces valid markdown that re-parses consistently.""" + source = """\ +--- +title: Test +--- + +# Hello **world** + +Use `rx.button` for [buttons](http://example.com). + +```python demo exec +x = 1 +``` + +```md alert info +# Warning +Be careful. +``` + +- item one +- item **two** + +--- +""" + doc = parse_document(source) + rendered = doc.as_markdown() + doc2 = parse_document(rendered) + # The re-parsed document should produce the same markdown. + assert doc2.as_markdown() == rendered + + +def test_nested_code_block_in_directive(): + """A directive using more backticks can contain inner code fences.""" + source = "````md alert\n# Example\n\n```python\nx = 1\n```\n````\n" + doc = parse_document(source) + assert len(doc.directives) == 1 + d = doc.directives[0] + assert d.name == "alert" + assert "```python" in d.content + assert "x = 1" in d.content + + +def test_nested_code_block_in_code_block(): + """A code block using more backticks can contain inner code fences.""" + source = "````python demo\nrx.markdown(\n '''```python\nx = 1\n```'''\n)\n````\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 1 + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("demo",) + assert "```python" in cb.content + + +def test_nested_code_block_roundtrip(): + """Nested code blocks survive a parse-render-reparse cycle.""" + source = "````md alert warning\n# Note\n\n```python\nx = 1\n```\n````\n" + doc = parse_document(source) + rendered = doc.as_markdown() + doc2 = parse_document(rendered) + assert len(doc2.directives) == 1 + assert doc2.directives[0].content == doc.directives[0].content diff --git a/tests/units/experimental/test_memo.py b/tests/units/experimental/test_memo.py index 7de8bc012e9..103c1c56c0e 100644 --- a/tests/units/experimental/test_memo.py +++ b/tests/units/experimental/test_memo.py @@ -6,22 +6,22 @@ from typing import Any import pytest +from reflex_core.components.component import CUSTOM_COMPONENTS, Component +from reflex_core.style import Style +from reflex_core.utils.imports import ImportVar +from reflex_core.vars import VarData +from reflex_core.vars.base import Var +from reflex_core.vars.function import FunctionVar import reflex as rx from reflex.compiler import compiler from reflex.compiler import utils as compiler_utils -from reflex.components.component import CUSTOM_COMPONENTS, Component from reflex.experimental.memo import ( EXPERIMENTAL_MEMOS, ExperimentalMemoComponent, ExperimentalMemoComponentDefinition, ExperimentalMemoFunctionDefinition, ) -from reflex.style import Style -from reflex.utils.imports import ImportVar -from reflex.vars import VarData -from reflex.vars.base import Var -from reflex.vars.function import FunctionVar @pytest.fixture(autouse=True) diff --git a/tests/units/middleware/conftest.py b/tests/units/middleware/conftest.py index d786db6521d..559d62c20cb 100644 --- a/tests/units/middleware/conftest.py +++ b/tests/units/middleware/conftest.py @@ -1,6 +1,6 @@ import pytest +from reflex_core.event import Event -from reflex.event import Event from reflex.state import State diff --git a/tests/units/plugins/test_sitemap.py b/tests/units/plugins/test_sitemap.py index 5c7958a772e..72df63ce113 100644 --- a/tests/units/plugins/test_sitemap.py +++ b/tests/units/plugins/test_sitemap.py @@ -3,9 +3,14 @@ import datetime from unittest.mock import MagicMock, patch +from reflex_core.plugins.sitemap import ( + SitemapLink, + generate_links_for_sitemap, + generate_xml, +) + import reflex as rx from reflex.app import UnevaluatedPage -from reflex.plugins.sitemap import SitemapLink, generate_links_for_sitemap, generate_xml def test_generate_xml_empty_links(): @@ -65,8 +70,8 @@ def test_generate_xml_multiple_links_all_fields(): assert xml_output == expected -@patch("reflex.config.get_config") -@patch("reflex.utils.console.warn") +@patch("reflex_core.config.get_config") +@patch("reflex_core.utils.console.warn") def test_generate_links_for_sitemap_static_routes( mock_warn: MagicMock, mock_get_config: MagicMock ): @@ -125,8 +130,8 @@ def mock_component(): mock_warn.assert_not_called() -@patch("reflex.config.get_config") -@patch("reflex.utils.console.warn") +@patch("reflex_core.config.get_config") +@patch("reflex_core.utils.console.warn") def test_generate_links_for_sitemap_dynamic_routes( mock_warn: MagicMock, mock_get_config: MagicMock ): @@ -194,8 +199,8 @@ def mock_component(): ) -@patch("reflex.config.get_config") -@patch("reflex.utils.console.warn") +@patch("reflex_core.config.get_config") +@patch("reflex_core.utils.console.warn") def test_generate_links_for_sitemap_404_route( mock_warn: MagicMock, mock_get_config: MagicMock ): @@ -240,7 +245,7 @@ def mock_component(): ) -@patch("reflex.config.get_config") +@patch("reflex_core.config.get_config") def test_generate_links_for_sitemap_opt_out(mock_get_config: MagicMock): """Test generate_links_for_sitemap with sitemap set to None. @@ -279,7 +284,7 @@ def mock_component(): assert {"loc": "/listed"} in links -@patch("reflex.config.get_config") +@patch("reflex_core.config.get_config") def test_generate_links_for_sitemap_loc_override(mock_get_config: MagicMock): """Test generate_links_for_sitemap with loc override in sitemap config. @@ -319,7 +324,7 @@ def mock_component(): assert {"loc": "http://localhost:3000/custom_pricing"} in links -@patch("reflex.config.get_config") +@patch("reflex_core.config.get_config") def test_generate_links_for_sitemap_priority_clamping(mock_get_config: MagicMock): """Test that priority is clamped between 0.0 and 1.0. @@ -373,7 +378,7 @@ def mock_component(): assert expected_link in links -@patch("reflex.config.get_config") +@patch("reflex_core.config.get_config") def test_generate_links_for_sitemap_no_deploy_url(mock_get_config: MagicMock): """Test generate_links_for_sitemap when deploy_url is not set. @@ -424,7 +429,7 @@ def mock_component(): assert expected_link in links -@patch("reflex.config.get_config") +@patch("reflex_core.config.get_config") def test_generate_links_for_sitemap_deploy_url_trailing_slash( mock_get_config: MagicMock, ): @@ -455,7 +460,7 @@ def mock_component(): assert {"loc": "https://example.com/testpage"} in links -@patch("reflex.config.get_config") +@patch("reflex_core.config.get_config") def test_generate_links_for_sitemap_loc_leading_slash(mock_get_config: MagicMock): """Test generate_links_for_sitemap with loc having a leading slash. @@ -484,7 +489,7 @@ def mock_component(): assert {"loc": "https://example.com/another"} in links -@patch("reflex.config.get_config") +@patch("reflex_core.config.get_config") def test_generate_links_for_sitemap_loc_full_url(mock_get_config: MagicMock): """Test generate_links_for_sitemap with loc being a full URL. diff --git a/tests/units/states/mutation.py b/tests/units/states/mutation.py index 4075f192d4d..74f2434d7c3 100644 --- a/tests/units/states/mutation.py +++ b/tests/units/states/mutation.py @@ -1,23 +1,24 @@ """Test states for mutable vars.""" -import reflex as rx +import pydantic + from reflex.state import BaseState -class OtherBase(rx.Base): - """A Base model with a str field.""" +class OtherBase(pydantic.BaseModel): + """A BaseModel with a str field.""" - bar: str = "" + bar: str = pydantic.Field(default="") -class CustomVar(rx.Base): - """A Base model with multiple fields.""" +class CustomVar(pydantic.BaseModel): + """A BaseModel with multiple fields.""" - foo: str = "" - array: list[str] = [] - hashmap: dict[str, str] = {} - test_set: set[str] = set() - custom: OtherBase = OtherBase() + foo: str = pydantic.Field(default="") + array: list[str] = pydantic.Field(default_factory=list) + hashmap: dict[str, str] = pydantic.Field(default_factory=dict) + test_set: set[str] = pydantic.Field(default_factory=set) + custom: OtherBase = pydantic.Field(default_factory=OtherBase) class MutableTestState(BaseState): diff --git a/tests/units/test_app.py b/tests/units/test_app.py index d71a04b3fdc..d34cb93283d 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -16,6 +16,16 @@ import pytest from pytest_mock import MockerFixture +from reflex_components_core.base.bare import Bare +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core.cond import Cond +from reflex_components_radix.themes.typography.text import Text +from reflex_core.components.component import Component +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.event import Event +from reflex_core.style import Style +from reflex_core.utils import console, exceptions, format +from reflex_core.vars.base import computed_var from starlette.applications import Starlette from starlette.datastructures import FormData, Headers, UploadFile from starlette.responses import StreamingResponse @@ -29,14 +39,7 @@ process, upload, ) -from reflex.components import Component -from reflex.components.base.bare import Bare -from reflex.components.base.fragment import Fragment -from reflex.components.core.cond import Cond -from reflex.components.radix.themes.typography.text import Text -from reflex.constants.state import FIELD_MARKER from reflex.environment import environment -from reflex.event import Event from reflex.istate.manager.disk import StateManagerDisk from reflex.istate.manager.memory import StateManagerMemory from reflex.istate.manager.redis import StateManagerRedis @@ -50,9 +53,6 @@ StateUpdate, _substate_key, ) -from reflex.style import Style -from reflex.utils import console, exceptions, format -from reflex.vars.base import computed_var from .conftest import chdir from .states import GenState @@ -2048,7 +2048,7 @@ def test_app_wrap_compile_theme( mocker: pytest mocker object. """ conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode) - mocker.patch("reflex.config._get_config", return_value=conf) + mocker.patch("reflex_core.config._get_config", return_value=conf) app, web_dir = compilable_app mocker.patch("reflex.utils.prerequisites.get_web_dir", return_value=web_dir) app.theme = rx.theme(accent_color="plum") @@ -2100,7 +2100,7 @@ def test_app_wrap_priority( mocker: pytest mocker object. """ conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode) - mocker.patch("reflex.config._get_config", return_value=conf) + mocker.patch("reflex_core.config._get_config", return_value=conf) app, web_dir = compilable_app @@ -2186,7 +2186,7 @@ def test_call_app(): def test_app_with_optional_endpoints(): - from reflex.components.core.upload import Upload + from reflex_components_core.core.upload import Upload app = App() Upload.is_used = True diff --git a/tests/units/test_attribute_access_type.py b/tests/units/test_attribute_access_type.py index dd81961ef4d..8f360f434a0 100644 --- a/tests/units/test_attribute_access_type.py +++ b/tests/units/test_attribute_access_type.py @@ -4,15 +4,15 @@ import attrs import pytest +from reflex_core.utils.types import GenericType, get_attribute_access_type import reflex as rx -from reflex.utils.types import GenericType, get_attribute_access_type pytest.importorskip("sqlalchemy") pytest.importorskip("sqlmodel") pytest.importorskip("pydantic") -import pydantic.v1 +import pydantic import sqlalchemy import sqlmodel from sqlalchemy import JSON, TypeDecorator @@ -217,19 +217,21 @@ def first_label(self) -> SQLALabel | None: return self.labels[0] if self.labels else None -class BaseClass(rx.Base): - """Test rx.Base class.""" +class BaseClass(pydantic.BaseModel): + """Test pydantic BaseModel class.""" - no_default: int | None = pydantic.v1.Field(required=False) - count: int = 0 - name: str = "test" - int_list: list[int] = [] - str_list: list[str] = [] - optional_int: int | None = None - sqla_tag: SQLATag | None = None - labels: list[SQLALabel] = [] - dict_str_str: dict[str, str] = {} - default_factory: list[int] = pydantic.v1.Field(default_factory=list) + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + no_default: int | None = pydantic.Field(default=None) + count: int = pydantic.Field(default=0) + name: str = pydantic.Field(default="test") + int_list: list[int] = pydantic.Field(default_factory=list) + str_list: list[str] = pydantic.Field(default_factory=list) + optional_int: int | None = pydantic.Field(default=None) + sqla_tag: SQLATag | None = pydantic.Field(default=None) + labels: list[SQLALabel] = pydantic.Field(default_factory=list) + dict_str_str: dict[str, str] = pydantic.Field(default_factory=dict) + default_factory: list[int] = pydantic.Field(default_factory=list) @property def str_property(self) -> str: diff --git a/tests/units/test_base.py b/tests/units/test_base.py deleted file mode 100644 index 796fa9fd86d..00000000000 --- a/tests/units/test_base.py +++ /dev/null @@ -1,77 +0,0 @@ -import pytest - -from reflex.base import Base - -pytest.importorskip("pydantic") - - -@pytest.fixture -def child() -> Base: - """A child class. - - Returns: - A child class. - """ - - class Child(Base): - num: float - key: str - - return Child(num=3.15, key="pi") - - -def test_get_fields(child): - """Test that the fields are set correctly. - - Args: - child: A child class. - """ - assert child.__fields__.keys() == {"num", "key"} - - -def test_json(child): - """Test converting to json. - - Args: - child: A child class. - """ - assert child.json().replace(" ", "") == '{"num":3.15,"key":"pi"}' - - -@pytest.fixture -def complex_child() -> Base: - """A child class. - - Returns: - A child class. - """ - - class Child(Base): - num: float - key: str - name: str - age: int - active: bool - - return Child(num=3.15, key="pi", name="John Doe", age=30, active=True) - - -def test_complex_get_fields(complex_child): - """Test that the fields are set correctly. - - Args: - complex_child: A child class. - """ - assert complex_child.__fields__.keys() == {"num", "key", "name", "age", "active"} - - -def test_complex_json(complex_child): - """Test converting to json. - - Args: - complex_child: A child class. - """ - assert ( - complex_child.json().replace(" ", "") - == '{"num":3.15,"key":"pi","name":"JohnDoe","age":30,"active":true}' - ) diff --git a/tests/units/test_config.py b/tests/units/test_config.py index a29b526f52a..ee64d6e293a 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -4,11 +4,13 @@ from typing import Any import pytest +import reflex_core.config from pytest_mock import MockerFixture +from reflex_core.constants import Endpoint, Env +from reflex_core.plugins import Plugin +from reflex_core.plugins.sitemap import SitemapPlugin import reflex as rx -import reflex.config -from reflex.constants import Endpoint, Env from reflex.environment import ( EnvVar, env_var, @@ -17,8 +19,6 @@ interpret_enum_env, interpret_int_env, ) -from reflex.plugins import Plugin -from reflex.plugins.sitemap import SitemapPlugin def test_requires_app_name(): @@ -155,9 +155,9 @@ def test_event_namespace(mocker: MockerFixture, kwargs, expected): expected: Expected namespace """ conf = rx.Config(**kwargs) - mocker.patch("reflex.config.get_config", return_value=conf) + mocker.patch("reflex_core.config.get_config", return_value=conf) - config = reflex.config.get_config() + config = reflex_core.config.get_config() assert conf == config assert config.get_event_namespace() == expected @@ -246,7 +246,7 @@ def test_replace_defaults( exp_config_values: The expected config values. """ mock_os_env = os.environ.copy() - monkeypatch.setattr(reflex.config.os, "environ", mock_os_env) + monkeypatch.setattr(reflex_core.config.os, "environ", mock_os_env) mock_os_env.update({k: str(v) for k, v in env_vars.items()}) c = rx.Config(app_name="a", **config_kwargs) c._set_persistent(**set_persistent_vars) diff --git a/tests/units/test_db_config.py b/tests/units/test_db_config.py index d778098713a..61699d31b51 100644 --- a/tests/units/test_db_config.py +++ b/tests/units/test_db_config.py @@ -1,8 +1,7 @@ import urllib.parse import pytest - -from reflex.config import DBConfig +from reflex_core.config import DBConfig @pytest.mark.parametrize( diff --git a/tests/units/test_environment.py b/tests/units/test_environment.py index ee22bcc1357..ab1b805a4d1 100644 --- a/tests/units/test_environment.py +++ b/tests/units/test_environment.py @@ -8,9 +8,8 @@ from unittest.mock import patch import pytest - -from reflex import constants -from reflex.environment import ( +from reflex_core import constants +from reflex_core.environment import ( EnvironmentVariables, EnvVar, ExecutorType, @@ -33,8 +32,8 @@ interpret_plugin_class_env, interpret_plugin_env, ) -from reflex.plugins import Plugin -from reflex.utils.exceptions import EnvironmentVarValueError +from reflex_core.plugins import Plugin +from reflex_core.utils.exceptions import EnvironmentVarValueError class TestPlugin(Plugin): @@ -510,7 +509,7 @@ def test_paths_from_environment_not_set(self): result = _paths_from_environment() assert result == [] - @patch("reflex.environment.load_dotenv") + @patch("reflex_core.environment.load_dotenv") def test_load_dotenv_from_files_with_dotenv(self, mock_load_dotenv): """Test _load_dotenv_from_files when dotenv is available. @@ -529,8 +528,8 @@ def test_load_dotenv_from_files_with_dotenv(self, mock_load_dotenv): mock_load_dotenv.assert_any_call(file1, override=True) mock_load_dotenv.assert_any_call(file2, override=True) - @patch("reflex.environment.load_dotenv", None) - @patch("reflex.utils.console") + @patch("reflex_core.environment.load_dotenv", None) + @patch("reflex_core.utils.console") def test_load_dotenv_from_files_without_dotenv(self, mock_console): """Test _load_dotenv_from_files when dotenv is not available. @@ -549,7 +548,7 @@ def test_load_dotenv_from_files_empty_list(self): # Should not raise any errors _load_dotenv_from_files([]) - @patch("reflex.environment.load_dotenv") + @patch("reflex_core.environment.load_dotenv") def test_load_dotenv_from_files_nonexistent_file(self, mock_load_dotenv): """Test _load_dotenv_from_files with non-existent file. diff --git a/tests/units/test_event.py b/tests/units/test_event.py index 50f75cabcf7..9cd80487895 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -3,10 +3,8 @@ from typing import Any, cast import pytest - -import reflex as rx -from reflex.constants.compiler import Hooks, Imports -from reflex.event import ( +from reflex_core.constants.compiler import Hooks, Imports +from reflex_core.event import ( BACKGROUND_TASK_MARKER, Event, EventChain, @@ -18,9 +16,11 @@ event, fix_events, ) +from reflex_core.utils import format +from reflex_core.vars.base import Field, LiteralVar, Var, VarData, field + +import reflex as rx from reflex.state import BaseState -from reflex.utils import format -from reflex.vars.base import Field, LiteralVar, Var, VarData, field def make_var(value) -> Var: @@ -675,7 +675,7 @@ async def handle_old_background(self): def test_event_var_in_rx_cond(): """Test that EventVar and EventChainVar cannot be used in rx.cond().""" - from reflex.components.core.cond import cond as rx_cond + from reflex_components_core.core.cond import cond as rx_cond class S(BaseState): @event diff --git a/tests/units/test_model.py b/tests/units/test_model.py index e7f64f736e5..7cfb7782fd2 100644 --- a/tests/units/test_model.py +++ b/tests/units/test_model.py @@ -3,10 +3,10 @@ from unittest import mock import pytest +from reflex_core.constants.state import FIELD_MARKER import reflex.constants import reflex.model -from reflex.constants.state import FIELD_MARKER from reflex.model import Model, ModelRegistry from reflex.state import BaseState, State from tests.units.test_state import ( diff --git a/tests/units/test_page.py b/tests/units/test_page.py index bf4b637601d..e25792fa043 100644 --- a/tests/units/test_page.py +++ b/tests/units/test_page.py @@ -1,5 +1,6 @@ +from reflex_core.config import get_config + from reflex import text -from reflex.config import get_config from reflex.page import DECORATED_PAGES, page diff --git a/tests/units/test_prerequisites.py b/tests/units/test_prerequisites.py index cf22404bd69..ee528ef9957 100644 --- a/tests/units/test_prerequisites.py +++ b/tests/units/test_prerequisites.py @@ -4,12 +4,12 @@ import pytest from click.testing import CliRunner +from reflex_core.config import Config +from reflex_core.constants.installer import PackageJson +from reflex_core.utils.decorator import cached_procedure -from reflex.config import Config -from reflex.constants.installer import PackageJson from reflex.reflex import cli from reflex.testing import chdir -from reflex.utils.decorator import cached_procedure from reflex.utils.frontend_skeleton import ( _compile_package_json, _compile_vite_config, diff --git a/tests/units/test_route.py b/tests/units/test_route.py index db095b3c802..feb94d3e5e2 100644 --- a/tests/units/test_route.py +++ b/tests/units/test_route.py @@ -1,7 +1,7 @@ import pytest from pytest_mock import MockerFixture +from reflex_core import constants -from reflex import constants from reflex.app import App from reflex.route import get_route_args, verify_route_validity diff --git a/tests/units/test_sqlalchemy.py b/tests/units/test_sqlalchemy.py index ca8e663cdfa..4f6c14b2dbc 100644 --- a/tests/units/test_sqlalchemy.py +++ b/tests/units/test_sqlalchemy.py @@ -3,12 +3,12 @@ from unittest import mock import pytest +from reflex_core.utils.serializers import serializer import reflex.constants import reflex.model from reflex.model import Model, ModelRegistry, sqla_session from reflex.state import MutableProxy -from reflex.utils.serializers import serializer pytest.importorskip("sqlalchemy") pytest.importorskip("sqlmodel") diff --git a/tests/units/test_state.py b/tests/units/test_state.py index d02940d24bd..341584df409 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -17,18 +17,29 @@ import pytest import pytest_asyncio +import reflex_core.config from plotly.graph_objects import Figure +from pydantic import BaseModel as Base from pytest_mock import MockerFixture +from reflex_core import constants +from reflex_core.constants import CompileVars, RouteVar, SocketEvent +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.event import Event, EventHandler +from reflex_core.utils import format, types +from reflex_core.utils.exceptions import ( + InvalidLockWarningThresholdError, + LockExpiredError, + ReflexRuntimeError, + SetUndefinedStateVarError, + StateSerializationError, + UnretrievableVarValueError, +) +from reflex_core.utils.format import json_dumps +from reflex_core.vars.base import Field, Var, computed_var, field import reflex as rx -import reflex.config -from reflex import constants from reflex.app import App -from reflex.base import Base -from reflex.constants import CompileVars, RouteVar, SocketEvent -from reflex.constants.state import FIELD_MARKER from reflex.environment import environment -from reflex.event import Event, EventHandler from reflex.istate.data import HeaderData, _FrozenDictStrStr from reflex.istate.manager import StateManager from reflex.istate.manager.disk import StateManagerDisk @@ -47,18 +58,8 @@ _substate_key, ) from reflex.testing import chdir -from reflex.utils import format, prerequisites, types -from reflex.utils.exceptions import ( - InvalidLockWarningThresholdError, - LockExpiredError, - ReflexRuntimeError, - SetUndefinedStateVarError, - StateSerializationError, - UnretrievableVarValueError, -) -from reflex.utils.format import json_dumps +from reflex.utils import prerequisites from reflex.utils.token_manager import SocketRecord -from reflex.vars.base import Field, Var, computed_var, field from tests.units.mock_redis import mock_redis from .states import GenState @@ -66,8 +67,7 @@ pytest.importorskip("pydantic") -from pydantic import BaseModel as BaseModelV2 -from pydantic.v1 import BaseModel as BaseModelV1 +from pydantic import BaseModel from tests.units.states.mutation import MutableTestState @@ -1979,7 +1979,7 @@ async def test_state_manager_lock_warning_threshold_contend( substate_token_redis: A token + substate name for looking up in state manager. mocker: Pytest mocker object. """ - console_warn = mocker.patch("reflex.utils.console.warn") + console_warn = mocker.patch("reflex_core.utils.console.warn") state_manager_redis.lock_expiration = LOCK_EXPIRATION state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD @@ -3551,7 +3551,7 @@ def test_redis_state_manager_config_knobs(tmp_path, expiration_kwargs, expected_ with chdir(proj_root): # reload config for each parameter to avoid stale values - reflex.config.get_config(reload=True) + reflex_core.config.get_config(reload=True) from reflex.state import State state_manager = StateManagerRedis(state=State, redis=mock_redis()) @@ -3588,7 +3588,7 @@ def test_redis_state_manager_config_knobs_invalid_lock_warning_threshold( with chdir(proj_root): # reload config for each parameter to avoid stale values - reflex.config.get_config(reload=True) + reflex_core.config.get_config(reload=True) from reflex.state import State with pytest.raises(InvalidLockWarningThresholdError): @@ -3614,7 +3614,7 @@ def test_state_manager_create_respects_explicit_memory_mode_with_redis_url( monkeypatch.setenv("REFLEX_REDIS_URL", "redis://localhost:6379") with chdir(proj_root): - reflex.config.get_config(reload=True) + reflex_core.config.get_config(reload=True) monkeypatch.setattr(prerequisites, "get_redis", mock_redis) from reflex.state import State @@ -3640,7 +3640,7 @@ def test_auto_setters_off(tmp_path): with chdir(proj_root): # reload config for each parameter to avoid stale values - reflex.config.get_config(reload=True) + reflex_core.config.get_config(reload=True) from reflex.state import State class TestState(State): @@ -3861,7 +3861,7 @@ class Child(State): class Obj(Base): """A object containing a callable for testing fallback pickle.""" - _f: Callable + f: Callable def test_fallback_pickle(): @@ -3873,7 +3873,7 @@ class DillState(BaseState): _g: Any = None state = DillState(_reflex_internal_init=True) # pyright: ignore [reportCallIssue] - state._o = Obj(_f=lambda: 42) + state._o = Obj(f=lambda: 42) state._f = lambda: 420 pk = state._serialize() @@ -3883,7 +3883,7 @@ class DillState(BaseState): assert unpickled_state._f is not None assert unpickled_state._f() == 420 assert unpickled_state._o is not None - assert unpickled_state._o._f() == 42 + assert unpickled_state._o.f() == 42 # Threading locks are unpicklable normally, and raise TypeError instead of PicklingError. state2 = DillState(_reflex_internal_init=True) # pyright: ignore [reportCallIssue] @@ -3908,29 +3908,7 @@ class TypedState(rx.State): _ = TypedState(field="str") -class ModelV1(BaseModelV1): - """A pydantic BaseModel v1.""" - - foo: str = "bar" - - def set_foo(self, val: str): - """Set the attribute foo. - - Args: - val: The value to set. - """ - self.foo = val - - def double_foo(self) -> str: - """Concatenate foo with foo. - - Returns: - foo + foo - """ - return self.foo + self.foo - - -class ModelV2(BaseModelV2): +class ModelV2(BaseModel): """A pydantic BaseModel v2.""" foo: str = "bar" @@ -3955,7 +3933,6 @@ def double_foo(self) -> str: class PydanticState(rx.State): """A state with pydantic BaseModel vars.""" - v1: ModelV1 = ModelV1() v2: ModelV2 = ModelV2() dc: ModelDC = ModelDC() @@ -3963,17 +3940,6 @@ class PydanticState(rx.State): def test_mutable_models(): """Test that dataclass and pydantic BaseModel v1 and v2 use dep tracking.""" state = PydanticState() - assert isinstance(state.v1, MutableProxy) - state.v1.foo = "baz" - assert state.dirty_vars == {"v1"} - state.dirty_vars.clear() - state.v1.set_foo("quuc") - assert state.dirty_vars == {"v1"} - state.dirty_vars.clear() - assert state.v1.double_foo() == "quucquuc" - assert state.dirty_vars == set() - state.v1.copy(update={"foo": "larp"}) - assert state.dirty_vars == set() assert isinstance(state.v2, MutableProxy) state.v2.foo = "baz" @@ -4144,10 +4110,6 @@ def rx_base_or_none(self, o: Object | None): # noqa: D102 assert isinstance(o, Object) self.passed = True - def rx_basemodelv1(self, m: ModelV1): # noqa: D102 - assert isinstance(m, ModelV1) - self.passed = True - def rx_basemodelv2(self, m: ModelV2): # noqa: D102 assert isinstance(m, ModelV2) self.passed = True @@ -4197,7 +4159,6 @@ def py_unresolvable(self, u: Unresolvable): # noqa: D102, F821 # pyright: ignor (UpcastState.rx_base, {"o": {"foo": "bar"}}), (UpcastState.rx_base_or_none, {"o": {"foo": "bar"}}), (UpcastState.rx_base_or_none, {"o": None}), - (UpcastState.rx_basemodelv1, {"m": {"foo": "bar"}}), (UpcastState.rx_basemodelv2, {"m": {"foo": "bar"}}), (UpcastState.rx_dataclass, {"dc": {"foo": "bar"}}), (UpcastState.py_set, {"s": ["foo", "foo"]}), diff --git a/tests/units/test_state_tree.py b/tests/units/test_state_tree.py index 7ed19500cc2..e36185de893 100644 --- a/tests/units/test_state_tree.py +++ b/tests/units/test_state_tree.py @@ -4,9 +4,9 @@ import pytest import pytest_asyncio +from reflex_core.constants.state import FIELD_MARKER import reflex as rx -from reflex.constants.state import FIELD_MARKER from reflex.istate.manager.redis import StateManagerRedis from reflex.state import BaseState, StateManager, _substate_key diff --git a/tests/units/test_style.py b/tests/units/test_style.py index 8706d7bc2ca..cbd27eaf0bc 100644 --- a/tests/units/test_style.py +++ b/tests/units/test_style.py @@ -3,14 +3,14 @@ from typing import Any import pytest +from reflex_core.components.component import evaluate_style_namespaces +from reflex_core.style import Style +from reflex_core.utils.exceptions import ReflexError +from reflex_core.vars import VarData +from reflex_core.vars.base import LiteralVar, Var import reflex as rx from reflex import style -from reflex.components.component import evaluate_style_namespaces -from reflex.style import Style -from reflex.utils.exceptions import ReflexError -from reflex.vars import VarData -from reflex.vars.base import LiteralVar, Var style_var = rx.Var.create({"height": "42px"}) diff --git a/tests/units/test_testing.py b/tests/units/test_testing.py index 36c8f173b01..0815326648c 100644 --- a/tests/units/test_testing.py +++ b/tests/units/test_testing.py @@ -5,13 +5,13 @@ from unittest import mock import pytest +import reflex_core.config +from reflex_core.components.component import CUSTOM_COMPONENTS +from reflex_core.constants import IS_WINDOWS -import reflex.config import reflex.reflex as reflex_cli import reflex.testing as reflex_testing import reflex.utils.prerequisites -from reflex.components.component import CUSTOM_COMPONENTS -from reflex.constants import IS_WINDOWS from reflex.experimental.memo import EXPERIMENTAL_MEMOS from reflex.testing import AppHarness @@ -69,8 +69,10 @@ def harness_mocks(monkeypatch): ) ) - monkeypatch.setattr(reflex_testing, "get_config", lambda: fake_config) - monkeypatch.setattr(reflex.config, "get_config", lambda reload=False: fake_config) + monkeypatch.setattr(reflex_testing, "get_config", lambda reload=False: fake_config) + monkeypatch.setattr( + reflex_core.config, "get_config", lambda reload=False: fake_config + ) monkeypatch.setattr( reflex.utils.prerequisites, "get_and_validate_app", diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 4cc9c7a159d..09cadb85c81 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -7,22 +7,18 @@ import pytest from pandas import DataFrame +from pydantic import BaseModel as Base from pytest_mock import MockerFixture - -import reflex as rx -from reflex.base import Base -from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG -from reflex.constants.state import FIELD_MARKER -from reflex.environment import PerformanceMode -from reflex.state import BaseState -from reflex.utils.exceptions import ( +from reflex_core.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.utils.exceptions import ( PrimitiveUnserializableToJSONError, UntypedComputedVarError, ) -from reflex.utils.imports import ImportVar -from reflex.utils.types import get_default_value_for_type -from reflex.vars import VarData -from reflex.vars.base import ( +from reflex_core.utils.imports import ImportVar +from reflex_core.utils.types import get_default_value_for_type +from reflex_core.vars import VarData +from reflex_core.vars.base import ( ComputedVar, LiteralVar, Var, @@ -30,20 +26,24 @@ var_operation, var_operation_return, ) -from reflex.vars.function import ( +from reflex_core.vars.function import ( ArgsFunctionOperation, DestructuredArg, FunctionStringVar, ) -from reflex.vars.number import LiteralBooleanVar, LiteralNumberVar, NumberVar -from reflex.vars.object import LiteralObjectVar, ObjectVar -from reflex.vars.sequence import ( +from reflex_core.vars.number import LiteralBooleanVar, LiteralNumberVar, NumberVar +from reflex_core.vars.object import LiteralObjectVar, ObjectVar +from reflex_core.vars.sequence import ( ArrayVar, ConcatVarOperation, LiteralArrayVar, LiteralStringVar, ) +import reflex as rx +from reflex.environment import PerformanceMode +from reflex.state import BaseState + test_vars = [ Var(_js_expr="prop1", _var_type=int), Var(_js_expr="key", _var_type=str), @@ -392,14 +392,14 @@ def test_list_tuple_contains(var, expected): assert str(var.contains(other_var)) == f"{expected}.includes(other)" -class Foo(rx.Base): +class Foo(Base): """Foo class.""" bar: int baz: str -class Bar(rx.Base): +class Bar(Base): """Bar class.""" bar: str @@ -1177,7 +1177,7 @@ class Foo(Base): baz: int parent_obj = LiteralObjectVar.create( - Foo(bar=Boo(foo="bar", bar=5), baz=5).dict(), Foo + Foo(bar=Boo(foo="bar", bar=5), baz=5).model_dump(), Foo ) assert ( @@ -1910,7 +1910,7 @@ class StateWithVar(rx.State): field: int = 1 mocker.patch( - "reflex.components.base.bare.get_performance_mode", + "reflex_components_core.base.bare.get_performance_mode", return_value=PerformanceMode.RAISE, ) @@ -1920,7 +1920,7 @@ class StateWithVar(rx.State): ) mocker.patch( - "reflex.components.base.bare.get_performance_mode", + "reflex_components_core.base.bare.get_performance_mode", return_value=PerformanceMode.OFF, ) diff --git a/tests/units/utils/test_format.py b/tests/units/utils/test_format.py index 48db442223c..38cebccad72 100644 --- a/tests/units/utils/test_format.py +++ b/tests/units/utils/test_format.py @@ -6,22 +6,21 @@ import plotly.graph_objects as go import pytest - -from reflex.components.tags.tag import Tag -from reflex.constants.state import FIELD_MARKER -from reflex.event import ( +from reflex_core.components.tags.tag import Tag +from reflex_core.constants.state import FIELD_MARKER +from reflex_core.event import ( EventChain, EventHandler, EventSpec, JavascriptInputEvent, no_args_event_spec, ) -from reflex.style import Style -from reflex.utils import format -from reflex.utils.serializers import serialize_figure -from reflex.vars.base import LiteralVar, Var -from reflex.vars.function import FunctionStringVar -from reflex.vars.object import ObjectVar +from reflex_core.style import Style +from reflex_core.utils import format +from reflex_core.utils.serializers import serialize_figure +from reflex_core.vars.base import LiteralVar, Var +from reflex_core.vars.function import FunctionStringVar +from reflex_core.vars.object import ObjectVar pytest.importorskip("pydantic") diff --git a/tests/units/utils/test_imports.py b/tests/units/utils/test_imports.py index 4166a62e8f8..a232f489ef8 100644 --- a/tests/units/utils/test_imports.py +++ b/tests/units/utils/test_imports.py @@ -1,6 +1,5 @@ import pytest - -from reflex.utils.imports import ( +from reflex_core.utils.imports import ( ImportDict, ImportVar, ParsedImportDict, diff --git a/tests/units/utils/test_serializers.py b/tests/units/utils/test_serializers.py index 6c085c4ceef..fb7418b8379 100644 --- a/tests/units/utils/test_serializers.py +++ b/tests/units/utils/test_serializers.py @@ -6,12 +6,12 @@ from typing import Any import pytest +from pydantic import BaseModel as Base +from reflex_components_core.core.colors import Color +from reflex_core.utils.format import json_dumps +from reflex_core.vars.base import LiteralVar -from reflex.base import Base -from reflex.components.core.colors import Color from reflex.utils import serializers -from reflex.utils.format import json_dumps -from reflex.vars.base import LiteralVar pytest.importorskip("pydantic") diff --git a/tests/units/utils/test_token_manager.py b/tests/units/utils/test_token_manager.py index 9f740a29f37..2f01cc9b1f6 100644 --- a/tests/units/utils/test_token_manager.py +++ b/tests/units/utils/test_token_manager.py @@ -265,7 +265,7 @@ def manager(self, mock_redis): Returns: RedisTokenManager instance for testing. """ - with patch("reflex.config.get_config") as mock_get_config: + with patch("reflex_core.config.get_config") as mock_get_config: mock_config = Mock() mock_config.redis_token_expiration = 3600 mock_get_config.return_value = mock_config diff --git a/tests/units/utils/test_types.py b/tests/units/utils/test_types.py index bf1a77c967b..c1f0841664d 100644 --- a/tests/units/utils/test_types.py +++ b/tests/units/utils/test_types.py @@ -1,9 +1,8 @@ from typing import Any, Literal, TypedDict import pytest - -from reflex.utils import types -from reflex.vars.base import Var +from reflex_core.utils import types +from reflex_core.vars.base import Var @pytest.mark.parametrize( @@ -45,7 +44,7 @@ def test_validate_literal_error_msg(params, allowed_value_str, value_str): def test_issubclass( cls: types.GenericType, cls_check: types.GenericType, expected: bool ) -> None: - assert types._issubclass(cls, cls_check) == expected + assert types.typehint_issubclass(cls, cls_check) == expected class CustomDict(dict[str, str]): diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index c1dfc37190a..81294299ce3 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -8,15 +8,15 @@ import pytest from packaging import version from pytest_mock import MockerFixture +from reflex_core import constants +from reflex_core.event import EventHandler +from reflex_core.utils.exceptions import ReflexError, SystemPackageMissingError +from reflex_core.vars.base import Var -from reflex import constants from reflex.environment import environment -from reflex.event import EventHandler from reflex.state import BaseState from reflex.utils import exec as utils_exec from reflex.utils import frontend_skeleton, js_runtimes, prerequisites, templates, types -from reflex.utils.exceptions import ReflexError, SystemPackageMissingError -from reflex.vars.base import Var class ExampleTestState(BaseState): @@ -283,8 +283,8 @@ def test_is_backend_base_variable( (float, int | float, True), (str, int | float, False), (list[int], list[int], True), - (list[int], list[float], True), - (int | float, int | float, False), + (list[int], list[float], False), + (int | float, int | float, True), (int | Var[int], Var[int], False), (int, Any, True), (Any, Any, True), @@ -296,7 +296,7 @@ def test_is_backend_base_variable( ], ) def test_issubclass(cls: type, cls_check: type, expected: bool): - assert types._issubclass(cls, cls_check) == expected + assert types.typehint_issubclass(cls, cls_check) == expected @pytest.mark.parametrize("cls", [Literal["test", 1], Literal[1, "test"]]) @@ -514,7 +514,7 @@ def test_output_system_info(mocker: MockerFixture): This test makes no assertions about the output, other than it executes without crashing. """ - mocker.patch("reflex.utils.console._LOG_LEVEL", constants.LogLevel.DEBUG) + mocker.patch("reflex_core.utils.console._LOG_LEVEL", constants.LogLevel.DEBUG) utils_exec.output_system_info() diff --git a/tests/units/vars/test_base.py b/tests/units/vars/test_base.py index 54b3d48182f..e7a6b07488e 100644 --- a/tests/units/vars/test_base.py +++ b/tests/units/vars/test_base.py @@ -1,9 +1,9 @@ from collections.abc import Mapping, Sequence import pytest +from reflex_core.vars.base import computed_var, figure_out_type from reflex.state import State -from reflex.vars.base import computed_var, figure_out_type class CustomDict(dict[str, str]): diff --git a/tests/units/vars/test_dep_tracking.py b/tests/units/vars/test_dep_tracking.py index 1acd5f864bc..87dc848d37b 100644 --- a/tests/units/vars/test_dep_tracking.py +++ b/tests/units/vars/test_dep_tracking.py @@ -5,17 +5,17 @@ import sys import pytest - -import reflex as rx -import tests.units.states.upload as tus_upload -from reflex.state import State -from reflex.utils.exceptions import VarValueError -from reflex.vars.dep_tracking import ( +from reflex_core.utils.exceptions import VarValueError +from reflex_core.vars.dep_tracking import ( DependencyTracker, UntrackedLocalVarError, get_cell_value, ) +import reflex as rx +import tests.units.states.upload as tus_upload +from reflex.state import State + class DependencyTestState(State): """Test state for dependency tracking tests.""" diff --git a/tests/units/vars/test_object.py b/tests/units/vars/test_object.py index 817c6e786ba..6867711e6fe 100644 --- a/tests/units/vars/test_object.py +++ b/tests/units/vars/test_object.py @@ -1,14 +1,15 @@ import dataclasses from collections.abc import Sequence +import pydantic import pytest +from reflex_core.utils.types import GenericType +from reflex_core.vars.base import Var +from reflex_core.vars.object import LiteralObjectVar, ObjectVar +from reflex_core.vars.sequence import ArrayVar from typing_extensions import assert_type import reflex as rx -from reflex.utils.types import GenericType -from reflex.vars.base import Var -from reflex.vars.object import LiteralObjectVar, ObjectVar -from reflex.vars.sequence import ArrayVar pytest.importorskip("sqlalchemy") pytest.importorskip("pydantic") @@ -35,8 +36,8 @@ def serialize_bare(obj: Bare) -> dict: return {"quantity": obj.quantity} -class Base(rx.Base): - """A reflex base class with a single attribute.""" +class Base(pydantic.BaseModel): + """A pydantic BaseModel class with a single attribute.""" quantity: int = 0 diff --git a/uv.lock b/uv.lock index 154e46b320c..e7df6683183 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,21 @@ resolution-markers = [ [manifest] members = [ + "hatch-reflex-pyi", "reflex", + "reflex-components-code", + "reflex-components-core", + "reflex-components-dataeditor", + "reflex-components-gridjs", + "reflex-components-lucide", + "reflex-components-markdown", + "reflex-components-moment", + "reflex-components-plotly", + "reflex-components-radix", + "reflex-components-react-player", + "reflex-components-recharts", + "reflex-components-sonner", + "reflex-core", "reflex-docgen", ] @@ -689,6 +703,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hatch-reflex-pyi" +source = { editable = "packages/hatch-reflex-pyi" } +dependencies = [ + { name = "hatchling" }, +] + +[package.metadata] +requires-dist = [{ name = "hatchling" }] + [[package]] name = "hatchling" version = "1.29.0" @@ -903,6 +927,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mistletoe" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/ae/d33647e2a26a8899224f36afc5e7b7a670af30f1fd87231e9f07ca19d673/mistletoe-1.5.1.tar.gz", hash = "sha256:c5571ce6ca9cfdc7ce9151c3ae79acb418e067812000907616427197648030a3", size = 111769, upload-time = "2025-12-07T16:19:01.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/60/0980fefdc4d12c18c1bbab9d62852f27aded8839233c7b0a9827aaf395f5/mistletoe-1.5.1-py3-none-any.whl", hash = "sha256:d3e97664798261503f685f6a6281b092628367cf3128fc68a015a993b0c4feb3", size = 55331, upload-time = "2025-12-07T16:18:59.65Z" }, +] + [[package]] name = "narwhals" version = "2.18.0" @@ -2018,7 +2051,6 @@ wheels = [ [[package]] name = "reflex" -version = "0.9.0.dev1" source = { editable = "." } dependencies = [ { name = "alembic" }, @@ -2026,12 +2058,24 @@ dependencies = [ { name = "granian", extra = ["reload"] }, { name = "httpx" }, { name = "packaging" }, - { name = "platformdirs" }, { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "pydantic" }, { name = "python-multipart" }, { name = "python-socketio" }, { name = "redis" }, + { name = "reflex-components-code" }, + { name = "reflex-components-core" }, + { name = "reflex-components-dataeditor" }, + { name = "reflex-components-gridjs" }, + { name = "reflex-components-lucide" }, + { name = "reflex-components-markdown" }, + { name = "reflex-components-moment" }, + { name = "reflex-components-plotly" }, + { name = "reflex-components-radix" }, + { name = "reflex-components-react-player" }, + { name = "reflex-components-recharts" }, + { name = "reflex-components-sonner" }, + { name = "reflex-core" }, { name = "reflex-hosting-cli" }, { name = "rich" }, { name = "sqlmodel" }, @@ -2092,13 +2136,25 @@ requires-dist = [ { name = "granian", extras = ["reload"], specifier = ">=2.5.5" }, { name = "httpx", specifier = ">=0.23.3,<1.0" }, { name = "packaging", specifier = ">=24.2,<27" }, - { name = "platformdirs", specifier = ">=4.3.7,<5.0" }, { name = "psutil", marker = "sys_platform == 'win32'", specifier = ">=7.0.0,<8.0" }, - { name = "pydantic", specifier = ">=1.10.21,<3.0" }, - { name = "pydantic", marker = "extra == 'db'", specifier = ">=1.10.21,<3.0" }, + { name = "pydantic", specifier = ">=2.12.0,<3.0" }, + { name = "pydantic", marker = "extra == 'db'", specifier = ">=2.12.0,<3.0" }, { name = "python-multipart", specifier = ">=0.0.20,<1.0" }, { name = "python-socketio", specifier = ">=5.12.0,<6.0" }, { name = "redis", specifier = ">=5.2.1,<8.0" }, + { name = "reflex-components-code", editable = "packages/reflex-components-code" }, + { name = "reflex-components-core", editable = "packages/reflex-components-core" }, + { name = "reflex-components-dataeditor", editable = "packages/reflex-components-dataeditor" }, + { name = "reflex-components-gridjs", editable = "packages/reflex-components-gridjs" }, + { name = "reflex-components-lucide", editable = "packages/reflex-components-lucide" }, + { name = "reflex-components-markdown", editable = "packages/reflex-components-markdown" }, + { name = "reflex-components-moment", editable = "packages/reflex-components-moment" }, + { name = "reflex-components-plotly", editable = "packages/reflex-components-plotly" }, + { name = "reflex-components-radix", editable = "packages/reflex-components-radix" }, + { name = "reflex-components-react-player", editable = "packages/reflex-components-react-player" }, + { name = "reflex-components-recharts", editable = "packages/reflex-components-recharts" }, + { name = "reflex-components-sonner", editable = "packages/reflex-components-sonner" }, + { name = "reflex-core", editable = "packages/reflex-core" }, { name = "reflex-hosting-cli", specifier = ">=0.1.61" }, { name = "rich", specifier = ">=13,<15" }, { name = "sqlmodel", specifier = ">=0.0.27,<0.1" }, @@ -2144,12 +2200,155 @@ dev = [ { name = "uvicorn" }, ] +[[package]] +name = "reflex-components-code" +source = { editable = "packages/reflex-components-code" } +dependencies = [ + { name = "reflex-components-core" }, + { name = "reflex-components-lucide" }, + { name = "reflex-components-radix" }, +] + +[package.metadata] +requires-dist = [ + { name = "reflex-components-core", editable = "packages/reflex-components-core" }, + { name = "reflex-components-lucide", editable = "packages/reflex-components-lucide" }, + { name = "reflex-components-radix", editable = "packages/reflex-components-radix" }, +] + +[[package]] +name = "reflex-components-core" +source = { editable = "packages/reflex-components-core" } +dependencies = [ + { name = "python-multipart" }, + { name = "reflex-components-lucide" }, + { name = "reflex-components-sonner" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "python-multipart" }, + { name = "reflex-components-lucide", editable = "packages/reflex-components-lucide" }, + { name = "reflex-components-sonner", editable = "packages/reflex-components-sonner" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] + +[[package]] +name = "reflex-components-dataeditor" +source = { editable = "packages/reflex-components-dataeditor" } +dependencies = [ + { name = "reflex-components-core" }, +] + +[package.metadata] +requires-dist = [{ name = "reflex-components-core", editable = "packages/reflex-components-core" }] + +[[package]] +name = "reflex-components-gridjs" +source = { editable = "packages/reflex-components-gridjs" } + +[[package]] +name = "reflex-components-lucide" +source = { editable = "packages/reflex-components-lucide" } + +[[package]] +name = "reflex-components-markdown" +source = { editable = "packages/reflex-components-markdown" } +dependencies = [ + { name = "reflex-components-code" }, + { name = "reflex-components-core" }, + { name = "reflex-components-radix" }, +] + +[package.metadata] +requires-dist = [ + { name = "reflex-components-code", editable = "packages/reflex-components-code" }, + { name = "reflex-components-core", editable = "packages/reflex-components-core" }, + { name = "reflex-components-radix", editable = "packages/reflex-components-radix" }, +] + +[[package]] +name = "reflex-components-moment" +source = { editable = "packages/reflex-components-moment" } + +[[package]] +name = "reflex-components-plotly" +source = { editable = "packages/reflex-components-plotly" } +dependencies = [ + { name = "reflex-components-core" }, +] + +[package.metadata] +requires-dist = [{ name = "reflex-components-core", editable = "packages/reflex-components-core" }] + +[[package]] +name = "reflex-components-radix" +source = { editable = "packages/reflex-components-radix" } +dependencies = [ + { name = "reflex-components-core" }, + { name = "reflex-components-lucide" }, +] + +[package.metadata] +requires-dist = [ + { name = "reflex-components-core", editable = "packages/reflex-components-core" }, + { name = "reflex-components-lucide", editable = "packages/reflex-components-lucide" }, +] + +[[package]] +name = "reflex-components-react-player" +source = { editable = "packages/reflex-components-react-player" } +dependencies = [ + { name = "reflex-components-core" }, +] + +[package.metadata] +requires-dist = [{ name = "reflex-components-core", editable = "packages/reflex-components-core" }] + +[[package]] +name = "reflex-components-recharts" +source = { editable = "packages/reflex-components-recharts" } + +[[package]] +name = "reflex-components-sonner" +source = { editable = "packages/reflex-components-sonner" } +dependencies = [ + { name = "reflex-components-lucide" }, +] + +[package.metadata] +requires-dist = [{ name = "reflex-components-lucide", editable = "packages/reflex-components-lucide" }] + +[[package]] +name = "reflex-core" +source = { editable = "packages/reflex-core" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pydantic" }, + { name = "rich" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "packaging", specifier = ">=24.2,<27" }, + { name = "platformdirs", specifier = ">=4.3.7,<5.0" }, + { name = "pydantic", specifier = ">=1.10.21,<3.0" }, + { name = "rich", specifier = ">=13,<15" }, + { name = "typing-extensions", specifier = ">=4.13.0" }, +] + [[package]] name = "reflex-docgen" -version = "0.0.1" source = { editable = "packages/reflex-docgen" } dependencies = [ { name = "griffelib" }, + { name = "mistletoe" }, + { name = "pyyaml" }, { name = "reflex" }, { name = "typing-extensions" }, { name = "typing-inspection" }, @@ -2158,6 +2357,8 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "griffelib", specifier = ">=2.0.1" }, + { name = "mistletoe", specifier = ">=1.4.0" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "reflex", editable = "." }, { name = "typing-extensions" }, { name = "typing-inspection", specifier = ">=0.4.2" },