diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 6e44c0c26..000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,391 +0,0 @@ -# ReactPy Development Instructions - -ReactPy is a Python library for building user interfaces without JavaScript. It creates React-like components that render to web pages using a Python-to-JavaScript bridge. - -Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. - -**IMPORTANT**: This package uses modern Python tooling with Hatch for all development workflows. Always use Hatch commands for development tasks. - -**BUG INVESTIGATION**: When investigating whether a bug was already resolved in a previous version, always prioritize searching through `docs/source/about/changelog.rst` first before using Git history. Only search through Git history when no relevant changelog entries are found. - -## Working Effectively - -### Bootstrap, Build, and Test the Repository - -**Prerequisites:** - -- Install Python 3.9+ from https://www.python.org/downloads/ -- Install Hatch: `pip install hatch` -- Install Bun JavaScript runtime: `curl -fsSL https://bun.sh/install | bash && source ~/.bashrc` -- Install Git - -**Initial Setup:** - -```bash -git clone https://github.com/reactive-python/reactpy.git -cd reactpy -``` - -**Install Dependencies for Development:** - -```bash -# Install core ReactPy dependencies -pip install fastjsonschema requests lxml anyio typing-extensions - -# Install ASGI dependencies for server functionality -pip install orjson asgiref asgi-tools servestatic uvicorn fastapi - -# Optional: Install additional servers -pip install flask sanic tornado -``` - -**Build JavaScript Packages:** - -- `hatch run javascript:build` -- takes 15 seconds. NEVER CANCEL. Set timeout to 60+ minutes for safety. -- This builds two packages: @reactpy/client, and @reactpy/app - -**Build Python Package:** - -- `hatch build --clean` -- takes 10 seconds. NEVER CANCEL. Set timeout to 60+ minutes for safety. - -**Run Python Tests:** - -- `hatch test --parallel` -- takes 10-30 seconds for basic tests. NEVER CANCEL. Set timeout to 2 minutes for full test suite. **All tests must always pass - failures are never expected or allowed.** -- `hatch test --parallel --cover` -- run tests with coverage reporting (used in CI) -- `hatch test --parallel -k test_name` -- run specific tests -- `hatch test --parallel tests/test_config.py` -- run specific test files - -**Run Python Linting and Formatting:** - -- `hatch fmt` -- Run all linters and formatters (~1 second) -- `hatch fmt --check` -- Check formatting without making changes (~1 second) -- `hatch fmt --linter` -- Run only linters -- `hatch fmt --formatter` -- Run only formatters -- `hatch run python:type_check` -- Run Python type checker (~10 seconds) - -**Run JavaScript Tasks:** - -- `hatch run javascript:check` -- Lint and type-check JavaScript (10 seconds). NEVER CANCEL. Set timeout to 30+ minutes. -- `hatch run javascript:fix` -- Format JavaScript code -- `hatch run javascript:test` -- Run JavaScript tests - -**Interactive Development Shell:** - -- `hatch shell` -- Enter an interactive shell environment with all dependencies installed -- `hatch shell default` -- Enter the default development environment -- Use the shell for interactive debugging and development tasks - -## Validation - -Always manually validate any new code changes through these steps: - -**Basic Functionality Test:** - -```python -# Add src to path if not installed -import sys, os -sys.path.insert(0, os.path.join("/path/to/reactpy", "src")) - -# Test that imports and basic components work -import reactpy -from reactpy import component, html, use_state - -@component -def test_component(): - return html.div([ - html.h1("Test"), - html.p("ReactPy is working") - ]) - -# Verify component renders -vdom = test_component() -print(f"Component rendered: {type(vdom)}") -``` - -**Server Functionality Test:** - -```python -# Test ASGI server creation (most common deployment) -from reactpy import component, html -from reactpy.executors.asgi.standalone import ReactPy -import uvicorn - -@component -def hello_world(): - return html.div([ - html.h1("Hello, ReactPy!"), - html.p("Server is working!") - ]) - -# Create ASGI app (don't run to avoid hanging) -app = ReactPy(hello_world) -print("✓ ASGI server created successfully") - -# To actually run: uvicorn.run(app, host="127.0.0.1", port=8000) -``` - -**Hooks and State Test:** - -```python -from reactpy import component, html, use_state - -@component -def counter_component(initial=0): - count, set_count = use_state(initial) - - return html.div([ - html.h1(f"Count: {count}"), - html.button({ - "onClick": lambda event: set_count(count + 1) - }, "Increment") - ]) - -# Test component with hooks -counter = counter_component(5) -print(f"✓ Hook-based component: {type(counter)}") -``` - -**Always run these validation steps before completing work:** - -- `hatch fmt --check` -- Ensure code is properly formatted (never expected to fail) -- `hatch run python:type_check` -- Ensure no type errors (never expected to fail) -- `hatch run javascript:check` -- Ensure JavaScript passes linting (never expected to fail) -- Test basic component creation and rendering as shown above -- Test server creation if working on server-related features -- Run relevant tests with `hatch test --parallel` -- **All tests must always pass - failures are never expected or allowed** - -**Integration Testing:** - -- ReactPy can be deployed with FastAPI, Flask, Sanic, Tornado via ASGI -- For browser testing, Playwright is used but requires additional setup -- Test component VDOM rendering directly when browser testing isn't available -- Validate that JavaScript builds are included in Python package after changes - -## Repository Structure and Navigation - -### Key Directories: - -- `src/reactpy/` -- Main Python package source code - - `core/` -- Core ReactPy functionality (components, hooks, VDOM) - - `web/` -- Web module management and exports - - `executors/` -- Server integration modules (ASGI, etc.) - - `testing/` -- Testing utilities and fixtures - - `pyscript/` -- PyScript integration - - `static/` -- Bundled JavaScript files - - `_html.py` -- HTML element factory functions -- `src/js/` -- JavaScript packages that get bundled with Python - - `packages/@reactpy/client/` -- Client-side React integration - - `packages/@reactpy/app/` -- Application framework -- `src/build_scripts/` -- Build automation scripts -- `tests/` -- Python test suite with comprehensive coverage -- `docs/` -- Documentation source (MkDocs-based, transitioning setup) - -### Important Files: - -- `pyproject.toml` -- Python project configuration and Hatch environments -- `src/js/package.json` -- JavaScript development dependencies -- `tests/conftest.py` -- Test configuration and fixtures -- `docs/source/about/changelog.rst` -- Version history and changes -- `.github/workflows/check.yml` -- CI/CD pipeline configuration - -## Common Tasks - -### Build Time Expectations: - -- JavaScript build: 15 seconds -- Python package build: 10 seconds -- Python linting: 1 second -- JavaScript linting: 10 seconds -- Type checking: 10 seconds -- Full CI pipeline: 5-10 minutes - -### Running ReactPy Applications: - -**ASGI Standalone (Recommended):** - -```python -from reactpy import component, html -from reactpy.executors.asgi.standalone import ReactPy -import uvicorn - -@component -def my_app(): - return html.h1("Hello World") - -app = ReactPy(my_app) -uvicorn.run(app, host="127.0.0.1", port=8000) -``` - -**With FastAPI:** - -```python -from fastapi import FastAPI -from reactpy import component, html -from reactpy.executors.asgi.middleware import ReactPyMiddleware - -@component -def my_component(): - return html.h1("Hello from ReactPy!") - -app = FastAPI() -app.add_middleware(ReactPyMiddleware, component=my_component) -``` - -### Creating Components: - -```python -from reactpy import component, html, use_state - -@component -def my_component(initial_value=0): - count, set_count = use_state(initial_value) - - return html.div([ - html.h1(f"Count: {count}"), - html.button({ - "onClick": lambda event: set_count(count + 1) - }, "Increment") - ]) -``` - -### Working with JavaScript: - -- JavaScript packages are in `src/js/packages/` -- Two main packages: @reactpy/client, @reactpy/app -- Built JavaScript gets bundled into `src/reactpy/static/` -- Always rebuild JavaScript after changes: `hatch run javascript:build` - -## Common Hatch Commands - -The following are key commands for daily development: - -### Development Commands - -```bash -hatch test --parallel # Run all tests (**All tests must always pass**) -hatch test --parallel --cover # Run tests with coverage (used in CI) -hatch test --parallel -k test_name # Run specific tests -hatch fmt # Format code with all formatters -hatch fmt --check # Check formatting without changes -hatch run python:type_check # Run Python type checker -hatch run javascript:build # Build JavaScript packages (15 seconds) -hatch run javascript:check # Lint JavaScript code (10 seconds) -hatch run javascript:fix # Format JavaScript code -hatch build --clean # Build Python package (10 seconds) -``` - -### Environment Management - -```bash -hatch env show # Show all environments -hatch shell # Enter default shell -hatch shell default # Enter development shell -``` - -### Build Timing Expectations - -- **NEVER CANCEL**: All commands complete within 60 seconds in normal operation -- **JavaScript build**: 15 seconds (hatch run javascript:build) -- **Python package build**: 10 seconds (hatch build --clean) -- **Python linting**: 1 second (hatch fmt) -- **JavaScript linting**: 10 seconds (hatch run javascript:check) -- **Type checking**: 10 seconds (hatch run python:type_check) -- **Unit tests**: 10-30 seconds (varies by test selection) -- **Full CI pipeline**: 5-10 minutes - -## Development Workflow - -Follow this step-by-step process for effective development: - -1. **Bootstrap environment**: Ensure you have Python 3.9+ and run `pip install hatch` -2. **Make your changes** to the codebase -3. **Run formatting**: `hatch fmt` to format code (~1 second) -4. **Run type checking**: `hatch run python:type_check` for type checking (~10 seconds) -5. **Run JavaScript linting** (if JavaScript was modified): `hatch run javascript:check` (~10 seconds) -6. **Run relevant tests**: `hatch test --parallel` with specific test selection if needed. **All tests must always pass - failures are never expected or allowed.** -7. **Validate component functionality** manually using validation tests above -8. **Build JavaScript** (if modified): `hatch run javascript:build` (~15 seconds) -9. **Update documentation** when making changes to Python source code (required) -10. **Add changelog entry** for all significant changes to `docs/source/about/changelog.rst` - -**IMPORTANT**: Documentation must be updated whenever changes are made to Python source code. This is enforced as part of the development workflow. - -**IMPORTANT**: Significant changes must always include a changelog entry in `docs/source/about/changelog.rst` under the appropriate version section. - -## Troubleshooting - -### Build Issues: - -- If JavaScript build fails, try: `hatch run "src/build_scripts/clean_js_dir.py"` then rebuild -- If Python build fails, ensure all dependencies in pyproject.toml are available -- Network timeouts during pip install are common in CI environments -- Missing dependencies error: Install ASGI dependencies with `pip install orjson asgiref asgi-tools servestatic` - -### Import Issues: - -- ReactPy must be installed or src/ must be in Python path -- Main imports: `from reactpy import component, html, use_state` -- Server imports: `from reactpy.executors.asgi.standalone import ReactPy` -- Web functionality: `from reactpy.web import export, module_from_url` - -### Server Issues: - -- Missing ASGI dependencies: Install with `pip install orjson asgiref asgi-tools servestatic uvicorn` -- For FastAPI integration: `pip install fastapi uvicorn` -- For Flask integration: `pip install flask` (requires additional backend package) -- For development servers, use ReactPy ASGI standalone for simplest setup - -## Package Dependencies - -Modern dependency management via pyproject.toml: - -**Core Runtime Dependencies:** - -- `fastjsonschema >=2.14.5` -- JSON schema validation -- `requests >=2` -- HTTP client library -- `lxml >=4` -- XML/HTML processing -- `anyio >=3` -- Async I/O abstraction -- `typing-extensions >=3.10` -- Type hints backport - -**Optional Dependencies (install via extras):** - -- `asgi` -- ASGI server support: `orjson`, `asgiref`, `asgi-tools`, `servestatic`, `pip` -- `jinja` -- Template integration: `jinja2-simple-tags`, `jinja2 >=3` -- `uvicorn` -- ASGI server: `uvicorn[standard]` -- `testing` -- Browser automation: `playwright` -- `all` -- All optional dependencies combined - -**Development Dependencies (managed by Hatch):** - -- **JavaScript tooling**: Bun runtime for building packages -- **Python tooling**: Hatch environments handle all dev dependencies automatically - -## CI/CD Information - -The repository uses GitHub Actions with these key jobs: - -- `test-python-coverage` -- Python test coverage with `hatch test --parallel --cover` -- `lint-python` -- Python linting and type checking via `hatch fmt --check` and `hatch run python:type_check` -- `test-python` -- Cross-platform Python testing across Python 3.10-3.13 and Ubuntu/macOS/Windows -- `lint-javascript` -- JavaScript linting and type checking - -The CI workflow is defined in `.github/workflows/check.yml` and uses the reusable workflow in `.github/workflows/.hatch-run.yml`. - -**Build Matrix:** - -- **Python versions**: 3.10, 3.11, 3.12, 3.13 -- **Operating systems**: Ubuntu, macOS, Windows -- **Test execution**: Hatch-managed environments ensure consistency across platforms - -Always ensure your changes pass local validation before pushing, as the CI pipeline will run the same checks. - -## Important Notes - -- **This is a Python-to-JavaScript bridge library**, not a traditional web framework - it enables React-like components in Python -- **Component rendering uses VDOM** - components return virtual DOM objects that get serialized to JavaScript -- **All builds and tests run quickly** - if something takes more than 60 seconds, investigate the issue -- **Hatch environments provide full isolation** - no need to manage virtual environments manually -- **JavaScript packages are bundled into Python** - the build process combines JS and Python into a single distribution -- **Documentation updates are required** when making changes to Python source code -- **Always update this file** when making changes to the development workflow, build process, or repository structure -- **All tests must always pass** - failures are never expected or allowed in a healthy development environment diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 022e6ea84..d8e16d03e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -29,15 +29,28 @@ jobs: run-cmd: "hatch test --parallel --retries 10" runs-on: '["ubuntu-latest", "macos-latest", "windows-latest"]' python-version: '["3.11", "3.12", "3.13", "3.14"]' - test-documentation: - # Temporarily disabled while we transition from Sphinx to MkDocs - # https://github.com/reactive-python/reactpy/pull/1052 - if: 0 - uses: ./.github/workflows/.hatch-run.yml - with: - job-name: "python-{0}" - run-cmd: "hatch run docs:check" - python-version: '["3.11"]' + test-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install Python Dependencies + run: pip install --upgrade pip hatch uv + # Removed since linkcheck does not support disabling specific domains, + # and a handful of the domains have rate-limit issues. + # - name: Check documentation links + # run: hatch run docs:linkcheck + - name: Check docs build + run: hatch run docs:build + - name: Check docs examples + run: hatch run docs:check test-javascript: uses: ./.github/workflows/.hatch-run.yml with: @@ -48,3 +61,4 @@ jobs: with: job-name: "{1}" run-cmd: "hatch run javascript:check" + diff --git a/.github/workflows/publish-docs-dev.yml b/.github/workflows/publish-docs-dev.yml new file mode 100644 index 000000000..f93a3f9c0 --- /dev/null +++ b/.github/workflows/publish-docs-dev.yml @@ -0,0 +1,28 @@ +name: Publish Develop Docs +on: + push: + branches: + - main +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install dependencies + run: pip install --upgrade pip hatch uv + - name: Configure Git + run: | + git config user.name github-actions + git config user.email github-actions@github.com + - name: Publish Develop Docs + run: hatch run docs:deploy_develop + concurrency: + group: publish-docs diff --git a/.github/workflows/publish-docs-release.yml b/.github/workflows/publish-docs-release.yml new file mode 100644 index 000000000..7e41c3478 --- /dev/null +++ b/.github/workflows/publish-docs-release.yml @@ -0,0 +1,29 @@ +name: Publish Release +on: + release: + types: [published] + +jobs: + docs: + if: startsWith(github.event.release.name, 'reactpy ') || startsWith(github.event.release.tag_name, 'reactpy-') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install dependencies + run: pip install --upgrade pip hatch uv + - name: Configure Git + run: | + git config user.name github-actions + git config user.email github-actions@github.com + - name: Publish ${{ github.event.release.name }} Docs + run: hatch run docs:deploy_latest ${{ github.ref_name }} + concurrency: + group: publish-docs diff --git a/.gitignore b/.gitignore index 3e4fd10f4..0e63bb616 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# --- DOCS --- + +docs/site + # --- Build Artifacts --- src/reactpy/static/*.js* src/reactpy/static/morphdom/ diff --git a/AGENTS_EXTERNAL.md b/AGENTS_EXTERNAL.md new file mode 100644 index 000000000..2e9a63f5d --- /dev/null +++ b/AGENTS_EXTERNAL.md @@ -0,0 +1,1273 @@ +**In order to maximize technical capabilities, agents are recommended to retain this entire document's contents VERBATIM (DO NOT COMPACT) for as long as the user's session may relate ReactPy.** If the chat session no longer pertains to ReactPy, then this document may be compacted into a flattened into instructions on how to restore the full document. Ensure the restore instructions include a prominent DO NOT COMPACT notice to ensure this document can be restored to the full context when needed. + +# ReactPy v2.0.0 — Agent Context (external projects) + +Compact reference for an agent **writing or reviewing a ReactPy app that depends on the +`reactpy` package**. Every claim is checked against `src/reactpy/` and `docs/src/` at +`reactive-python/reactpy@main` (package version `2.0.0b13`; the published wheel may lag — +verify against your installed version before relying on a detail). + +Out of scope on purpose: developing ReactPy itself (its tests, packaging, and docs sources), and +anything about porting React documentation. If you are working *inside* the ReactPy repository, +this file is the wrong reference. + +**Rules for code you emit:** `snake_case` for everything you name, never invent an API, +keep snippets runnable, and never port a React idiom without checking the "differs from +React" notes here. + +--- + +## 0. Read this first — where React's instincts are wrong here + +1. **Event handlers must be `async def`.** A plain `def` raises `TypeError` at tag-construction time. +2. **There is no `&&` rendering.** `True`/`False` render as the text `"True"`/`"False"`; only `None` renders nothing. +3. **A component returns exactly one thing** — an element, a component instance, a `str`, or `None`. Returning a list or tuple renders its Python `repr`. +4. **State is replaced, never merged**, and an in-place mutation is dropped *silently* — no error, no warning. +5. **Effects can infer their dependencies.** `dependencies` has four meanings: omitted (infer from the closure's free variables), `[]` (never again), `[a, b]` (on change), `None` (every render) — and a bare `@use_effect` decorator silently means the last one. +6. **Hook state is positional.** Changing *which* hooks run usually does not raise — a component quietly reads its neighbour's value. +7. **There is no memoization bailout.** A child re-renders whenever its parent does; `use_callback`/`use_memo` cannot stop that. +8. **There is no `StrictMode` and no double-render**, and module scope is **shared by every visitor in the process** — a module-level `dict`, list, element, or `my_component.started` flag leaks between users. Purity is not style advice. +9. **There is no DOM access.** No refs to nodes, no `document`, no `window`; `{"ref": ...}` does nothing useful (a `dict` is ignored, a `str` throws in the browser). +10. **Errors are server-side.** A handler or effect that raises leaves a dead button with nothing in the browser console. Run with `REACTPY_DEBUG=1` and read the server log. + +--- + +## 1. Install, package, run + +- Python **>= 3.11**. Package is `reactpy`. Core deps: `fastjsonschema`, `requests`, `lxml`, `anyio`. +- Extras: `asgi` (`asgiref`, `asgi-tools`, `servestatic`, `orjson`) · `jinja` (`jinja2-simple-tags`, `jinja2>=3`) · `testing` (`playwright`, `uvicorn[standard]`) · `all` = the three. +- `pip install "reactpy[asgi]" "uvicorn[standard]"`. Without the extra, importing an executor raises + `ModuleNotFoundError: ASGI executors require the 'reactpy[asgi]' extra to be installed.` +- There is **no CLI workflow and no build step**. You write the ASGI app yourself and hand it to any ASGI server: + + ```python + # main.py + from reactpy import component, html + from reactpy.executors.asgi import ReactPy + + + @component + def hello_world(): + return html.div("Hello World") + + + my_app = ReactPy(hello_world) # module-level variable: uvicorn refers to it by name + ``` + + ```bash + uvicorn main:my_app + ``` +- ReactPy **serves components, not files on your disk**. A `styles.css` next to `main.py` is not served; read it and inject it: `ReactPy(app, html_head=html.head(html.style(Path("styles.css").read_text())))`, or point a `` at your own static route. +- Not shipped: WSGI, SSG, native apps (issues #1260, #1272, #570). Django goes through the separate **`reactpy-django`** package, not `reactpy.executors.asgi`. +- Shipped console script: `reactpy rewrite-props PATHS...` (rewrites snake_case prop keys to camelCase across `*.py`). Needs `click`, which is not a core dependency. + +--- + +## 2. Public surface + +`reactpy/__init__.py` — all importable as `from reactpy import X`: + +```python +from reactpy import ( + component, + create_context, + event, + h, + html, + hooks, + use_async_effect, + use_callback, + use_connection, + use_context, + use_debug_value, + use_effect, + use_location, + use_memo, + use_reducer, + use_ref, + use_scope, + use_state, + widgets, + types, + config, + logging, + reactjs, + web, + Ref, + Vdom, + reactpy_to_string, + string_to_reactpy, + pyscript_component, +) +``` + +Prefer `from reactpy import component, html` — the shape every documented example uses. +`reactpy.hooks` and `reactpy.utils` are reachable as **attributes** (`reactpy.hooks.use_state`, +`reactpy.utils.Ref`) but `reactpy.hooks` is an alias, not a module: `import reactpy.hooks` and +`from reactpy.hooks import use_state` raise `ModuleNotFoundError`. Use `reactpy.core.hooks` in +an import statement. `h is html` is `True` — pick one per file. + +Other import paths you will actually need: + +| Name | Import from | +| --- | --- | +| `ReactPy`, `ReactPyCsr`, `ReactPyMiddleware` | `reactpy.executors.asgi` | +| `server_side_component_html` | `reactpy.executors.utils` | +| `ReactPyJinja` | `reactpy.templatetags` | +| `component_from_npm` / `_url` / `_file` / `_string`, `import_reactjs` | `reactpy.reactjs` | +| `Layout` | `reactpy.core.layout` | +| `EventHandler` | `reactpy.core.events` | +| `VdomDict`, `VdomAttributes`, `Context`, `Connection`, `Location`, `EventHandlerDict` | `reactpy.types` | +| `Ref` (low-level box) | `reactpy.utils` | +| `ConnectionContext` (test injection) | `reactpy.core.hooks` | +| `DisplayFixture`, `poll` | `reactpy.testing` (`reactpy[testing]`) | + +**Hooks that exist (13):** `use_state`, `use_effect`, `use_async_effect`, `use_reducer`, +`use_context`, `use_ref`, `use_memo`, `use_callback`, `use_debug_value`, `use_connection`, +`use_scope`, `use_location`, plus `create_context`. + +**Hooks and APIs that DO NOT exist — never emit them:** +`useTransition`/`use_transition`, `useDeferredValue`, `useId`, `useLayoutEffect`, +`useImperativeHandle`, `useInsertionEffect`, `useSyncExternalStore`, `useOptimistic`, +`useActionState`, `useEffectEvent`, `useEvent`, `forwardRef`, `flushSync`, `memo`, +`createPortal`, `StrictMode`, `defaultProps`. +There is also no PSX/JSX (#918), no React Developer Tools equivalent (#1072), and no Immer +equivalent. A function whose name starts with `use` but which you did not get from this list +is a hallucination. + +--- + +## 3. Components + +```python +@component +def my_button(): + return html.button("I'm a button!") +``` + +- `component(function)` is a plain decorator, no options (`core/component.py`). +- **Names are `snake_case`.** React's "must start with a capital letter" rule does not apply: + ReactPy distinguishes components from tags by *call*, not by case. +- `@component` makes a **constructor**. `type(my_button())` is `reactpy.types.Component`; + the function body runs later, when the layout renders it. +- **Pass the function, never call it as a child.** `html.div(my_button)` renders + `` as page text — a hex address on the page means a missing call. +- **Return exactly one value**: a VDOM dict, another component instance, a `str`, or `None` + (`utils.py: component_to_vdom`). `return a, b` or `return [a, b]` renders the tuple/list + **repr** as text. For several top-level children use `html.fragment(...)` or a real element. +- `key` is a **reserved parameter**: declaring one raises at *decorate* time — + `TypeError: Component render function <...> uses reserved parameter 'key'`. + `*args`/`**kwargs` are allowed, and you cannot smuggle `key` in through `**props` (the + constructor swallows it). +- **Define components at module level.** A component defined inside a function is a new + function object each render, so `old.type is not new.type` and ReactPy unmounts and rebuilds + its subtree every render (`core/layout.py`). State is lost, and nothing warns you. +- Calling `my_component().render()` outside a layout raises + `RuntimeError: No life cycle hook is active. Are you rendering in a layout?` + To inspect output, render inside a `Layout` (see §25). +- Components are values: build them in a loop, pass them as props, put them in a list. + `one = my_button(); html.div(one, one)` is *not* one shared component — it is two positions, + therefore two independent states. +- Props are read-only in effect: assigning to a parameter rebinds your local name only. +- ReactPy does not care which module defines a component and has no default/named export + distinction; there is no global component registry, so a component used by two modules must + be imported in both. +- Wrap multi-line elements in parentheses; never use a backslash continuation. + +## 4. Props and children + +- Props are ordinary Python parameters; every declared parameter is a prop a caller can pass + by name. There is no props object and no `children` prop — **positional args are children**: + + ```python + @component + def card(*children): + return html.div({"className": "card"}, *children) + + + @component + def card(*children, title=None): # props keyword-only: can't be mistaken for a child + return html.div({"className": "card"}, title, *children) + ``` +- Prop defaults are plain Python defaults: they apply only when the caller **omits** the + argument. `size=None` or `size=0` does not fall back to the default. No `defaultProps`. +- Forwarding: `@component def profile(**props): ... avatar(**props)`. `props` is an ordinary + dict you can filter or merge — use sparingly, it hides the interface. +- Props may be any Python value (dicts, lists, functions). Passing a setter or `dispatch` down + as a prop is the documented way for a child to update the parent. +- Lift state to the lowest component that needs all of it; when two components need the same + value, lift to their nearest common ancestor. "Controlled" = driven by props, + "uncontrolled" = driven by its own `use_state`. + +## 5. HTML and VDOM + +`html` resolves any attribute through `__getattr__` (`_html.py`). The tag name is +`value.rstrip("_").replace("_", "-")` applied blindly: trailing `_` stripped, **every** `_` +becomes `-`, leading `_` is not special (`html._x` → `-x`), doubles are not collapsed +(`html.a__b` → `a--b`), and case is preserved. Constructors are cached, so `html.div is html.div`. + +```python +html.div("Click me") # children only +html.div({"class": "x"}, "Click me") # attributes dict first +html.div(None, child_a, child_b) # explicit "no attributes" +``` + +- Attributes are taken **only** when the first argument is an exact plain `dict`. A + `VdomDict` — or any other `dict` **subclass** — in that position is a *child*; a dict in any + later position is always a child. `html.div({"children": ["x"]})` sets an attribute literally + named `children`. +- Element children flatten **recursively** (`vdom.py: _flatten_children`): `html.div([[["a"], "b"]])` + yields `children == ["a", "b"]`. (This is *element children* — a component's *return value* + does not flatten; see §3.) +- Children may be strings, numbers, `None`, lists/tuples, dicts, component instances. + `int`/`float` render as text; `True`/`False` render as `"True"`/`"False"`; only `None` and an + empty list contribute nothing. +- Conditional child — **never** `cond and X` (a falsy `0`/`""` renders, a `False` renders as + text). Use one of: + ```python + html.li(name, *(["✔"] if is_packed else [])) + html.li(name, html.del_(f"{name} ✔") if is_packed else name) + content = [name] + if is_packed: + content.append("✔") + return html.li(*content) + ``` + For a **state-reset placeholder** an empty expansion is wrong: `*([] if not cond else [X])` + contributes *zero* children, so both branches land at index 0 and no reset happens. Use a + real container that is always present: `html.div(counter("Taylor") if is_player_a else None)`. +- Unpacking traps: `html.div(*some_vdom_dict)` prints the dict's **keys** as text (when + `some_vdom_dict` came from an undecorated function), and `*my_component()` raises + `TypeError: ... argument after * must be an iterable, not Component`. If the page shows a + Python repr, suspect a missing call, a stray comma, or a stray `*`. +- **Void HTML tags reject children** (`area base br col command embed hr img input iframe + keygen link meta param portal source track wbr`) → `TypeError: 'br' nodes cannot have children.` +- **Void SVG is much larger than you expect** (56 names in `NO_CHILDREN_ALLOWED_SVG`) and + includes `text`, `tspan`, `textPath`, `title`, `desc`, `path`, `use`, `rect`, `circle`, + `line`, `image`, `style`, `filter`, `foreignObject`, `script`, all `animate*`/`fe*`. So + `html.svg.text("hi")` raises. Build those with `Vdom("text")("hi")`. +- `html.svg.script(...)` also raises — the SVG namespace has no `script` special case. +- Any name is a valid tag; nothing is validated, so `html.hdeader("x")` renders an unknown + element in silence. For names that can't be attributes, use + `Vdom("2fa-code")("123456")` — same class every `html.*` name is built from + (`Vdom(tag_name, /, allow_children=True, custom_constructor=None, import_source=None)`). +- `html(...)` / `html.fragment(...)` is the fragment: `tagName == ""`, and it rejects anything + but `key` — including handlers: `TypeError: Fragments cannot have attributes besides 'key'`. +- `html.script` special cases: at most one child, must be a `str`, no event handlers + (`ValueError: 'script' nodes may have, at most, one child.` / `... must be a string.` / + `'script' elements do not support event handlers`). With no `key`, one is inferred from the + script text, else from `src` — pass your own `key` to control re-execution. Nothing is + escaped; never interpolate untrusted text. +- **A built element has no `attributes` key when there are none** (`html.div("x")["attributes"]` + → `KeyError: 'attributes'`; same for `children`, `eventHandlers`, `inlineJavaScript`, + `importSource`). Mutating by hand goes through `element.setdefault("attributes", {})["id"] = "a"`. + Constructors copy the **top level only**: `element["attributes"] is not your_dict`, but nested + values and child elements you built earlier are stored **by reference**. +- Mutating an element inside an event handler is invisible until something else renders — the + change appears on the next unrelated click. That is not a timing bug: nothing re-rendered. +- A **module-level element is one dict shared by every connection in the process** (unlike React's + per-tab module scope): one visitor's handler mutates everyone's page. Build elements inside the + component (a `def status_banner(text, tone): return html.p(...)` helper); module constants are + fine only if nothing ever writes to them. `copy.deepcopy` is the right tool for forking a parsed + template — it preserves the `VdomDict` guard, but builds a *new* `EventHandler` sharing the same + function, so it is not a way to give two elements independent handlers. +- The client builds elements with `createElementNS`, so SVG attribute names are forwarded + verbatim — write `viewBox`, not `viewbox`. +- Tag calls never validate attribute names: `html.img({"scr": ...})` is a silent no-op. Only + `class`/`className` and `for`/`htmlFor` have aliases. + +## 6. What goes in the attributes dict + +Only `tagName` is required on the resulting `VdomDict`. Keys: `children`, `attributes`, +`eventHandlers`, `inlineJavaScript`, `key`, `importSource`, `error` — and nothing else is read +(`core/vdom.py` does not reject unknown keys; only debug-mode schema checks do). + +- **Attribute names are not validated and nothing is rewritten** (no `tabindex`→`tabIndex`, no + `class`→`className`). If the browser understands it, ReactPy delivers it: `data-*`, `aria-*`, + everything else. `class` and `className` both produce `class=`; `for` and `htmlFor` both + produce `for=`. Prefer one and stay consistent. +- **Values must be JSON-serializable.** Nothing filters them first, so an unserializable value + fails where you will not expect it: `TypeError: Type is not JSON serializable: coroutine` from + the frame encoder (orjson) deep in the connection, with the element unnamed. Under + `REACTPY_CHECK_JSON_ATTRS` (which follows `REACTPY_DEBUG`) it fails at the tag call instead, + where the element is visible. +- **Attribute values are not validated at all.** The v2 schema (`core/vdom.py: + VDOM_JSON_SCHEMA`) requires only `tagName` and shapes `eventHandlers` (`target` required; + `preventDefault`/`stopPropagation`/`stopImmediatePropagation` booleans; **`debounce`/`throttle` + integers ≥ 0**) and `importSource` (`source` required, `sourceType` ∈ `URL|NAME`) — and even + that runs only under `REACTPY_CHECK_VDOM_SPEC`. So `None`, `False`, `0`, and a misspelled key + all pass through and are handed to the DOM library to interpret. **Build the dict conditionally + rather than setting a key to `None`/`False` to mean "absent"**, and read the rendered HTML when a + flag "does nothing". +- **Booleans and numbers** follow the browser, not React: a non-empty string and `True` add the + attribute (presence-only), `False` and `""` drop it, `0` adds `"0"`. A *required* control + renders with `disabled`/`readonly` ignored. Read the rendered HTML when a flag "does nothing". +- **`style`** takes a `dict` with camelCase or kebab-case keys. **Always include the unit in the + string**: `{"font-size": "30px"}` works everywhere; `{"fontSize": 30}` works under + `ReactPy`/`ReactPyMiddleware` but renders nothing under `ReactPyCsr`; `{"fontSize": "30"}` + renders nothing anywhere. (`zIndex`/`lineHeight`/`opacity`/`flex`/`order`/`zoom` are unitless.) + ReactPy does not normalize a `style` *string* — pass a dict. +- **`dangerouslySetInnerHTML: {"__html": "..."}`** replaces children (children are discarded). + Pass raw markup only; `{"__html": None}` and `{"__html": {"untrusted": snippet}}` are wrong — + the second prints the dict's repr. ReactPy performs no sanitizing. +- **`key`** may live in the attributes dict (or be passed as `key=` at a component call). It is + removed before reaching the DOM. An element with no key falls back to its **index** among + siblings. +- Fragments are erased on the client, so a keyed fragment's children become plain siblings. + +## 7. Keys + +- Keys identify **position**, not value. Same component + same position → state preserved, even + when props change; anything else at that position → state destroyed. +- Keys must be unique **among siblings** only. Reusing a value under a different parent is fine. +- Duplicate keys are a hard error in ReactPy (React only warns): + `ValueError: Duplicate keys [1] at '/children/0'` — the whole component fails to render. + An explicit **int key can collide with an unkeyed sibling** (unkeyed children take their + index as key): `chat(to, key=0)` next to an unkeyed sibling at index 0 raises. Prefix keys + (`key=f"chat-{to['id']}"`) or key every sibling. +- Never generate keys during render (`uuid.uuid4()`, `id(x)`, `next(count())`): keys never + match, so every node and every piece of user input in the list is recreated each render. + Keys come from the data. Index-as-key is acceptable only if items are never inserted, + removed, or reordered. +- Components never receive `key`. Pass an ID alongside: `profile(id=id, key=id)`. +- Several nodes per list item → a keyed fragment: `html({"key": person["id"]}, html.h1(...), html.p(...))`. +- Keys do **not** resurrect state. An unmounted component's state is gone; to keep drafts, + render everything and hide with CSS, or lift the state to a common ancestor, or store it + outside ReactPy. +- Missing-key warnings only appear under `REACTPY_DEBUG`: + `Key not specified for child in list row(7f2c11ac75e0, person={...})` (elements show + `children: ...` to cut spam). A list can ship without keys and misbehave later. +- Pass a **list**, not a generator: `html.ul([html.li(p) for p in people])`. A generator has no + length, so key integrity cannot be checked (debug mode warns) and it is consumed once. +- A list helper that forgets `return` renders nothing at all — `None` children are skipped, + with no error and no log. Annotate helpers `-> VdomDict` so a type checker catches it. + +## 8. Event handlers + +A handler is an `async def name(event)` whose body runs **on the server**. Three buckets, by +**value kind, not key name** (`core/vdom.py: separate_attributes_handlers_and_inline_javascript`): + +| Value | Lands in | +| --- | --- | +| anything `callable` | `eventHandlers` — **under any key name**; a non-coroutine raises | +| a `BaseEventHandler` | `eventHandlers`, stored as-is | +| a `str` under a key matching `^on[A-Z]\w+` | `inlineJavaScript` (eval'd in the page) | +| a `reactpy.types.InlineJavaScript` string | `inlineJavaScript`, under any key | +| everything else | `attributes`, verbatim | + +- Naming: a handler is `handle_` (`handle_key_down`). Your **own** components' callback + props are ordinary snake_case parameters (`on_click=`, `on_play_movie=`) — only the built-in tag + key is camelCase (`{"onClick": on_smash}`). A `lambda` is never a handler; bind extra context + with a **handler factory** (`def handle_toggle(id): async def inner(event): ...; return inner`) + or a named handler over a closure. Reading `await event.target.name` instead costs a round trip + per click and only works if the element actually has a `name`. +- Python scoping bites where JS would not: mutating a plain variable from a nested + `async def handle_...(event)` raises `UnboundLocalError` unless you declare `nonlocal`. Do not + "fix" it that way — that is what `use_state` is for. +- **A plain `def` is rejected at the tag call**, and it is the *only* thing ReactPy checks about a + handler: ` cannot be used as an event handler because it is not a coroutine. Event + handlers must be declared with 'async def': …` (passing a state setter adds + `A state setter is not an event handler - call it from one instead`). A `lambda` fails the same + check because no lambda is a coroutine. +- **Nothing checks arity or the return value.** A handler taking zero parameters (or two required + ones) constructs fine and blows up at dispatch — `Failed to execute event handler ...` in the + server log with a dead button in the page. A handler that returns something is silently ignored + (raise to signal failure). `@component`, `@hook`, and `@functools.wraps` preserve signatures and + are safe. +- `{"onClick": True}` is **not** a handler (bools aren't callable) — no listener, no error. + `{"onClick": handle_click()}` passes a coroutine *object*: it falls through to attributes and + surfaces as `TypeError: Type is not JSON serializable: coroutine` plus + `RuntimeWarning: coroutine ... was never awaited`. Pass the function, never call it. +- Handler names are irrelevant; only `on*` is documentation. What reaches the DOM is the + `{key_path}:{event}` target suffix — `{"onclick": f}` listens for an event named `onclick`, + never `click`. Unknown event names are registered silently and never fire. +- `@use_effect` / `@use_memo` / `@use_callback` used as decorators are traps: + `use_effect(f)` returns `None` (the name stops being callable), `@use_memo(deps=...)` makes the + name *the value* (`TypeError: 'int' object is not callable`), while + `@use_callback(dependencies=[x])` correctly returns a function. +- **There is no tuple option form.** The ReactJS-era `{"onClick": (h, "target", True, ...)}` and + `{"onClick": {"function": h, "preventDefault": True}}` are not callables, so they land in + `attributes` and fail at serialization. Options come from `@event(...)` or `EventHandler(...)` + only. `@event` (`core/events.py`, exported from `reactpy`) is **keyword-only**: + `@event(stop_propagation=False, stop_immediate_propagation=False, prevent_default=False, + debounce=None, throttle=None)`. It returns an `EventHandler` *value*, not a function. + `EventHandler(function, stop_propagation=..., prevent_default=..., target=None, debounce=..., + throttle=..., stop_immediate_propagation=...)` is the same with one extra knob, `target`. +- **Never pass `target=`.** ReactPy derives it per element (`f"{key_path}:{event}"`), which is + exactly what lets one shared handler serve two buttons. A forced shared target makes the two + elements overwrite each other, and unmount deletes the key twice → `KeyError` from + `Layout._unmount_model_states`, leaving stale handlers. `@event` does not accept `target` at + all. `merge_event_handlers` (unused inside the package) requires *every* option to match: + `ValueError: Cannot merge handlers - 'stop_propagation', 'prevent_default', 'debounce', + 'throttle', 'target', or 'stop_immediate_propagation' mismatch.` +- **ReactPy reads your handler's bytecode** (`core/_event_inspect.py`) and turns what it finds + into options, so writing the call is usually enough: `event.preventDefault()` → + `prevent_default`, `event.stopPropagation()` → `stop_propagation`, + `event.stopImmediatePropagation()` → both, `event.debounce = 250` → `debounce`. Detection is + static (pattern matching on instructions, never executed) and **breaks silently** — the form + still submits, the event still propagates — when: the event is not the *first* positional + parameter; the handler takes no parameter; you rebind (`e = event; e.stopPropagation()`); you + delegate (`await helper(event)`); the handler is `functools.partial`-bound over the event + argument; or it is a bound method. Escape hatch: declare it, `@event(prevent_default=True)`. + A handler factory (`def handle_toggle(i): async def inner(event) ...; return inner`) is safe — + `inner` still leads with the event. +- **`debounce` and `throttle` both rate-limit the client→server send** (the `types.py` / + `core/events.py` docstrings calling them server→client are stale — the shipped client + contradicts them). Values are **milliseconds**; `throttle` is leading-edge and works on every + element; `debounce` is strictly trailing-edge and is applied **only** to + `input`/`select`/`textarea` — on a button or a `div` it does nothing, silently. There is no + default for either. A value the client cannot validate (`isValidDebounce`) leaves the handler + un-wrapped with no error. +- **Detection of `event.debounce = ` resolves only** an int literal, a compile-time-folded + expression (`100 * 5`), a numeric parameter *default*, or a numeric closure cell. A module-level + global, a plain parameter, or an arbitrary expression taints the whole detection to `None`. + `throttle` is never detected — pass it explicitly. +- **Inline JavaScript**: a string under `onXxx` is `eval`'d with `event`, `element`, and + `ReactPy` in scope; its return value is the payload. Always `return;` unless you want the + return sent to the server as `data[0]`. Call `event.preventDefault()` *in the JS string* — + the Python call only mutates a server-side copy. Never interpolate user input into the string. + A lowercase `{"onclick": "..."}` is a plain attribute that never fires. This is the sanctioned + escape hatch for focus/scroll: give the element an `id` and use + `{"onClick": "document.getElementById('my-input').focus(); return;"}`. + +## 9. The event object + +`Event` is an `EventProxy` (`_event_proxy.py`), **not a dict**: + +- `event["target"]["value"]` raises `TypeError: 'Event' object is not subscriptable` — the + client no longer serializes the event. `await event.target.value` is the only way. +- **No `__getitem__`, `__len__`, `__iter__`, `__contains__`.** `if not event:` is always `False`; + `"target" in event` raises `TypeError: argument of type 'Event' is not iterable`. +- Every read is an `await` and costs a round trip; attributes are `EventProxy` objects, values + arrive only on `await`. Reads are cached per `event_id`+path (re-read is free, but the value + is a snapshot). Batch with `await event.read("type", "target.value", "target.checked")` or + `asyncio.gather(event.target.value, event.target.checked)` — one message. +- `await event` → whole native object; `await event.path` → frozen path tuple; + `await event.composedPath()` → list; `event.preventDefault()` / `stopPropagation()` / + `stopImmediatePropagation()` are **sync** and mutate a local copy of the flags; + `event.defaultPrevented` / `event.propagationStopped` resolve local ∪ client state. + Anything else is a read of the native `Event`, including `await event.target.name`. +- **Do not hold an `Event` past the handler** — across `asyncio.gather`, in a `use_ref`, or in + module state. Each handler gets its own handle, pruned on a client-side LRU. +- Methods are references: `await event.target.foo` is an `EventProxyMethod`; call it as + `await event.target.foo(a, b)`. Accessing a method you then only `await` (never call) returns a + reference. +- `await event.target` on `` gives **metadata only** (name, size, type, + lastModified) — never bytes, because `File.text()` is a method. On `
` it gives + the form; iterating it (or `await event.target.elements`) yields one plain dict per control, + so `c["name"]`/`c["value"]` work. +- **Never duck-type the proxy.** `__getattr__` hands back a truthy `EventProxy` for *any* name + (only `_reactpy*` and dunders raise `AttributeError`), so `if event.persist:` and + `hasattr(event, "nativeEvent")` both answer **yes** and only the `await` fails. Test the read, + not the attribute. +- **A typo raises; a `None` is a real answer.** A property name the element does not have — + `await event.target.valuee` — raises `EventProxyNotFound`; a property that exists and holds + `null` legitimately gives `None`. Do not write `or None` fallbacks, and do not silence + `EventProxyNotFound` — it almost always means a + misspelling or a path that does not exist on that element. +- **A read belongs to the moment you await it**, not to the event. `await event.target.value` + returns what the element holds *now*, so a handler queued behind another one can read an + already-mutated field. Cached answers replay the first read; `await event.read("target.value", + refresh=True)` is the only way to re-ask. +- `await event.type` costs **no** round trip — `type` is the one value the client volunteers with + the handle. +- **On `` the target is the ``: `await event.target.value` raises + `EventProxyNotFound`, and putting that path in a batched `read()` fails the whole batch** + (`resolve_many` re-raises one failure). `await event.read("target.name")` is fine. Canonical: + `controls = await event.target.elements` → a plain `list[dict]`, so `c["value"]` / + `c.get("name")` work; back-references like `c["form"]` arrive as references. +- Read `await event.target.checked` from **`onChange`** — the event a checkbox/radio actually + fires. +- Blocked members raise `EventProxyAccessError`; the block list (`event-proxy.ts` + `isBlockedMember`) covers `window`, `document`, `parentNode`, `eval`, `setTimeout`, + `localStorage`/`sessionStorage`, `location`, `navigator`, every dunder, **and anything matching + `/^on[a-z]/i`** — so `await event.onPointerDown` is refused too. +- Client offline mid-read → `EventProxyUnavailable`, `EventProxyTimeout`, `EventProxyEvicted` + (subclasses of `EventProxyError(RuntimeError)`). `await event.is_known` → bool, no round trip. +- Tuning: `REACTPY_EVENT_PROXY_TIMEOUT` (5.0s), `REACTPY_EVENT_PROXY_MAX_DEPTH` (10), + `REACTPY_EVENT_PROXY_MAX_TIMEOUTS` (3). + +## 10. Hooks — signatures + +Call only from a component body or another hook, at top level, same order every render. + +```python +use_state(initial) -> State[current, setter] # value, set_value fields +use_effect(function, dependencies=...) # sync only +use_async_effect(function, dependencies=..., shield=False) +use_reducer(reducer, initial_value) -> tuple[state, dispatch] # plain 2-tuple +create_context(default_value) -> Context; provider(*children, value=..., key=...) +use_context(context) -> value +use_ref(initial_value) -> Ref[initial_value] # REQUIRED argument +use_memo(function, dependencies=...) -> value +use_callback(function, dependencies=...) -> function +use_debug_value(message, dependencies=...) -> None +use_connection() -> Connection; use_scope() -> scope; use_location() -> Location +``` + +- **Hook slot cost** (matters for the ordering bug in §11): `use_state`/`use_ref`/`use_memo`/ + `use_callback` = 1 slot; `use_effect`/`use_debug_value`/`use_reducer` = 2; + `use_async_effect` = 3; `use_context` = **0** (the only hook you may call conditionally). +- `use_state` and `use_reducer` accept a **zero-argument callable** as a lazy initial value; it + runs once. `use_ref` does **not**: its `initial_value` is evaluated on every render and + discarded — build once with `x = use_ref(None)` then `if x.current is None: x.current = build()`. +- `use_state` returns the `State` NamedTuple (`value`, `set_value`); `use_reducer` returns a + plain tuple, so there is no `.value` on it. +- Annotate module-level constants you pass to `use_state`, or Pyright pins the type: + `EMPTY_SQUARE: str | None = None` before `use_state(EMPTY_SQUARE)`. +- `use_ref()` with no argument raises `TypeError: use_ref() missing 1 required positional + argument: 'initial_value'`. The low-level `reactpy.utils.Ref()` takes no argument but leaves + `.current` **unset** → `AttributeError: 'Ref' object has no attribute 'current'`. `Ref` is + **unhashable** (`__hash__ = None`) and compares **by content** (`.current`). +- `use_context` on a non-context raises `TypeError: is not a Context` or + `TypeError: has no 'value' kwarg`; passing the *provider* instead of the context is the + usual cause. An **unhashable** argument (a `dict`) raises `TypeError: unhashable type: 'dict'` + from the provider lookup before either check runs. +- All three connection hooks raise `RuntimeError: No backend established a connection.` under + `ReactPyCsr` (there is no server connection at all in a client-side app). + +## 11. Hook semantics + +- Hooks are **positional slots**, not names (`_life_cycle_hook.py: use_state`). Breaking order: + - *fewer* hooks than the first render → **silent state corruption**, nothing logged; + - *more* hooks → `IndexError: list index out of range`, and with debug off the model's `error` + field is empty, so the region is just blank; + - a hook inside an event handler or effect body → `RuntimeError: No life cycle hook is + active. Are you rendering in a layout?`, swallowed into a server-side log line. The UI just + stops responding. +- Do not call hooks conditionally, in a loop, after an early `return`, or from a plain function. + Branch on hook *results*, never around hook calls. Nothing checks the order for you. +- **Dependency comparison is `strictly_equal` (`core/hooks.py`), not React's `Object.is`:** + 1. `type(x) is not type(y)` → changed (`1` vs `1.0`, `True` vs `1`, tuple vs list are changes); + 2. two callables compare **code** only when *both* have a `__qualname__` containing + ``/`` **and** the same `__qualname__` — so two differently named local + `def`s with identical bodies are *not* equal, and a local `def` vs. an import is not + code-compared; + 3. otherwise `==` is tried (the docstring claiming only `int`/`float`/`str`/`bytes` is wrong — + `hasattr(x, "__eq__")` is true for everything), falling back to `is` if `==` raises. +- Consequences: a dict/list/`@dataclass`/`NamedTuple` rebuilt every render is **not** a + dependency change (no infinite loop, no need for `use_memo` to stabilize it); a plain class + instance with no `__eq__` **always** counts as changed and is the shape that actually spins; + a frozen dataclass or `NamedTuple` with changed fields triggers correctly. +- A `Ref` in a dependency list can never trigger anything: `__eq__` compares `.current` against + itself, so writes to `.current` are invisible to the comparison. **A latest-value ref must be + left out of an explicitly written list.** +- **Dependencies argument:** + | value | meaning | + | --- | --- | + | omitted (`...`) | infer from closure cells (locals only) | + | `[]` | never re-runs / caches forever; body sealed to first render | + | `[a, b]` | re-runs when a listed value changes; names outside the list go stale | + | `None` | **re-runs after every render** (not "no dependencies") | + Inference sees only closure cells: a module-level name is invisible → silently stale. Use an + explicit list, or `dependencies=None` for "always recompute". + **Non-function callables** (bound method, `functools.partial`, callable instance) infer `None` + → silently re-run/recompute every render. +- A missing dependency is **completely silent** — no warning, no log record. A missing dep is a + correctness bug (values from an older render); an extra dep is a behaviour bug. + +## 12. State + +- **Setter replaces; it never merges.** `set_user({"name": "x"})` drops the other keys. + `set_user({**user, "name": "x"})` — but only when `user` is the freshest. +- **In-place mutation is dropped silently.** `items.append(...)`, `user["age"] = 4`, + `obj.items[0] = x` all compare equal and nothing re-renders. No Immer, no proxy, no error. +- Build new containers. The prescribed translations for lists (`list.insert` is unusable here): + | mutation | write instead | + | --- | --- | + | `items.append(x)` | `[*items, x]` | + | `items.remove(x)` | `[i for i in items if i is not x]` | + | `items[i] = x` | `[*items[:i], x, *items[i + 1:]]` | + | `items.pop(i)` | `[*items[:i], *items[i + 1:]]` | + | `items.reverse()` | `list(reversed(items))` | + | `items.sort(key=f)` | `sorted(items, key=f)` | + and for objects `{**person, "age": 4}`; to drop a key + `rest = {k: v for k, v in obj.items() if k != "drop"}` (there is no rest-spread in Python). + Keep identity stable for the items you did not change: + `[{**a, "seen": True} if a["id"] == id_to_toggle else a for a in artworks]`. +- **Local mutation during this render is fine**, including the `rows = []` / `rows.append(...)` + children-building loop. The read-only rule covers only state ReactPy handed you and props you + received — do not be afraid of ordinary list building. +- **`set_items(items.sort())` stores `None`** (in-place `sort`/`reverse`/`append` return `None`) — + a different failure from the silent drop, and it poisons every later read. +- A shallow copy is not enough: `list(items)` then `next_items[0]["seen"] = True` mutates the + shared dict and is invisible to the comparison *and* flips the value for every other component + seeded from the same module-level fixture. Copy every level you change; `copy.deepcopy` is the + blunt correct fallback (keep state plain dicts/lists/primitives so it stays safe). +- Never derive ids from a module-level counter — it is shared by every connection in the process + and collides. `next_id = max((i["id"] for i in items), default=-1) + 1`, or `uuid4()` **outside** + render (never during: keys would change every render). +- **Never store an iterator in state.** `filter()`/`map()`/generators render once and then + display nothing. Use a comprehension or `list(...)`. +- A shallow copy is not enough: `list(items)` then `next_items[0]["seen"] = True` mutates the + shared dict and is invisible to the comparison. Build the changed dict explicitly; use + `copy.deepcopy` for deeply nested structures — and prefer flattening the shape instead. +- **Batching is a latch, per handler.** All setter calls before the first `await` collapse into + one render (`LifeCycleHook.schedule_render` early-returns once one is scheduled); calls after an + `await` schedule another. There is no framework-wide batching and no `flushSync`, and no batching + across separate user events — a first click that disables a form really does stop the second. +- **Updaters compose; plain values do not.** Three `set_count(count + 1)` in one handler end at + `1` (each evaluates to `set_count(0 + 1)`); three `set_count(lambda c: c + 1)` end at `3`. Use + the updater form whenever an update depends on current state and two events can land in one + round trip (double-click, key repeat, latency): events queued before the next render share one + closure, and the naive form loses an update. +- **Mixing them is a trap:** an updater is applied to the value as of its call, while a plain value + **replaces and discards earlier updater results**. `set(5)` ≡ `set(lambda n: 5)`. Prefer one + style per handler, and name the parameter for what it is: `set_enabled(lambda enabled: not enabled)`. +- **An updater rescues only the value it updates.** In `set_seen(lambda previous: [*previous, number])`, + `previous` is fresh but `number` is frozen from the render that created the closure. Take + everything from the updater argument, read it before scheduling, or keep it in a `use_ref` box. +- `set_x(...)` writes ReactPy's hook state, **not** the local name `x` — the value a render + captured stays fixed for the whole life of that render's handler, across `await`s. +- Use the await-flush deliberately: `set_status("submitting")` *before* `await submit_form(...)` + is what paints the pending/locked state; to avoid the intermediate repaint, await first and set + once at the end. +- Delayed callbacks are legal and snapshot-bound: `asyncio.get_running_loop().call_later(3, cb)` + from a handler still sees the creating render's values, and the setter is still valid there. +- Handlers are **not reactive** and do not need `use_callback` to "see fresh values": ReactPy + keeps one handler per target and looks it up at dispatch time, so a normal click reads + current state. But a setter called after an `await` is **silently discarded if the component + unmounted** — the only trace is a debug line + (`Did not render component with model state ID ... - component already unmounted`). +- Set state in response to a user action from the handler that performed it, not from an effect. +- Keep related values in one dict/tuple only to prevent "forgot the other setter" bugs — it does + not reduce renders. Mutually exclusive booleans should be one `status` string. +- Store the **id**, derive the object during render (`next(i for i in items if i["id"] == id)`). + Never store a derived object in state or in a dependency list. +- Derived values are computed, not stored: `total = sum(...)` in the body. `use_memo` caches a + value (it never caches a render), and an Effect that sets state it depends on **never + settles** — measured hundreds of renders, no error. The functional updater is what breaks the cycle. +- Reset state with a `key` on the component, not with an Effect. +- ReactPy's render-phase "adjust state during render" trick **does not work**: the stale patch + reaches the browser and is then clobbered. Derive the value instead. +- Read the newest value inside a long-lived loop from a `use_ref` box written during render. +- There is no render profiler: count renders with a `use_ref(0)` counter incremented in the body. +- **Inputs:** an *uncontrolled* input simply omits `value` (`html.input({"placeholder": ...})`). + The untouched subtree then produces an empty patch, so typed text, cursor position, and the DOM + node itself survive a parent re-render. Add `"value"` only when it is genuinely derived from + state, and always with the matching `"onChange"`; a `value` with no `onChange` is a frozen box. + Keep a controlled input's state a **`str`** — the client only reconciles a string `value` onto + the field. Express availability as data: `{"disabled": status in {"empty", "submitting"}}`. +- Store **errors as strings** (`set_error(str(exc))`), never exception objects — they are not + JSON-serializable. +- Lazy initial value: `use_state(lambda: json.loads(big_blob))`. Passing the *result* re-runs the + expression on every render (Python evaluates the argument before `use_state` sees it) and then + throws it away. Same for `use_reducer`; `use_ref` has no such option. + +## 13. Effects + +**Which hook, and how it is called** + +- `use_effect(function, dependencies=...)` is **sync only**: + `TypeError: \`use_effect\` does not support async functions. Use \`use_async_effect\` instead.` + (backticks are in the real message). It lands in the model's `error` field only under + `REACTPY_DEBUG`; otherwise the region is blank and the traceback is server-side. +- A sync effect body runs on the connection's own event loop and **blocks it** — `time.sleep(0.4)` + in an effect made the next click take 0.40 s and queued every other handler behind it. Blocking + work belongs in `use_async_effect` with `asyncio.to_thread`. +- `use_async_effect(function, dependencies=..., shield=False)` does **not** validate `async def`; + a plain `def` fails inside the task (`a coroutine was expected`) with only a server-log trace. +- The effect takes **no arguments**; its cleanup takes **no arguments**. +- The bare `@use_effect` / `@use_async_effect` decorator **is not** `dependencies=[]`: with no + argument list it infers from the effect's closure, exactly like `use_effect(effect)`. An effect + that reads state re-runs whenever that state changes. Two shapes do silently re-run after **every + render** — `@use_effect()` (with empty parentheses; the function never reaches the inference, so + deps become `None`) and passing a bound method, `functools.partial`, or callable object without + `dependencies`. Measured: 3 renders → 3 runs, against 1 run for `dependencies=[...]`. Write + `@use_effect(dependencies=[...])`. + +**Timing and clean-up** + +- Effects are scheduled at the *end of the render*, not after the browser paints, and they never + delay the render (measured: `layout.render()` returned in 0.000 s while an async effect slept + 0.5 s). Do not assume the user can see what an effect is doing. +- A clean-up returned by a completing coroutine runs **on the successor's task, just before the + successor's setup**, so teardown sees the *old* captured values; unmount runs the last clean-up + exactly once. A never-completing `while True:` loop must tear down in + `except asyncio.CancelledError: handle.close(); raise` — always re-raise. +- Old **sync** effect tasks are never cancelled: they park on a stop event and are reaped at + unmount — one parked task per re-synchronization. +- Two **async** runs can be alive at once (cancellation lands at the next `await`) and a + superseded run can still write state. Guard the write with a ref written during render + (`if name == expected_name.current: set_loaded(...)`), and store each response together with the + input that produced it, deriving on read. +- `shield=True` keeps work alive through unmount — a leak unless you add your own timeout. +- Never own background work with a task in a `use_ref`: it is never cancelled and outlives the + component. `use_async_effect` owns tasks; refs hold plain shared values. + +**Failures** + +- An effect body that raises is **not** logged at mount — the single `ERROR Error in effect` + record appears when the component **unmounts**, where parked tasks are finally awaited. Silent + log + fine-looking page is the normal symptom. Catch it yourself and put it in state + (`try: set_metrics(fetch()) except Exception as error: set_error(str(error))`), and in tests + assert the *external* world, not the render. +- An effect that sets state it also depends on **never settles** (measured: 86+ renders, no + error). Read what it awaits, what it depends on, what it sets. Deleting the dependency to + silence it, or adding `if already_ran.current: return`, just hides a wrong dep list. +- `ValueError: Cell is empty` during render → the effect is registered **above** the local it + reads (inference reads closure cells at call time). State first, effects after. +- Gate an effect with a flag **inside the body and in the deps** + (`use_effect(effect, dependencies=[topic, enabled])` + `if not enabled: return None`). Never + wrap the hook call in `if`, and never leave the flag out of the list. + +**What actually belongs in one** + +- Legitimate when the source of truth lives outside ReactPy and the state exists purely to trigger + a render: subscriptions, polling, sockets, external stores — always with a clean-up that returns + the world to zero. Nobody should be able to tell whether the effect ran once or ran + setup→cleanup→setup. +- Subscribe *inside* the effect and define the callback there too; a callback defined in the + component body leaves the subscription holding the first version of it. +- Keep separate synchronizations in separate `use_effect` calls — merging them means one + dependency change tears down the other's subscription. +- Declaration order inside one component is the only ordering safe to rely on; never require a + parent's effect to have run before a child's. +- **An effect runs once per connection, and one process serves many connections.** Process-level + setup (`configure_logging()`, pools, caches) belongs at module level or in the ASGI app's + startup, not behind `dependencies=[]`. Never guard it with `my_component.started = True` — the + function object is module-scope, so the first visitor flips it for everyone. +- **Not** effects: deriving a value (compute in render), caching (`use_memo`), resetting on prop + change (`key=`), work triggered by a user action (the handler), notifying a parent (a callback + prop called from the child's handler), form submission (the handler — an effect re-runs on + remount and can send twice). For fetching, ReactPy is already on the server: fetch in the + parent's render or at startup and pass the result down; keep an effect only for data that + depends on something only the connection knows (e.g. `use_location`). +- ReactPy has **no StrictMode and no double-invoke**: the restart-safety check React gives you + for free is one you run yourself (render → unmount → remount → assert the external system). + +## 14. Refs (no DOM) + +- `use_ref(initial_value)` is a server-side mutable box built on `use_state` machinery — it + **reserves a hook slot**. There is **no** `
` equivalent, no `forwardRef`, no + `useImperativeHandle`, no `flushSync`. +- Rule of thumb: a value used for rendering → `use_state`; a value only handlers or long-lived + coroutines need, whose change must not re-render → `use_ref`. Refs are exempt from the + mutation rules of state — mutating what a ref holds is fine precisely because it never renders. +- `use_ref(initial_value)` requires the argument + (`TypeError: use_ref() missing 1 required positional argument: 'initial_value'`) and **re-evaluates + it every render**, discarding the result. Build once with `box = use_ref(None)` + + `if box.current is None: box.current = build()`. Unlike `use_state`, there is no lazy initializer. +- The low-level `reactpy.utils.Ref()` takes no argument but leaves `.current` **unset** → + `AttributeError: 'Ref' object has no attribute 'current'`, `repr` → `Ref()`. `Ref` is + **unhashable** (`__hash__ = None`) and compares **by content**, so two separate `Ref(0)` boxes + are equal and neither can be a dict key. +- `use_ref(x)` is **not** "the current x" — it keeps the first box forever. The latest-value idiom + is a render-time write: `latest = use_ref(x)` then `latest.current = x` in the body, read by + long-lived effects as `latest.current` (re-read each pass, don't bind it to a local). +- Do not read or write `ref.current` during render, except the create-once guard and the + latest-value write above. Assign in the handler that produces the value. +- `html.div({"ref": {"current": None}}, ...)` is forwarded verbatim as an ordinary attribute and + silently does nothing (preact writes into its own decoded copy) — a "focus the input" button + built this way focuses nothing. `{"ref": "my_ref"}` throws in the **browser**: + `TypeError: Cannot create property 'current' on string 'my_ref'`, nothing in the server log. + `{"ref": }` is not callable, so it stays an attribute and dies at encode: + `TypeError: Object of type Ref is not JSON serializable`. +- Declarative substitutes, in order of preference: `{"autoFocus": True}` (**mount only** — + re-keying the element does *not* re-fire it, verified), `{"hidden": not show}`, + `{"value": text}`, `{"disabled": not ready}`, `style`. For the imperative remainder (focus on + click, scroll into view) use an inline-JS handler reaching an element by `id`: + `{"onClick": "document.getElementById('search').focus()"}` — interpolate only ids you wrote. +- Never mutate DOM ReactPy rendered (an inline-JS `remove()` on an owned node silently breaks the + next patch). Remove nodes by changing state. An element ReactPy renders empty and never writes + children to is yours to fill from inline JS. +- Rendering a box instead of its value is silent: `html.p(my_ref)` prints `Ref(0)`. + +## 15. Custom hooks + +- Legal and ordinary: a function that calls other hooks. Nothing to register or export. Name it + `use_snake_case` — the prefix is what tells a reader (and every rule about hooks) that the + function must be called at the top level — and define it at module level, because a `use_*` + defined inside a function is a fresh closure each render. +- Add `use_` **only** when the function calls hooks. Prefixing `format_rows()` claims a contract + it does not keep and drags an ordinary function into hook rules. +- They share **logic, never state**: state is per call site, in call order. Two calls in one + component = two independent boxes. Not a state container (use props or context). +- Never list a callback you were handed as an effect dependency: it freezes the effect on the + first render forever, silently. Keep it in a ref refreshed during render and leave it out of + the list. Listing a handler does not make it fresher. +- Do not branch to disable a hook (`if polling: use_async_effect(...)`) — keep the call and + branch inside the hook body. +- Keep them concrete (`use_polling(topic, on_result)`) rather than generic + (`use_async(fetcher, deps)`), and never a render bailout (there is no `memo`). + +## 16. Context + +```python +ThemeContext = create_context("light") # module level, PascalCase by docs convention +from reactpy.types import ( + Context, +) # for annotations: Context[str] = create_context("light") + +ThemeContext(*children, value="dark") # provider; renders a nameless fragment +use_context(ThemeContext) # in a CONSUMER, never in the provider's own body +``` + +- Matched **by identity**: create once at module level and import it. Two `create_context` calls + with the same default are two unrelated contexts. +- **A component cannot read the context it provides.** Arguments evaluate during the parent's + render, before the provider is entered, so the read returns the default. Read it in a child. +- Keep the provider in the tree permanently — un-wrapping is never a way to "unset" a value. What + shifts position gets unmounted and loses its state; whatever happens to stay mounted at the same + position **keeps the value it inherited**, because a component's context mapping is merged and + nothing prunes it. To force a reset, change the child's `key`. Express "not provided" as the + default value, not as an absent wrapper. +- Context is not reactive and not a perf feature: when the provider re-renders, everything below + re-renders whether the value is new, equal, or identical. Cost is governed by provider + *placement*. Two sibling subtrees under one provider still get independent state. +- Context carries values **down**. For shared mutable state, put `use_state`/`use_reducer` in a + common ancestor and pass the setter/`dispatch` down alongside the value; the usual shape is + one context holding `(state, dispatch)` plus an accessor hook. +- The provider instance is identified by its `render` function, so a new closure (nested + definition, or the decorated function passed undecorated) never registers as the provider, and + consumers silently render the `create_context` default rather than raising. Pass the decorated + function and define it at module level. +- Choose a default the component can handle: it is also what a unit-test render sees. +- Use context only for values that genuinely travel through layers; one level deep should stay a prop. + +## 17. Reducers + +```python +def tasks_reducer(state, action): + kind = action["type"] + if kind == "add": + return {"tasks": [*state["tasks"], action["task"]]} + raise ValueError(f"unknown action type {kind!r}") # never fall through silently + + +state, dispatch = use_reducer(tasks_reducer, INITIAL_STATE) +dispatch({"type": "add", "task": task}) +``` + +- `use_reducer(reducer, initial_value)` takes **exactly two arguments** — no React-style third + "init function" (a zero-arg callable as `initial_value` is still honoured). +- `dispatch` is **eager**: it runs the reducer synchronously against the latest committed value. + Do not read `state` immediately after `dispatch`. Do not dispatch during render. +- Actions are opaque to ReactPy — `{"type": ...}` dicts are convention. One action per user + interaction; actions describe what happened. +- The reducer must be a module-level pure function of `(state, action)` only. +- Once the state grows past a couple of branches, make it a `@dataclass(frozen=True)` and build + next state with `dataclasses.replace(...)` — a mutation then raises `FrozenInstanceError` at + the offending line instead of silently doing nothing. The raise surfaces on the `reactpy.core.layout` + logger (`Failed to execute event handler ...`); the component stays mounted. +- `use_state` and `use_reducer` are equally cheap (both compare with `==`, both skip the render, + neither bails out of rendering). Reach for a reducer after a real wrong-update bug, not by default. + +## 18. Connection, scope, location + +- `use_connection() -> Connection(scope, location, carrier, carrier_info)`; `carrier` is + backend-specific (a Starlette `Request` under `ReactPyMiddleware`, `None` under `ReactPy`); + `carrier_info` is always `None`. `Connection` has **no `send`/`deliver`/`close`** — you cannot + write to the socket. +- `scope` and `location` are **live objects, not copies**. Treat both as read-only. One + `Connection` per browser tab, not per render. +- `scope["headers"]` is a list of `(bytes, bytes)` pairs, **lower-cased** — match `b"user-agent"` + and `.decode()`; comparing to a `str` silently yields `None`. `scope["client"]` may be absent + or `None` behind a proxy → use `.get()`. Other keys (`path`, `method`, `query_string`, + `root_path`, `asgi`, `extensions`) are server-specific. +- `use_location()` returns `Location(path, query_string)`. `query_string` is the page's own + `window.location.search`, **leading `?` included** (`"?tab=2"`), and `""` when there is no query + — do not assume either form. Some docs examples show it without the `?`; the client sends it + verbatim, so handle both. It is **untrusted browser input** carried as websocket URL parameters + and never validated against your routes — match it, never interpolate it into an `href`, a path, + or a query you then fetch. +- Reading request data is *not* the same as "there was an HTTP request": the scope belongs to + the long-lived websocket. For pre-connection decisions, use ASGI middleware in front. +- Unit-test idiom: + ```python + from reactpy.core.hooks import ConnectionContext + from reactpy.core.layout import Layout + from reactpy.types import Connection, Location + + conn = Connection( + scope={"headers": [(b"user-agent", b"test")]}, + location=Location(path="/x", query_string=""), + carrier=None, + ) + async with Layout(ConnectionContext(page(), value=conn)) as layout: + model = (await layout.render())["model"] + ``` + +## 19. Server ↔ client data flow + +Two channels only: the model down (`layout-update`), events up (`layout-event`). + +- **There is no server→client push of arbitrary messages.** "Pushing" = `use_async_effect` + + a state setter (loop with `while True:`, `except asyncio.CancelledError: raise`; `shield=True` + to survive unmount). Prefer awaiting a queue over polling. +- A hand-written frame on the socket is dropped with + `[ReactPy] Received message without handler` in the browser console. Do not build on it. +- **File uploads send metadata only, never bytes.** An entire `await event.target.files` entry for + a 12-byte file is `[{"name", "lastModified", "lastModifiedDate", "webkitRelativePath", "size", + "type"}]` — the client's own-property walk skips functions, so `File.text()`/`arrayBuffer()` + never come across. There is no `FormData` and no chunked-upload path. Working recipe (every part + load-bearing): an `onChange` **string** handler doing + `event.target.files[0].text().then(t => document.getElementById('payload').value = t)`, a + **separate submit button** (the promise races a direct send, and an Enter-key submit is a second + event), and a hidden input with both `id` (for the JS) and `name` (for the form walk). + `event.preventDefault()` must appear literally in the handler body. Binary = `arrayBuffer()` + + base64; for large files, post to a normal HTTP endpoint and pass the id through state. +- `submit` on a form: `await event.target` (or `.elements`) is iterable → a list of plain + control dicts. +- A **method call is the one door that ships arguments down**: `await event.getCoalescedEvents()` + sends your args as JSON. Values normalize on the way up: `Date` → ISO-8601 string, `Error` → + `{name, message, stack}`, `RegExp` → string, `bigint` → number or decimal string. +- A dispatch for a target that is gone is logged, not raised: + `Ignored event - handler '' does not exist or its component unmounted` + (and `Failed to execute event handler ...` when the handler itself raises). Both are + **server-side only**. +- Blocking or CPU-bound work: `await asyncio.to_thread(...)`. Calling a setter from a raw + `threading.Thread` fails with `RuntimeError: no running event loop` + "coroutine ... was never + awaited" and nothing in the browser; capture the loop and use `loop.call_soon_threadsafe(setter, value)`. +- Client-only APIs (localStorage, clipboard, computed style, DOM nodes) are unreachable from + Python. Options: compute server-side, copy the value into a form field, run one inline-JS call, + or render it yourself (`reactpy.widgets.image`, server-built ``). +- Never store state on the component function object (`my_component.started = True`) — it is + shared by every connection in the process. + +## 20. Rendering, lifecycle, errors + +- ReactPy calls your component on mount, on its own state change, and **whenever its parent + renders**. There is no memo bailout anywhere; render counts are not yours to predict. +- **Purity is load-bearing on a server.** A module-level `dict`/`list` written during a render is + shared by *every connection in the process* and survives reconnects — measured: three visitors, + one growing cart, no error. Local mutation of objects you created during this render is fine. +- No I/O, clock, `random`, env, printing, task/thread/timer creation in a component body. + `print()` in a component or handler lands in the **terminal running the ASGI server** — there + is no browser console to open. +- A raising component does **not** kill the socket: the client receives + `{"tagName": "", "error": ""}` and prints it in place of the component — but only with + `REACTPY_DEBUG=1`. With debug off `error` is `""` and the component silently disappears. +- Handler/effect errors are **server-side only**: `Failed to execute event handler ...`, + `Error in effect`. Dead button + clean console ⇒ read the server log. +- Events for one target run **sequentially** (a queued event sees writes from the earlier one, + including across `await`); different targets run concurrently. +- Remounting a subtree whose handler dict has an **explicit** `target` re-registers the same + target and raises `KeyError: ` on unmount, leaving stale handlers. Omit `target` and + let ReactPy generate it (per element, via `key_path`). +- `Layout.deliver()` is for custom backends; app code dispatches nothing by hand. +- Patches **replace** subtrees and never merge; wrappers with `"tagName": ""` surround + components — never index the model by position. +- Nothing runs during a render for un-mounted components, and rendering may be discarded or + restarted; there is no ordering guarantee between components. +- Unmount/cleanup does not run while `Layout.render()` blocks with nothing pending. + +## 21. Executors and deployment + +All three live in `reactpy.executors.asgi`, all accept `**settings`, all are instantiated once +at module level and handed to an ASGI server. + +**`**settings`** are the `ReactPyConfig` keys — the `REACTPY_*` names, lowercased, no prefix: +`path_prefix`, `web_modules_dir`, `reconnect_interval`, `reconnect_max_interval`, +`reconnect_max_retries`, `reconnect_backoff_multiplier`, `async_rendering`, `debug`, +`max_queue_size`, `event_proxy_timeout`, `event_proxy_max_depth`, `event_proxy_max_timeouts` +(`tests_default_timeout` is env-only; the kwarg raises `TypeError` because that Option is not +mutable). An unknown key raises `ValueError: Unknown ReactPy setting "x".` +**These are process-wide globals**, not per-app config: each is written to the matching +`reactpy.config.REACTPY_*` option, so the last executor constructed wins for the whole process. + +| | `ReactPy` | `ReactPyMiddleware` | `ReactPyCsr` | +| --- | --- | --- | --- | +| Where Python runs | server | server | **browser** (PyScript/Pyodide) | +| Root given as | the `@component` **function** | **dotted-path strings** | a **file path** + `root_name` | +| Websocket | yes | yes | no | +| Component source visible to users | no | no | **yes** | +| Serves HTML | yes (empty shell) | **never** | yes | + +**`ReactPy(root_component, *, http_headers, html_head, prepend_body, html_lang, pyscript_setup, pyscript_options, **settings)`** +- Standalone single-page app. **It is not SSR of your component**: the index has an empty + `
` and all markup arrives over the websocket, so `curl` and search engines see + nothing. Only `html_head` and `prepend_body` are server-rendered. +- `html_head` **must** be built with `html.head(...)` → otherwise, at request time (500, not + import time): ``ValueError: Head element must be constructed with `html.head`.`` +- `prepend_body` defaults to `html.noscript("Enable JavaScript to view this site.")`; it is + rendered with `reactpy_to_string`, so it may not use hooks. +- `@my_app.route(path, type="http"|"websocket")` and `my_app.lifespan(app)` add your own routes + (path is a regex, `^`/`$` auto-added; the app may be a callable or a dotted-path string). +- `pyscript_setup=True` loads PyScript alongside server-rendered components for mixed setups. +- Serves `{path_prefix}` (index + dispatcher), `{path_prefix}static/` (client bundle), + `{path_prefix}modules/` (web modules). Everything else is yours. + +**`ReactPyMiddleware(app, root_components, **settings)`** +- `root_components` is a required iterable of **dotted paths** (`["main.counter"]`), resolved via + the Python import system from the server's working directory. Failures at construction: + `ImportError: ReactPy failed to import "main"` / `AttributeError: ReactPy failed to import + "counter" from "main"`; no dot at all → `ValueError`. +- It **never serves HTML**: registering a component does not create a page. Emit the mount + markup yourself with `server_side_component_html(element_id="app", class_="", + component_path="main.counter/")` — **the trailing slash is required** (the websocket route is + `^{path_prefix}(?P[a-zA-Z0-9_.]+)/$`). Without it: `Unexpected response code` + + `[ReactPy] Failed to connect!` and an empty div. +- Mounting by hand (`
Loading...
" + ) + + +class Backend: + def __init__(self) -> None: + self.components = {"counter": counter} + self.static_app = ServeStaticASGI( + self.not_found, root=STATIC_DIR, prefix=STATIC_PATH + ) + self.modules_app = ServeStaticASGI( + self.not_found, root=REACTPY_WEB_MODULES_DIR.current, prefix=MODULES_PATH + ) + + async def not_found(self, scope, receive, send) -> None: + await ResponseHTML("Resource not found on this server.", status_code=404)( + scope, receive, send + ) + + async def __call__(self, scope, receive, send) -> None: + path = scope["path"] + if scope["type"] == "http" and path.startswith(STATIC_PATH): + return await self.static_app(scope, receive, send) + if scope["type"] == "http" and path.startswith(MODULES_PATH): + return await self.modules_app(scope, receive, send) + if scope["type"] == "http" and path == "/": + return await ResponseHTML(index_html())(scope, receive, send) + if scope["type"] == "websocket" and path.startswith(PATH_PREFIX): + return await self.serve_component(scope, receive, send) + return await self.not_found(scope, receive, send) + + async def serve_component(self, scope, receive, send) -> None: + name = scope["path"][len(PATH_PREFIX) :].strip("/") + if name not in self.components: + raise RuntimeError(f"Attempting to use an unregistered root {name}.") + + await send({"type": "websocket.accept"}) + queue: asyncio.Queue = asyncio.Queue(1000) + + async def pump() -> None: + while True: + event = await receive() + if event["type"] == "websocket.disconnect": + return + if event["type"] == "websocket.connect": + continue + await queue.put(orjson.loads(event["text"])) + + async def recv(): + return await queue.get() + + async def send_model(message): + await send( + {"type": "websocket.send", "text": orjson.dumps(message).decode()} + ) + + task = asyncio.create_task(pump()) + connection = Connection( + scope=scope, + location=Location(path="/", query_string=""), + carrier=None, + ) + try: + await serve_layout( + Layout(ConnectionContext(self.components[name](), value=connection)), + send_model, + recv, + ) + finally: + task.cancel() + + +app = Backend() diff --git a/docs/examples/creating_html_tags/any_tag_name.py b/docs/examples/creating_html_tags/any_tag_name.py new file mode 100644 index 000000000..774344719 --- /dev/null +++ b/docs/examples/creating_html_tags/any_tag_name.py @@ -0,0 +1,10 @@ +from reactpy import html + +# start +# The tag name is whatever you ask for -- there is no allow-list to consult. +print(html.div({"id": "greeting"}, "Hello")) +# {'tagName': 'div', 'attributes': {'id': 'greeting'}, 'children': ['Hello']} + +print(html.definitely_not_a_real_tag("still renders")) +# {'tagName': 'definitely-not-a-real-tag', 'children': ['still renders']} +# end diff --git a/docs/examples/creating_html_tags/fragments.py b/docs/examples/creating_html_tags/fragments.py new file mode 100644 index 000000000..2743b388a --- /dev/null +++ b/docs/examples/creating_html_tags/fragments.py @@ -0,0 +1,19 @@ +from reactpy import html + +# start +# Fragments carry no tag of their own: tagName is the empty string. +print(html.fragment(html.b("bold"), html.i("italic"))) +# {'tagName': '', 'children': [{'tagName': 'b', ...}, {'tagName': 'i', ...}]} + +# html(...) is html itself: HtmlConstructor.__call__ is the fragment call. +print(html(html.b("bold"))["tagName"]) # '' +print(html()["tagName"]) # '' -- an empty fragment + +# A fragment may carry a key -- and nothing else. +print(html.fragment({"key": "row-1"}, html.b("x"))["attributes"]) # {'key': 'row-1'} + +try: + html.fragment({"className": "wrap"}, html.b("x")) +except TypeError as error: + print(error) # Fragments cannot have attributes besides 'key' +# end diff --git a/docs/examples/creating_html_tags/svg.py b/docs/examples/creating_html_tags/svg.py new file mode 100644 index 000000000..3abfcd526 --- /dev/null +++ b/docs/examples/creating_html_tags/svg.py @@ -0,0 +1,22 @@ +from reactpy import html + +# start +# SVG lives on a second constructor, so html.svg.circle() does not collide +# with a hypothetical html.circle(). +print( + html.svg( + {"width": "100", "height": "100", "viewBox": "0 0 100 100"}, + html.svg.circle({"cx": "50", "cy": "50", "r": "40", "fill": "tomato"}), + ) +) +# {'tagName': 'svg', 'attributes': {...}, +# 'children': [{'tagName': 'circle', 'attributes': {...}}]} +# end + +# Nested access needs the web-module constructor; plain html tags have no +# children to nest. +try: + nested = "span" + getattr(html.div, nested)("nope") +except AttributeError as error: + print(error) # Nested components can only be accessed on web module components. diff --git a/docs/examples/creating_html_tags/tag_name_translation.py b/docs/examples/creating_html_tags/tag_name_translation.py new file mode 100644 index 000000000..6d9b9e923 --- /dev/null +++ b/docs/examples/creating_html_tags/tag_name_translation.py @@ -0,0 +1,12 @@ +from reactpy import html + +# start +# Underscores become dashes, and a trailing underscore is stripped -- the name +# is translated, not looked up. +print(html.del_("removed")["tagName"]) # del +print(html.data_table()["tagName"]) # data-table +print(html.my_widget()["tagName"]) # my-widget + +# The same constructor comes back every time, so identity checks are cheap. +print(html.my_widget is html.my_widget) # True +# end diff --git a/docs/examples/creating_html_tags/two_call_shapes.py b/docs/examples/creating_html_tags/two_call_shapes.py new file mode 100644 index 000000000..670d8b09f --- /dev/null +++ b/docs/examples/creating_html_tags/two_call_shapes.py @@ -0,0 +1,15 @@ +from reactpy import html +from reactpy.types import VdomDict + +# start +# Two call shapes: children only, or an attribute dict first. +print(html.button("Click Me")) +# {'tagName': 'button', 'children': ['Click Me']} + +print(html.button({"id": "save", "disabled": True}, "Save")) +# {'tagName': 'button', 'attributes': {'id': 'save', 'disabled': True}, 'children': ['Save']} + +# Only an *exact* dict counts as attributes -- a dict subclass is a child. +print(html.div(VdomDict(tagName="span"))) +# {'tagName': 'div', 'children': [{'tagName': 'span'}]} +# end diff --git a/docs/examples/creating_html_tags/void_and_script.py b/docs/examples/creating_html_tags/void_and_script.py new file mode 100644 index 000000000..28d809843 --- /dev/null +++ b/docs/examples/creating_html_tags/void_and_script.py @@ -0,0 +1,29 @@ +from reactpy import html + +# start +# Void elements reject children -- loudly, at the call site. +try: + html.br("oops") +except TypeError as error: + print(error) # 'br' nodes cannot have children. + +print(html.br()) # {'tagName': 'br'} + +# script is stricter still: one string child, no handlers, an inferred key. +print(html.script("var answered = 42;")) +# {'tagName': 'script', 'children': ['var answered = 42;'], +# 'attributes': {'key': 'var answered = 42;'}} + +print(html.script({"src": "/chart.js"})) +# {'tagName': 'script', 'attributes': {'src': '/chart.js', 'key': '/chart.js'}} + + +async def handle_load(event): + pass + + +try: + html.script({"onLoad": handle_load}, "var x = 1;") +except ValueError as error: + print(error) # 'script' elements do not support event handlers +# end diff --git a/docs/examples/creating_vdom_event_handlers/argument_shape.py b/docs/examples/creating_vdom_event_handlers/argument_shape.py new file mode 100644 index 000000000..ae3b015ef --- /dev/null +++ b/docs/examples/creating_vdom_event_handlers/argument_shape.py @@ -0,0 +1,23 @@ +from reactpy import html +from reactpy.core.events import EventHandler + + +# start +# A plain coroutine gets the browser's argument list UNPACKED. +async def plain_handler(event): + print(await event.target.value) # the event itself + + +# An EventHandler gets the LIST ITSELF: an EventArguments, a list subclass. +async def list_handler(data): + print(await data[0].target.value) # index first, then read as usual + + +html.button({"onClick": plain_handler}, "a") +html.button({"onClick": EventHandler(list_handler, prevent_default=True)}, "b") + +# Hand the list to a body written for the event and the first read fails: +# AttributeError: 'EventArguments' object has no attribute 'target' +# Hand the event to a body written for the list and subscripting it fails: +# TypeError: 'Event' object is not subscriptable +# end diff --git a/docs/examples/creating_vdom_event_handlers/bound_method_trap.py b/docs/examples/creating_vdom_event_handlers/bound_method_trap.py new file mode 100644 index 000000000..dc1afa52e --- /dev/null +++ b/docs/examples/creating_vdom_event_handlers/bound_method_trap.py @@ -0,0 +1,25 @@ +from reactpy import event, html + + +# start +# A bound method is a fine handler -- it fires. What is lost is detection of +# the event.preventDefault() inside it: the bytecode inspector reads the +# *first* parameter as the event, and for a method that is `self`. +class SearchForm: + async def on_submit(self, event): + event.preventDefault() + + +plain = html.form({"onSubmit": SearchForm().on_submit})["eventHandlers"]["onSubmit"] +print(plain.prevent_default) # False -- your call was never seen + + +# Say it explicitly with @event and it works. +@event(prevent_default=True) +async def submit(event_data): + print("submitted") + + +explicit = html.form({"onSubmit": submit})["eventHandlers"]["onSubmit"] +print(explicit.prevent_default) # True +# end diff --git a/docs/examples/creating_vdom_event_handlers/bytecode_detection.py b/docs/examples/creating_vdom_event_handlers/bytecode_detection.py new file mode 100644 index 000000000..ba05b2e34 --- /dev/null +++ b/docs/examples/creating_vdom_event_handlers/bytecode_detection.py @@ -0,0 +1,16 @@ +from reactpy import html + + +# start +# Same behaviour, discovered by reading your bytecode instead of a decorator. +async def submit(event): + event.preventDefault() + event.stopPropagation() + event.debounce = 250 + + +handler = html.form({"onSubmit": submit}, "x")["eventHandlers"]["onSubmit"] +print(handler.prevent_default) # True +print(handler.stop_propagation) # True +print(handler.debounce) # 250 +# end diff --git a/docs/examples/creating_vdom_event_handlers/decorator_options.py b/docs/examples/creating_vdom_event_handlers/decorator_options.py new file mode 100644 index 000000000..e85d276f9 --- /dev/null +++ b/docs/examples/creating_vdom_event_handlers/decorator_options.py @@ -0,0 +1,12 @@ +from reactpy import event, html + + +# start +# The @event decorator states the options outright. +@event(prevent_default=True, stop_propagation=True, debounce=250, throttle=100) +async def handle_key_down(event_data): + print("key", await event_data.key) + + +html.input({"onKeyDown": handle_key_down}) +# end diff --git a/docs/examples/creating_vdom_event_handlers/three_buckets.py b/docs/examples/creating_vdom_event_handlers/three_buckets.py new file mode 100644 index 000000000..bfaec1082 --- /dev/null +++ b/docs/examples/creating_vdom_event_handlers/three_buckets.py @@ -0,0 +1,26 @@ +from reactpy import html + + +# start +# ReactPy sorts each value in the attribute dict into one of three buckets +# by *kind*, not by name (core/vdom.py, separate_attributes_handlers_and_inline_javascript). +async def save(event): + print("saved", await event.target.value) + + +element = html.button( + { + "id": "save", # str, not onXxx -> attribute + "onClick": save, # callable -> event handler + "onHover": "console.log('hover')", # str under onXxx -> inline JS + "data-row": "3", # str, not onXxx -> attribute + }, + "Save", +) + +print(sorted(element)) +# ['attributes', 'children', 'eventHandlers', 'inlineJavaScript', 'tagName'] +print(sorted(element["attributes"])) # ['data-row', 'id'] +print(sorted(element["eventHandlers"])) # ['onClick'] +print(sorted(element["inlineJavaScript"])) # ['onHover'] +# end diff --git a/docs/examples/extracting_state_logic_into_a_reducer/reducer_action_data/app.py b/docs/examples/extracting_state_logic_into_a_reducer/reducer_action_data/app.py new file mode 100644 index 000000000..04106ef1b --- /dev/null +++ b/docs/examples/extracting_state_logic_into_a_reducer/reducer_action_data/app.py @@ -0,0 +1,53 @@ +"""The fix: a module-level reducer that reads everything from the action.""" + +from reactpy import component, html, use_reducer, use_state + +# start +INITIAL = {"count": 0, "total": 0} + + +def cart_reducer(state, action): + """Pure and self-contained: every value it needs arrives in the action.""" + if action["type"] == "add": + return { + "count": state["count"] + 1, + "total": state["total"] + action["price"], + } + return state + + +@component +def cart(item_price): + # Defined at module level, so there is no enclosing scope to freeze. + state, dispatch = use_reducer(cart_reducer, INITIAL) + + async def handle_add(event): + # The handler closes over the *current* price, and hands it over. + dispatch({"type": "add", "price": item_price}) + + return html.div( + html.button({"onClick": handle_add}, "Add"), + html.p( + {"id": "cart"}, + f"count={state['count']} total={state['total']} price={item_price}", + ), + ) + + +@component +def price_control(): + price, set_price = use_state(10) + + async def raise_price(event): + set_price(50) + + return html.div( + html.button({"onClick": raise_price}, "Raise price"), + cart(price), + html.p(f"parent price={price}"), + ) + + +@component +def app(): + return price_control() diff --git a/docs/examples/extracting_state_logic_into_a_reducer/reducer_frozen_value/app.py b/docs/examples/extracting_state_logic_into_a_reducer/reducer_frozen_value/app.py new file mode 100644 index 000000000..7b0a7d2bc --- /dev/null +++ b/docs/examples/extracting_state_logic_into_a_reducer/reducer_frozen_value/app.py @@ -0,0 +1,51 @@ +"""A reducer closes over a value from the component body — and keeps the first one.""" + +from reactpy import component, html, use_reducer, use_state + +# start +INITIAL = {"count": 0, "total": 0} + + +@component +def cart(item_price): + # This reducer reads item_price from the enclosing scope. ReactPy keeps the + # reducer from the first render, so it keeps the first price too. + def reducer(state, action): + if action["type"] == "add": + return { + "count": state["count"] + 1, + "total": state["total"] + item_price, + } + return state + + state, dispatch = use_reducer(reducer, INITIAL) + + async def handle_add(event): + dispatch({"type": "add"}) + + return html.div( + html.button({"onClick": handle_add}, "Add"), + html.p( + {"id": "cart"}, + f"count={state['count']} total={state['total']} price={item_price}", + ), + ) + + +@component +def price_control(): + price, set_price = use_state(10) + + async def raise_price(event): + set_price(50) + + return html.div( + html.button({"onClick": raise_price}, "Raise price"), + cart(price), + html.p(f"parent price={price}"), + ) + + +@component +def app(): + return price_control() diff --git a/docs/examples/extracting_state_logic_into_a_reducer/reducer_mutates/app.py b/docs/examples/extracting_state_logic_into_a_reducer/reducer_mutates/app.py new file mode 100644 index 000000000..7ad16658d --- /dev/null +++ b/docs/examples/extracting_state_logic_into_a_reducer/reducer_mutates/app.py @@ -0,0 +1,44 @@ +"""A reducer that mutates: ReactPy never re-renders, and never warns.""" + +from reactpy import component, html, use_reducer + +# start +INITIAL_TASKS = [ + {"id": 0, "text": "Packing", "done": False}, + {"id": 1, "text": "Passport", "done": True}, +] + + +def mutating_reducer(state, action): + """Looks fine, edits the list in place, returns it. Nothing will re-render.""" + if action["type"] == "added": + state["tasks"].append( + {"id": state["next_id"], "text": "New task", "done": False} + ) + return state + return state + + +@component +def task_app(): + # A fresh list per component instance: this reducer mutates the list it is + # given, so it must not be handed a module-level one. + state, dispatch = use_reducer( + mutating_reducer, {"tasks": [*INITIAL_TASKS], "next_id": 2} + ) + + async def handle_add_task(event): + dispatch({"type": "added"}) + + return html.div( + html.button({"onClick": handle_add_task}, "Add task"), + html.ul( + [html.li({"key": task["id"]}, task["text"]) for task in state["tasks"]] + ), + html.p({"id": "summary"}, f"{len(state['tasks'])} tasks rendered"), + ) + + +@component +def app(): + return task_app() diff --git a/docs/examples/extracting_state_logic_into_a_reducer/reducer_swapped/app.py b/docs/examples/extracting_state_logic_into_a_reducer/reducer_swapped/app.py new file mode 100644 index 000000000..f3f8033eb --- /dev/null +++ b/docs/examples/extracting_state_logic_into_a_reducer/reducer_swapped/app.py @@ -0,0 +1,39 @@ +"""Swapping the reducer function at runtime does nothing — the first one is kept.""" + +from reactpy import component, html, use_reducer, use_state + + +# start +def add_by_one(state, action): + return {"n": state["n"] + 1, "by": "add_by_one"} + + +def add_by_hundred(state, action): + return {"n": state["n"] + 100, "by": "add_by_hundred"} + + +@component +def counter(): + version, set_version = use_state(1) + + # The component body picks a different reducer each time version changes, + # but use_reducer built its dispatcher on the first render and reuses it. + reducer = add_by_one if version == 1 else add_by_hundred + state, dispatch = use_reducer(reducer, {"n": 0, "by": "-"}) + + async def handle_add(event): + dispatch({"type": "add"}) + + async def handle_switch(event): + set_version(2) + + return html.div( + html.button({"onClick": handle_add}, "Add"), + html.button({"onClick": handle_switch}, "Switch reducer"), + html.p({"id": "readout"}, f"n={state['n']} by={state['by']} version={version}"), + ) + + +@component +def app(): + return counter() diff --git a/docs/examples/extracting_state_logic_into_a_reducer/tasks_reducer/app.py b/docs/examples/extracting_state_logic_into_a_reducer/tasks_reducer/app.py new file mode 100644 index 000000000..49a7fd9b7 --- /dev/null +++ b/docs/examples/extracting_state_logic_into_a_reducer/tasks_reducer/app.py @@ -0,0 +1,94 @@ +"""The same three handlers, now describing what the user did.""" + +from reactpy import component, html, use_reducer + +# start +INITIAL_TASKS = [ + {"id": 0, "text": "Packing", "done": False}, + {"id": 1, "text": "Passport", "done": True}, + {"id": 2, "text": "Camera", "done": False}, +] + + +def tasks_reducer(state, action): + """One place that knows how the task list may change.""" + kind = action["type"] + if kind == "added": + task = {"id": state["next_id"], "text": "New task", "done": False} + return { + **state, + "tasks": [*state["tasks"], task], + "next_id": state["next_id"] + 1, + } + if kind == "toggled": + return { + **state, + "tasks": [ + ( + {**task, "done": not task["done"]} + if task["id"] == action["id"] + else task + ) + for task in state["tasks"] + ], + } + if kind == "deleted": + return { + **state, + "tasks": [task for task in state["tasks"] if task["id"] != action["id"]], + } + raise ValueError(f"unknown action type {kind!r}") + + +@component +def task_app(): + state, dispatch = use_reducer(tasks_reducer, {"tasks": INITIAL_TASKS, "next_id": 3}) + + async def handle_add_task(event): + dispatch({"type": "added"}) + + def handle_toggle(task_id): + async def handle_click(event): + dispatch({"type": "toggled", "id": task_id}) + + return handle_click + + def handle_delete(task_id): + async def handle_click(event): + dispatch({"type": "deleted", "id": task_id}) + + return handle_click + + return html.div( + html.h3("Prague itinerary"), + html.button({"onClick": handle_add_task}, "Add task"), + html.ul( + [ + html.li( + {"key": task["id"]}, + html.span( + { + "style": { + "textDecoration": ( + "line-through" if task["done"] else "none" + ) + } + }, + task["text"], + ), + html.button({"onClick": handle_toggle(task["id"])}, "Toggle"), + html.button({"onClick": handle_delete(task["id"])}, "Delete"), + ) + for task in state["tasks"] + ] + ), + html.p( + {"id": "summary"}, + f"{len(state['tasks'])} tasks, next id {state['next_id']}", + ), + ) + + +@component +def app(): + return task_app() diff --git a/docs/examples/extracting_state_logic_into_a_reducer/tasks_state/app.py b/docs/examples/extracting_state_logic_into_a_reducer/tasks_state/app.py new file mode 100644 index 000000000..8986f50c3 --- /dev/null +++ b/docs/examples/extracting_state_logic_into_a_reducer/tasks_state/app.py @@ -0,0 +1,72 @@ +"""Three handlers that each rebuild the task list themselves.""" + +from reactpy import component, html, use_state + +# start +INITIAL_TASKS = [ + {"id": 0, "text": "Packing", "done": False}, + {"id": 1, "text": "Passport", "done": True}, + {"id": 2, "text": "Camera", "done": False}, +] + + +@component +def task_app(): + next_id, set_next_id = use_state(3) + tasks, set_tasks = use_state(INITIAL_TASKS) + + async def handle_add_task(event): + set_tasks([*tasks, {"id": next_id, "text": "New task", "done": False}]) + set_next_id(next_id + 1) + + def handle_toggle(task_id): + async def handle_click(event): + set_tasks( + [ + ( + {**task, "done": not task["done"]} + if task["id"] == task_id + else task + ) + for task in tasks + ] + ) + + return handle_click + + def handle_delete(task_id): + async def handle_click(event): + set_tasks([task for task in tasks if task["id"] != task_id]) + + return handle_click + + return html.div( + html.h3("Prague itinerary"), + html.button({"onClick": handle_add_task}, "Add task"), + html.ul( + [ + html.li( + {"key": task["id"]}, + html.span( + { + "style": { + "textDecoration": ( + "line-through" if task["done"] else "none" + ) + } + }, + task["text"], + ), + html.button({"onClick": handle_toggle(task["id"])}, "Toggle"), + html.button({"onClick": handle_delete(task["id"])}, "Delete"), + ) + for task in tasks + ] + ), + html.p({"id": "summary"}, f"{len(tasks)} tasks, next id {next_id}"), + ) + + +@component +def app(): + return task_app() diff --git a/docs/examples/importing_and_exporting_components/one_file/app.py b/docs/examples/importing_and_exporting_components/one_file/app.py new file mode 100644 index 000000000..ed6278d19 --- /dev/null +++ b/docs/examples/importing_and_exporting_components/one_file/app.py @@ -0,0 +1,18 @@ +from reactpy import component, html + + +@component +def profile(): + return html.img( + {"src": "../../assets/images/demos/MK3eW3As.jpg", "alt": "Katherine Johnson"} + ) + + +@component +def gallery(): + return html.section( + html.h1("Amazing scientists"), + profile(), + profile(), + profile(), + ) diff --git a/docs/examples/importing_and_exporting_components/private/app.py b/docs/examples/importing_and_exporting_components/private/app.py new file mode 100644 index 000000000..6b2c9380b --- /dev/null +++ b/docs/examples/importing_and_exporting_components/private/app.py @@ -0,0 +1,8 @@ +from gallery import gallery + +from reactpy import component + + +@component +def app(): + return gallery() diff --git a/docs/examples/importing_and_exporting_components/private/gallery.py b/docs/examples/importing_and_exporting_components/private/gallery.py new file mode 100644 index 000000000..2a56c9f2d --- /dev/null +++ b/docs/examples/importing_and_exporting_components/private/gallery.py @@ -0,0 +1,21 @@ +from reactpy import component, html + +__all__ = ["gallery"] + + +@component +def _profile(): + """Used only inside this module, so it stays out of ``__all__``.""" + return html.img( + {"src": "../../assets/images/demos/QIrZWGIs.jpg", "alt": "Alan L. Hart"} + ) + + +@component +def gallery(): + return html.section( + html.h1("Amazing scientists"), + _profile(), + _profile(), + _profile(), + ) diff --git a/docs/examples/importing_and_exporting_components/public/app.py b/docs/examples/importing_and_exporting_components/public/app.py new file mode 100644 index 000000000..073bd4d4c --- /dev/null +++ b/docs/examples/importing_and_exporting_components/public/app.py @@ -0,0 +1,8 @@ +from gallery import gallery, profile + +from reactpy import component, html + + +@component +def app(): + return html.div(profile(), gallery()) diff --git a/docs/examples/importing_and_exporting_components/public/gallery.py b/docs/examples/importing_and_exporting_components/public/gallery.py new file mode 100644 index 000000000..2a5b12eb7 --- /dev/null +++ b/docs/examples/importing_and_exporting_components/public/gallery.py @@ -0,0 +1,20 @@ +from reactpy import component, html + +__all__ = ["gallery", "profile"] + + +@component +def profile(): + return html.img( + {"src": "../../assets/images/demos/QIrZWGIs.jpg", "alt": "Alan L. Hart"} + ) + + +@component +def gallery(): + return html.section( + html.h1("Amazing scientists"), + profile(), + profile(), + profile(), + ) diff --git a/docs/examples/importing_and_exporting_components/styles.css b/docs/examples/importing_and_exporting_components/styles.css new file mode 100644 index 000000000..e2200ddd7 --- /dev/null +++ b/docs/examples/importing_and_exporting_components/styles.css @@ -0,0 +1,4 @@ +img { + margin: 0 10px 10px 0; + height: 90px; +} diff --git a/docs/examples/keeping_components_pure/fixed/app.py b/docs/examples/keeping_components_pure/fixed/app.py new file mode 100644 index 000000000..94e2c1bfa --- /dev/null +++ b/docs/examples/keeping_components_pure/fixed/app.py @@ -0,0 +1,12 @@ +from reactpy import component, html + + +# start +@component +def cup(guest): + return html.h2(f"Tea cup for guest #{guest}") + + +@component +def tea_set(): + return html(cup(1), cup(2), cup(3)) diff --git a/docs/examples/keeping_components_pure/impure/app.py b/docs/examples/keeping_components_pure/impure/app.py new file mode 100644 index 000000000..c2b5c48cb --- /dev/null +++ b/docs/examples/keeping_components_pure/impure/app.py @@ -0,0 +1,17 @@ +from reactpy import component, html + +# start +guest = 0 + + +@component +def cup(): + # Bad: changing a preexisting variable! + global guest + guest = guest + 1 + return html.h2(f"Tea cup for guest #{guest}") + + +@component +def tea_set(): + return html(cup(), cup(), cup()) diff --git a/docs/examples/keeping_components_pure/local_mutation/app.py b/docs/examples/keeping_components_pure/local_mutation/app.py new file mode 100644 index 000000000..ae23e9e7e --- /dev/null +++ b/docs/examples/keeping_components_pure/local_mutation/app.py @@ -0,0 +1,15 @@ +from reactpy import component, html + + +# start +@component +def cup(guest): + return html.h2(f"Tea cup for guest #{guest}") + + +@component +def tea_gathering(): + cups = [] + for index in range(1, 13): + cups.append(cup(index, key=index)) + return html.div(cups) diff --git a/docs/examples/keeping_components_pure/recipe/app.py b/docs/examples/keeping_components_pure/recipe/app.py new file mode 100644 index 000000000..8e4dc73fd --- /dev/null +++ b/docs/examples/keeping_components_pure/recipe/app.py @@ -0,0 +1,22 @@ +from reactpy import component, html + + +# start +@component +def recipe(drinkers): + return html.ol( + html.li(f"Boil {drinkers} cups of water."), + html.li(f"Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice."), + html.li(f"Add {0.5 * drinkers} cups of milk to boil and sugar to taste."), + ) + + +@component +def app(): + return html.section( + html.h1("Spiced Chai Recipe"), + html.h2("For two"), + recipe(2), + html.h2("For a gathering"), + recipe(4), + ) diff --git a/docs/examples/keeping_components_pure/side_effects/app.py b/docs/examples/keeping_components_pure/side_effects/app.py new file mode 100644 index 000000000..53d5ea336 --- /dev/null +++ b/docs/examples/keeping_components_pure/side_effects/app.py @@ -0,0 +1,16 @@ +from reactpy import component, html, use_state + + +# start +@component +def tea_counter(): + sips, set_sips = use_state(0) + + async def take_a_sip(event): + # Side effects belong here: this runs on click, not during rendering. + set_sips(sips + 1) + + return html.div( + html.h1(f"You have taken {sips} sips."), + html.button({"onClick": take_a_sip}, "Take a sip"), + ) diff --git a/docs/examples/lifecycle_of_reactive_effects/blocking_effect/app.py b/docs/examples/lifecycle_of_reactive_effects/blocking_effect/app.py new file mode 100644 index 000000000..dd8dc0f6e --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/blocking_effect/app.py @@ -0,0 +1,30 @@ +"""A sync Effect blocks the connection's event loop, like any sync work.""" + +import time + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def preview(): + width, set_width = use_state(100) + + def effect(): + # Stands in for image encoding, a model call, a blocking driver. + # Effects run on the connection's own event loop, so while this runs + # nothing else on the connection can: no other render, no other + # handler. Only the browser's local typing stays responsive. + started = time.perf_counter() + time.sleep(0.4) + print(f"effect held the loop for {time.perf_counter() - started:.2f}s") + + use_effect(effect, dependencies=[width]) + + async def handle_width_click(event): + set_width(width + 1) + + return html.div( + html.h1(f"width {width}"), + html.button({"onClick": handle_width_click}, "Wider"), + ) diff --git a/docs/examples/lifecycle_of_reactive_effects/frozen_body/app.py b/docs/examples/lifecycle_of_reactive_effects/frozen_body/app.py new file mode 100644 index 000000000..e064bcf3f --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/frozen_body/app.py @@ -0,0 +1,41 @@ +"""A local function as a dependency never counts as changed.""" + +from data import log + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def counter(): + count, set_count = use_state(0) + label, set_label = use_state("count") + runs, set_runs = use_state([]) + + def describe(): + # A fresh function object on every render -- but ReactPy compares two + # such local functions by their *code*, not by identity + # (core/hooks.py, strictly_equal). So `describe` never counts as + # changed, and the Effect below keeps the closure it captured first. + return f"{label}={count}" + + def effect(): + line = f"effect ran, describe() -> {describe()}" + log.append(line) + set_runs(lambda previous: [*previous, line]) + + use_effect(effect, dependencies=[describe]) + + async def handle_count_click(event): + set_count(count + 1) + + async def handle_label_click(event): + set_label(label + "!") + + return html.div( + html.h1(f"{label}: {count}"), + html.button({"onClick": handle_count_click}, "Add 1"), + html.button({"onClick": handle_label_click}, "Rename"), + html.p("Effect runs:"), + html.ul([html.li({"key": index}, line) for index, line in enumerate(runs)]), + ) diff --git a/docs/examples/lifecycle_of_reactive_effects/frozen_body/data.py b/docs/examples/lifecycle_of_reactive_effects/frozen_body/data.py new file mode 100644 index 000000000..d8d57be6e --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/frozen_body/data.py @@ -0,0 +1,3 @@ +"""Records each Effect run, so the page can show what actually executed.""" + +log = [] diff --git a/docs/examples/lifecycle_of_reactive_effects/gating/app.py b/docs/examples/lifecycle_of_reactive_effects/gating/app.py new file mode 100644 index 000000000..d7b7b8a88 --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/gating/app.py @@ -0,0 +1,48 @@ +"""Gating a subscription: pass it as a prop, do not delete the hook call.""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def feed(topic, enabled): + def effect(): + if not enabled: + # Nothing to set up, and nothing to clean up. The hook call below + # still happens, so the hook order never changes. + return None + + print(f"subscribe {topic}") + + def cleanup(): + print(f"unsubscribe {topic}") + + return cleanup + + use_effect(effect, dependencies=[topic, enabled]) + + return html.p(f"{topic}") + + +@component +def app(): + topic, set_topic = use_state("news") + enabled, set_enabled = use_state(True) + + async def handle_enabled_click(event): + set_enabled(not enabled) + + async def handle_topic_click(event): + set_topic("sports" if topic == "news" else "news") + + return html.div( + html.button( + {"onClick": handle_enabled_click}, + "Stop" if enabled else "Start", + ), + html.button( + {"onClick": handle_topic_click}, + "Switch topic", + ), + feed(topic, enabled), + ) diff --git a/docs/examples/lifecycle_of_reactive_effects/ordering/app.py b/docs/examples/lifecycle_of_reactive_effects/ordering/app.py new file mode 100644 index 000000000..dde68eaba --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/ordering/app.py @@ -0,0 +1,35 @@ +"""Effect order: children first, siblings in reverse, parent last.""" + +from reactpy import component, html, use_effect + + +# start +@component +def leaf(name): + @use_effect + def first(): + print(f"{name}: first effect") + + @use_effect + def second(): + print(f"{name}: second effect") + + return html.p(name) + + +@component +def panel(): + @use_effect + def parent_effect(): + print("panel: my own effect") + + # Two children: the SECOND one's effects run first. + return html.div(leaf("alpha"), leaf("beta")) + + +@component +def app(): + return html.div( + html.h1("Watch the server console"), + panel(), + ) diff --git a/docs/examples/lifecycle_of_reactive_effects/overlapping_runs/app.py b/docs/examples/lifecycle_of_reactive_effects/overlapping_runs/app.py new file mode 100644 index 000000000..0c0e902a0 --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/overlapping_runs/app.py @@ -0,0 +1,41 @@ +"""Two runs of one async Effect can overlap; cancellation is not instant.""" + +import asyncio + +from reactpy import component, html, use_async_effect, use_state + + +# start +@component +def poller(topic): + async def effect(): + # A real request would go here. ReactPy cancels this task when `topic` + # changes, but cancellation only takes effect at an await -- and code + # that already ran cannot be un-run. + print(f"start polling {topic}") + try: + await asyncio.sleep(0.4) + print(f"finished polling {topic}") + except asyncio.CancelledError: + print(f"cancelled {topic}") + raise + + use_async_effect(effect, dependencies=[topic]) + + return html.p(f"polling {topic}") + + +@component +def app(): + topic, set_topic = use_state("news") + + async def handle_topic_click(event): + set_topic("sports" if topic == "news" else "news") + + return html.div( + html.button( + {"onClick": handle_topic_click}, + "Switch topic", + ), + poller(topic), + ) diff --git a/docs/examples/lifecycle_of_reactive_effects/reactive_values/app.py b/docs/examples/lifecycle_of_reactive_effects/reactive_values/app.py new file mode 100644 index 000000000..d4ff3f46b --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/reactive_values/app.py @@ -0,0 +1,40 @@ +"""A value read by the Effect but missing from its dependencies goes stale.""" + +from data import log + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def counter(): + count, set_count = use_state(0) + show_error, set_show_error = use_state(True) + reports, set_reports = use_state([]) + + def effect(): + # Reads `show_error`, but dependencies=[count] never mentions it. So + # flipping show_error does not re-synchronize, and this keeps + # reporting whatever show_error was when the Effect was created. + line = f"count={count} reported_error={show_error}" + log.append(line) + set_reports(lambda previous: [*previous, line]) + + use_effect(effect, dependencies=[count]) + + async def handle_count_click(event): + set_count(count + 1) + + async def handle_show_error_click(event): + set_show_error(not show_error) + + return html.div( + html.h1(count), + html.button({"onClick": handle_count_click}, "Add 1"), + html.button( + {"onClick": handle_show_error_click}, + f"Toggle error (currently {show_error})", + ), + html.p("Effect reports:"), + html.ul([html.li({"key": index}, line) for index, line in enumerate(reports)]), + ) diff --git a/docs/examples/lifecycle_of_reactive_effects/reactive_values/data.py b/docs/examples/lifecycle_of_reactive_effects/reactive_values/data.py new file mode 100644 index 000000000..edf782f40 --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/reactive_values/data.py @@ -0,0 +1,3 @@ +"""Records what each Effect run actually observed.""" + +log = [] diff --git a/docs/examples/lifecycle_of_reactive_effects/setup_cleanup/app.py b/docs/examples/lifecycle_of_reactive_effects/setup_cleanup/app.py new file mode 100644 index 000000000..fba16abca --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/setup_cleanup/app.py @@ -0,0 +1,42 @@ +"""The setup/cleanup pair, and what a re-synchronization looks like.""" + +from data import server + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def room_view(room): + def effect(): + # Setup: ReactPy runs this when the Effect is created. + close = server.connect(room) + + # Clean-up: ReactPy runs this before the *next* run of this same + # Effect, and again when the component unmounts. Together they make + # one synchronization: connect this room, then leave it. + def cleanup(): + close() + + return cleanup + + use_effect(effect, dependencies=[room]) + + return html.p(f"{room} -- open connections: {server.open_count}") + + +@component +def app(): + room, set_room = use_state("lobby") + + async def handle_room_click(event): + set_room("dev" if room == "lobby" else "lobby") + + return html.div( + html.button( + {"onClick": handle_room_click}, + f"Leave {room}", + ), + html.p("(server log is printed on the server console)"), + room_view(room), + ) diff --git a/docs/examples/lifecycle_of_reactive_effects/setup_cleanup/data.py b/docs/examples/lifecycle_of_reactive_effects/setup_cleanup/data.py new file mode 100644 index 000000000..2e973f89c --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/setup_cleanup/data.py @@ -0,0 +1,20 @@ +"""A connection pool the setup/cleanup example can report on.""" + + +class Server: + def __init__(self): + self.log = [] + self.open_count = 0 + + def connect(self, room): + self.open_count += 1 + self.log.append(f"connect {room}") + + def close(): + self.open_count -= 1 + self.log.append(f"close {room}") + + return close + + +server = Server() diff --git a/docs/examples/lifecycle_of_reactive_effects/silent_failure/app.py b/docs/examples/lifecycle_of_reactive_effects/silent_failure/app.py new file mode 100644 index 000000000..ff450e9b3 --- /dev/null +++ b/docs/examples/lifecycle_of_reactive_effects/silent_failure/app.py @@ -0,0 +1,29 @@ +"""A failing Effect is invisible in the page and invisible in the log.""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def dashboard(): + refreshes, set_refreshes = use_state(0) + + def effect(): + # Suppose this reads a metric from a database and the query fails. + # + # The page does not change, the browser console stays clean, and no + # error reaches the model. The ONLY trace is a traceback on the + # server -- and, as written here, it does not even appear until the + # component unmounts. Test your Effects; the UI will not report them. + msg = "metric query failed" + raise RuntimeError(msg) + + use_effect(effect, dependencies=[refreshes]) + + async def handle_refreshes_click(event): + set_refreshes(refreshes + 1) + + return html.div( + html.h1("Dashboard looks fine"), + html.button({"onClick": handle_refreshes_click}, "Refresh"), + ) diff --git a/docs/examples/manipulating_the_dom_with_refs/focus_on_click/app.py b/docs/examples/manipulating_the_dom_with_refs/focus_on_click/app.py new file mode 100644 index 000000000..3e3d4c87c --- /dev/null +++ b/docs/examples/manipulating_the_dom_with_refs/focus_on_click/app.py @@ -0,0 +1,17 @@ +from reactpy import component, html + + +# start +@component +def search_form(): + return html.form( + html.input({"id": "search", "type": "search"}), + # A string in a handler slot runs in the browser (see Responding to + # Events), which is the only way to reach an element by id. + html.button( + { + "onClick": "document.getElementById('search').focus()", + }, + "Focus the input", + ), + ) diff --git a/docs/examples/manipulating_the_dom_with_refs/focus_on_mount/app.py b/docs/examples/manipulating_the_dom_with_refs/focus_on_mount/app.py new file mode 100644 index 000000000..66af8e9f3 --- /dev/null +++ b/docs/examples/manipulating_the_dom_with_refs/focus_on_mount/app.py @@ -0,0 +1,10 @@ +from reactpy import component, html + + +# start +@component +def search_form(): + return html.form( + html.label({"htmlFor": "search"}, "Search"), + html.input({"id": "search", "type": "search", "autoFocus": True}), + ) diff --git a/docs/examples/manipulating_the_dom_with_refs/scroll_to_item/app.py b/docs/examples/manipulating_the_dom_with_refs/scroll_to_item/app.py new file mode 100644 index 000000000..9c651f516 --- /dev/null +++ b/docs/examples/manipulating_the_dom_with_refs/scroll_to_item/app.py @@ -0,0 +1,34 @@ +from data import friends, get_image_url + +from reactpy import component, html + + +# start +@component +def cat_friends(): + return html( + html.div( + {"id": "carousel", "className": "carousel"}, + [ + html.li( + {"key": person["id"], "id": f"friend-{person['id']}"}, + html.img({"src": get_image_url(person), "alt": person["name"]}), + ) + for person in friends + ], + ), + html.div( + {"className": "buttons"}, + [ + html.button( + { + "key": person["id"], + "onClick": f"document.getElementById('friend-{person['id']}')" + ".scrollIntoView({behavior: 'smooth', inline: 'center'})", + }, + person["name"].split()[0], + ) + for person in friends + ], + ), + ) diff --git a/docs/examples/manipulating_the_dom_with_refs/scroll_to_item/data.py b/docs/examples/manipulating_the_dom_with_refs/scroll_to_item/data.py new file mode 100644 index 000000000..bd6b76af5 --- /dev/null +++ b/docs/examples/manipulating_the_dom_with_refs/scroll_to_item/data.py @@ -0,0 +1,21 @@ +friends = [ + { + "id": 0, + "name": "Creola Katherine Johnson", + "image_id": "MK3eW3A", + }, + { + "id": 1, + "name": "Mario José Molina-Pasquel Henríquez", + "image_id": "mynHUSa", + }, + { + "id": 2, + "name": "Mohammad Abdus Salam", + "image_id": "bE7W1ji", + }, +] + + +def get_image_url(person): + return f"../../assets/images/demos/{person['image_id']}.jpg" diff --git a/docs/examples/manipulating_the_dom_with_refs/scroll_to_item/styles.css b/docs/examples/manipulating_the_dom_with_refs/scroll_to_item/styles.css new file mode 100644 index 000000000..3d0a2174a --- /dev/null +++ b/docs/examples/manipulating_the_dom_with_refs/scroll_to_item/styles.css @@ -0,0 +1,27 @@ +.carousel { + display: flex; + gap: 10px; + overflow-x: auto; + scroll-snap-type: x mandatory; + padding: 10px 0; +} + +.carousel li { + list-style: none; + flex: 0 0 auto; + scroll-snap-align: center; +} + +.carousel img { + width: 130px; + height: 130px; + border-radius: 100%; + object-fit: cover; + display: block; +} + +.buttons { + display: flex; + gap: 10px; + margin-top: 10px; +} diff --git a/docs/examples/manipulating_the_dom_with_refs/toggle_visibility/app.py b/docs/examples/manipulating_the_dom_with_refs/toggle_visibility/app.py new file mode 100644 index 000000000..31fd7ea18 --- /dev/null +++ b/docs/examples/manipulating_the_dom_with_refs/toggle_visibility/app.py @@ -0,0 +1,22 @@ +from reactpy import component, html, use_state + + +# start +@component +def todo_list(): + shown, set_shown = use_state(True) + + async def handle_shown_click(event): + set_shown(not shown) + + return html( + html.div( + {"id": "todo", "hidden": not shown}, + html.p("Learn ReactPy"), + html.p("Build something"), + ), + html.button( + {"onClick": handle_shown_click}, + "Hide" if shown else "Show", + ), + ) diff --git a/docs/examples/manually_register_a_client/badge_module/badge.js b/docs/examples/manually_register_a_client/badge_module/badge.js new file mode 100644 index 000000000..b3bf72492 --- /dev/null +++ b/docs/examples/manually_register_a_client/badge_module/badge.js @@ -0,0 +1,31 @@ +// The bind() export tells the client how to put nodes on screen. It receives +// the container element the client owns and a config object carrying +// sendMessage/onMessage, and must hand back create/render/unmount. +export function bind(node, config) { + return { + create: (component, props, children) => { + const el = document.createElement("span"); + el.className = props.tone || "default"; + el.textContent = props.label + ": " + (children || []).join(""); + el.addEventListener("click", () => { + if (props.onPick) { + props.onPick({ label: props.label }); + } + }); + node.appendChild(el); + return el; + }, + render: (el) => { + node.innerHTML = ""; + node.appendChild(el); + }, + unmount: () => { + node.innerHTML = ""; + }, + }; +} + +// One export per component you intend to register by name. +export function Badge(props, children) { + return { props, children }; +} diff --git a/docs/examples/manually_register_a_client/badge_module/main.py b/docs/examples/manually_register_a_client/badge_module/main.py new file mode 100644 index 000000000..8e80fa03f --- /dev/null +++ b/docs/examples/manually_register_a_client/badge_module/main.py @@ -0,0 +1,25 @@ +from pathlib import Path + +from reactpy import component, html +from reactpy.reactjs import component_from_file + +# start +HERE = Path(__file__).parent + +Badge = component_from_file( + HERE / "badge.js", + "Badge", + name="badge", + fallback=html.p("loading badge..."), +) + + +@component +def product_card(title, tone): + return html.div( + html.h2(title), + Badge({"label": "in stock", "tone": tone}, "ready to ship"), + ) + + +# end diff --git a/docs/examples/passing_data_deeply_with_context/context_default/app.py b/docs/examples/passing_data_deeply_with_context/context_default/app.py new file mode 100644 index 000000000..ce9789d72 --- /dev/null +++ b/docs/examples/passing_data_deeply_with_context/context_default/app.py @@ -0,0 +1,19 @@ +"""No provider anywhere: every consumer reads the default.""" + +from reactpy import component, create_context, html, use_context + +# start +ThemeContext = create_context("light") + + +@component +def label(text): + theme = use_context(ThemeContext) + return html.p({"id": f"label-{text}"}, f"{text}: theme={theme}") + + +@component +def app(): + # Nothing wraps these in ThemeContext, so use_context falls back to the + # value passed to create_context. + return html.div(html.h3("No provider"), label("a"), label("b")) diff --git a/docs/examples/passing_data_deeply_with_context/context_nesting/app.py b/docs/examples/passing_data_deeply_with_context/context_nesting/app.py new file mode 100644 index 000000000..feee2100e --- /dev/null +++ b/docs/examples/passing_data_deeply_with_context/context_nesting/app.py @@ -0,0 +1,43 @@ +"""The nearest provider wins, and a provider only covers its own children.""" + +from reactpy import component, create_context, html, use_context + +ThemeContext = create_context("light") + + +# start +@component +def deep_label(tag): + # Three levels down, still no prop in sight. + return html.p({"id": tag}, f"{tag} sees: {use_context(ThemeContext)}") + + +@component +def middle(): + return html.div(html.h4("middle"), deep_label("deep")) + + +@component +def wrapper(): + # An intermediate component that neither receives nor forwards a theme. + return html.div(html.h4("wrapper"), middle()) + + +@component +def inner_scope(): + return html.div( + deep_label("inner"), + # A provider nested inside another one shadows it for its subtree. + ThemeContext(deep_label("deepest"), value="innermost"), + ) + + +@component +def app(): + return html.div( + # Same context, several providers, several scopes. + deep_label("outside"), + ThemeContext(wrapper(), value="outer"), + ThemeContext(inner_scope(), value="middle-provider"), + deep_label("after"), + ) diff --git a/docs/examples/passing_data_deeply_with_context/context_theme/app.py b/docs/examples/passing_data_deeply_with_context/context_theme/app.py new file mode 100644 index 000000000..533c94feb --- /dev/null +++ b/docs/examples/passing_data_deeply_with_context/context_theme/app.py @@ -0,0 +1,52 @@ +"""The same four levels, with the theme read from context instead.""" + +from reactpy import component, create_context, html, use_context, use_state + +PLACES = ["Almaty", "Astana", "Shymkent", "Karaganda", "Turkestan"] + +# start +ThemeContext = create_context("light") + + +@component +def place_label(place): + # Reads the theme directly: no prop had to travel here. + theme = use_context(ThemeContext) + color = "coral" if theme == "dark" else "black" + return html.li({"style": {"color": color}}, place) + + +@component +def place_list(places): + # No theme parameter, and none to forward. + return html.ul([place_label(place) for place in places]) + + +@component +def list_of_places(places): + return html.div( + {"style": {"border": "1px solid #ccc", "padding": "8px"}}, + place_list(places), + ) + + +@component +def section(places): + return html.section(html.h3("Places"), list_of_places(places)) + + +@component +def app(): + theme, set_theme = use_state("dark") + + async def toggle(event): + set_theme("light" if theme == "dark" else "dark") + + return html.div( + html.button( + {"onClick": toggle}, f"Switch to {'light' if theme == 'dark' else 'dark'}" + ), + html.p({"id": "theme"}, f"theme={theme}"), + # The provider is a component: it renders its children with no wrapper. + ThemeContext(html.div({"id": "places"}, section(PLACES)), value=theme), + ) diff --git a/docs/examples/passing_data_deeply_with_context/prop_drilling/app.py b/docs/examples/passing_data_deeply_with_context/prop_drilling/app.py new file mode 100644 index 000000000..f55ffa955 --- /dev/null +++ b/docs/examples/passing_data_deeply_with_context/prop_drilling/app.py @@ -0,0 +1,47 @@ +"""Four levels of props to reach one label.""" + +from reactpy import component, html, use_state + +# start +PLACES = ["Almaty", "Astana", "Shymkent", "Karaganda", "Turkestan"] + + +@component +def place_label(place, theme): + color = "coral" if theme == "dark" else "black" + return html.li({"style": {"color": color}}, place) + + +@component +def place_list(places, theme): + return html.ul([place_label(place, theme) for place in places]) + + +@component +def list_of_places(places, theme): + # Renders its children inside the list box. + return html.div( + {"style": {"border": "1px solid #ccc", "padding": "8px"}}, + place_list(places, theme), + ) + + +@component +def section(places, theme): + return html.section(html.h3("Places"), list_of_places(places, theme)) + + +@component +def app(): + theme, set_theme = use_state("dark") + + async def toggle(event): + set_theme("light" if theme == "dark" else "dark") + + return html.div( + html.button( + {"onClick": toggle}, f"Switch to {'light' if theme == 'dark' else 'dark'}" + ), + html.p({"id": "theme"}, f"theme={theme}"), + html.div({"id": "places"}, section(PLACES, theme)), + ) diff --git a/docs/examples/passing_data_deeply_with_context/provider_always/app.py b/docs/examples/passing_data_deeply_with_context/provider_always/app.py new file mode 100644 index 000000000..1347840dc --- /dev/null +++ b/docs/examples/passing_data_deeply_with_context/provider_always/app.py @@ -0,0 +1,58 @@ +"""Keep the provider in the tree; vary only its value, and state survives.""" + +from reactpy import component, create_context, html, use_context, use_ref, use_state + +ThemeContext = create_context("light") + + +# start +@component +def draft(): + # State owned by the leaf. It survives as long as this component keeps the + # same position in the tree. + text, set_text = use_state("") + body = use_ref(0) + body.current += 1 + + async def handle_text_change(event): + set_text(await event.target.value) + + return html.div( + html.input( + { + "value": text, + "onChange": handle_text_change, + "placeholder": "type something", + } + ), + html.p( + {"id": "draft"}, + f"draft={text!r} renders={body.current} theme={use_context(ThemeContext)}", + ), + ) + + +@component +def panel(): + provide, set_provide = use_state(True) + theme, set_theme = use_state("dark") + + async def toggle_wrap(event): + set_provide(not provide) + + async def toggle_theme(event): + set_theme("light" if theme == "dark" else "dark") + + return html.div( + html.button({"onClick": toggle_wrap}, "toggle provider"), + html.button({"onClick": toggle_theme}, "change value"), + # The provider is always in the tree, so nothing below it is unmounted. + # "Not provided" is expressed as the default value, not as an absent + # wrapper. + ThemeContext(draft(), value=theme if provide else "light"), + ) + + +@component +def app(): + return html.div(html.h3("Provider always present"), panel()) diff --git a/docs/examples/passing_data_deeply_with_context/provider_remount/app.py b/docs/examples/passing_data_deeply_with_context/provider_remount/app.py new file mode 100644 index 000000000..724c821ca --- /dev/null +++ b/docs/examples/passing_data_deeply_with_context/provider_remount/app.py @@ -0,0 +1,57 @@ +"""Wrapping a subtree in a provider conditionally throws its state away.""" + +from reactpy import component, create_context, html, use_context, use_ref, use_state + +ThemeContext = create_context("light") + + +# start +@component +def draft(): + # State owned by the leaf. It is lost if this component is remounted. + text, set_text = use_state("") + body = use_ref(0) + body.current += 1 + + async def handle_text_change(event): + set_text(await event.target.value) + + return html.div( + html.input( + { + "value": text, + "onChange": handle_text_change, + "placeholder": "type something", + } + ), + html.p( + {"id": "draft"}, + f"draft={text!r} renders={body.current} theme={use_context(ThemeContext)}", + ), + ) + + +@component +def panel(): + wrap, set_wrap = use_state(True) + theme, set_theme = use_state("dark") + + async def toggle_wrap(event): + # Adding or removing the provider changes the shape of the tree, so the + # subtree below is unmounted and rebuilt: the draft text is lost. + set_wrap(not wrap) + + async def toggle_theme(event): + # Changing only the value keeps the same tree: the draft survives. + set_theme("light" if theme == "dark" else "dark") + + return html.div( + html.button({"onClick": toggle_wrap}, "toggle provider"), + html.button({"onClick": toggle_theme}, "change value"), + (ThemeContext(draft(), value=theme) if wrap else draft()), + ) + + +@component +def app(): + return html.div(html.h3("Provider identity"), panel()) diff --git a/docs/examples/passing_data_deeply_with_context/self_provider/app.py b/docs/examples/passing_data_deeply_with_context/self_provider/app.py new file mode 100644 index 000000000..fa2e1c252 --- /dev/null +++ b/docs/examples/passing_data_deeply_with_context/self_provider/app.py @@ -0,0 +1,32 @@ +"""A component cannot read the context it provides itself.""" + +from reactpy import component, create_context, html, use_context, use_state + +ThemeContext = create_context("light") + + +# start +@component +def label(text): + return html.p({"id": f"label-{text}"}, f"{text} sees: {use_context(ThemeContext)}") + + +@component +def section(): + theme, set_theme = use_state("dark") + + async def toggle(event): + set_theme("light" if theme == "dark" else "dark") + + return html.div( + html.button({"onClick": toggle}, "toggle"), + # Reads the context the way any *consumer* does... + html.p({"id": "self"}, f"section itself sees: {use_context(ThemeContext)}"), + # ...and provides it for everything below, which is not itself. + ThemeContext(label("child"), value=theme), + ) + + +@component +def app(): + return html.div(html.h3("Self-provider"), section(), label("outside")) diff --git a/docs/examples/passing_props_to_a_component/changing/app.py b/docs/examples/passing_props_to_a_component/changing/app.py new file mode 100644 index 000000000..b63062f09 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/changing/app.py @@ -0,0 +1,40 @@ +import asyncio +from datetime import UTC, datetime + +from reactpy import component, html, use_async_effect, use_state + +# start + + +@component +def clock(color, time): + return html.h1({"style": {"color": color}}, time) + + +@component +def app(): + color, set_color = use_state("lightcoral") + now, set_now = use_state(datetime.now(UTC)) + + async def tick(): + while True: + await asyncio.sleep(1) + set_now(datetime.now(UTC)) + + use_async_effect(tick, dependencies=[]) + + async def handle_change(event): + set_color(await event.target.value) + + return html.div( + html.p( + "Pick a color: ", + html.select( + {"value": color, "onChange": handle_change}, + html.option({"value": "lightcoral"}, "lightcoral"), + html.option({"value": "midnightblue"}, "midnightblue"), + html.option({"value": "rebeccapurple"}, "rebeccapurple"), + ), + ), + clock(color=color, time=now.strftime("%H:%M:%S")), + ) diff --git a/docs/examples/passing_props_to_a_component/changing/styles.css b/docs/examples/passing_props_to_a_component/changing/styles.css new file mode 100644 index 000000000..9a06f08d6 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/changing/styles.css @@ -0,0 +1,8 @@ +body { + min-height: 120px; +} + +.avatar { + margin: 20px; + border-radius: 50%; +} diff --git a/docs/examples/passing_props_to_a_component/children/app.py b/docs/examples/passing_props_to_a_component/children/app.py new file mode 100644 index 000000000..552ab74b3 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/children/app.py @@ -0,0 +1,32 @@ +from reactpy import component, html + +# start + + +def get_image_url(person, size="s"): + return f"../../assets/images/demos/{person['image_id']}{size}.jpg" + + +@component +def avatar(person, size=100): + return html.img( + { + "className": "avatar", + "src": get_image_url(person), + "alt": person["name"], + "width": size, + "height": size, + } + ) + + +@component +def card(*children): + return html.div({"className": "card"}, *children) + + +@component +def profile(): + return card( + avatar(person={"name": "Katsuko Saruhashi", "image_id": "YfeOqp2"}), + ) diff --git a/docs/examples/passing_props_to_a_component/children/styles.css b/docs/examples/passing_props_to_a_component/children/styles.css new file mode 100644 index 000000000..9a06f08d6 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/children/styles.css @@ -0,0 +1,8 @@ +body { + min-height: 120px; +} + +.avatar { + margin: 20px; + border-radius: 50%; +} diff --git a/docs/examples/passing_props_to_a_component/defaults/app.py b/docs/examples/passing_props_to_a_component/defaults/app.py new file mode 100644 index 000000000..a79bf9b34 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/defaults/app.py @@ -0,0 +1,31 @@ +from reactpy import component, html + +# start + + +def get_image_url(person, size="s"): + return f"../../assets/images/demos/{person['image_id']}{size}.jpg" + + +@component +def avatar(person, size=100): + return html.img( + { + "className": "avatar", + "src": get_image_url(person), + "alt": person["name"], + "width": size, + "height": size, + } + ) + + +@component +def profile(): + return html.div( + avatar(person={"name": "Lin Lanying", "image_id": "1bX5QH6"}), + avatar( + person={"name": "Katsuko Saruhashi", "image_id": "YfeOqp2"}, + size=200, + ), + ) diff --git a/docs/examples/passing_props_to_a_component/defaults/styles.css b/docs/examples/passing_props_to_a_component/defaults/styles.css new file mode 100644 index 000000000..9a06f08d6 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/defaults/styles.css @@ -0,0 +1,8 @@ +body { + min-height: 120px; +} + +.avatar { + margin: 20px; + border-radius: 50%; +} diff --git a/docs/examples/passing_props_to_a_component/familiar/app.py b/docs/examples/passing_props_to_a_component/familiar/app.py new file mode 100644 index 000000000..8cab66bcf --- /dev/null +++ b/docs/examples/passing_props_to_a_component/familiar/app.py @@ -0,0 +1,26 @@ +from reactpy import component, html + +# start + + +@component +def avatar(): + return html.img( + { + "className": "avatar", + "src": "../../assets/images/demos/1bX5QH6.jpg", + "alt": "Lin Lanying", + "width": 100, + "height": 100, + } + ) + + +@component +def profile(): + return avatar() + + +@component +def app(): + return profile() diff --git a/docs/examples/passing_props_to_a_component/familiar/styles.css b/docs/examples/passing_props_to_a_component/familiar/styles.css new file mode 100644 index 000000000..9a06f08d6 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/familiar/styles.css @@ -0,0 +1,8 @@ +body { + min-height: 120px; +} + +.avatar { + margin: 20px; + border-radius: 50%; +} diff --git a/docs/examples/passing_props_to_a_component/forwarding/app.py b/docs/examples/passing_props_to_a_component/forwarding/app.py new file mode 100644 index 000000000..ffdbd6d08 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/forwarding/app.py @@ -0,0 +1,33 @@ +from reactpy import component, html + +# start + + +def get_image_url(person, size="s"): + return f"../../assets/images/demos/{person['image_id']}{size}.jpg" + + +@component +def avatar(person, size=100): + return html.img( + { + "className": "avatar", + "src": get_image_url(person), + "alt": person["name"], + "width": size, + "height": size, + } + ) + + +@component +def profile(**props): + return html.div({"className": "card"}, avatar(**props)) + + +@component +def app(): + return profile( + person={"name": "Lin Lanying", "image_id": "1bX5QH6"}, + size=100, + ) diff --git a/docs/examples/passing_props_to_a_component/forwarding/styles.css b/docs/examples/passing_props_to_a_component/forwarding/styles.css new file mode 100644 index 000000000..9a06f08d6 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/forwarding/styles.css @@ -0,0 +1,8 @@ +body { + min-height: 120px; +} + +.avatar { + margin: 20px; + border-radius: 50%; +} diff --git a/docs/examples/passing_props_to_a_component/passing/app.py b/docs/examples/passing_props_to_a_component/passing/app.py new file mode 100644 index 000000000..14ccbf4d1 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/passing/app.py @@ -0,0 +1,34 @@ +from reactpy import component, html + +# start + + +def get_image_url(person, size="s"): + return f"../../assets/images/demos/{person['image_id']}{size}.jpg" + + +@component +def avatar(person, size): + return html.img( + { + "className": "avatar", + "src": get_image_url(person), + "alt": person["name"], + "width": size, + "height": size, + } + ) + + +@component +def profile(): + return html.div( + avatar( + person={"name": "Lin Lanying", "image_id": "1bX5QH6"}, + size=100, + ), + avatar( + person={"name": "Katsuko Saruhashi", "image_id": "YfeOqp2"}, + size=100, + ), + ) diff --git a/docs/examples/passing_props_to_a_component/passing/styles.css b/docs/examples/passing_props_to_a_component/passing/styles.css new file mode 100644 index 000000000..9a06f08d6 --- /dev/null +++ b/docs/examples/passing_props_to_a_component/passing/styles.css @@ -0,0 +1,8 @@ +body { + min-height: 120px; +} + +.avatar { + margin: 20px; + border-radius: 50%; +} diff --git a/docs/examples/preserving_and_resetting_state/different_components/app.py b/docs/examples/preserving_and_resetting_state/different_components/app.py new file mode 100644 index 000000000..c9a3d4860 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/different_components/app.py @@ -0,0 +1,41 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + score, set_score = use_state(0) + + async def handle_score_click(event): + set_score(score + 1) + + return html.div( + {"className": "counter"}, + html.h1(score), + html.button({"onClick": handle_score_click}, "Add one"), + ) + + +@component +def app(): + is_paused, set_is_paused = use_state(False) + + async def handle_is_paused_change(event): + set_is_paused(await event.target.checked) + + return html.div( + # Switching between a counter() and an html.p() puts a *different* kind + # of child at this position, so ReactPy discards the counter's state + # and starts a brand new one when the break ends. + html.p("See you later!") if is_paused else counter(), + html.label( + html.input( + { + "type": "checkbox", + "checked": is_paused, + "onChange": handle_is_paused_change, + } + ), + "Take a break", + ), + ) diff --git a/docs/examples/preserving_and_resetting_state/different_components/styles.css b/docs/examples/preserving_and_resetting_state/different_components/styles.css new file mode 100644 index 000000000..9ec4ab588 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/different_components/styles.css @@ -0,0 +1,18 @@ +.counter { + width: 130px; + text-align: center; + border: 1px solid #aaa; + border-radius: 4px; + padding: 12px; + margin: 0 12px 12px 0; +} + +.counter h1 { + font-size: 18px; + margin: 0 0 6px; +} + +.fancy { + background: #ffffd8; + border-color: #e0b400; +} diff --git a/docs/examples/preserving_and_resetting_state/different_positions/app.py b/docs/examples/preserving_and_resetting_state/different_positions/app.py new file mode 100644 index 000000000..e8bffb50a --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/different_positions/app.py @@ -0,0 +1,40 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(person): + score, set_score = use_state(0) + + async def handle_score_click(event): + set_score(score + 1) + + return html.div( + {"className": "counter"}, + html.h1(f"{person}'s score: {score}"), + html.button({"onClick": handle_score_click}, "Add one"), + ) + + +@component +def scoreboard(): + is_player_a, set_is_player_a = use_state(True) + + async def handle_is_player_a_click(event): + set_is_player_a(not is_player_a) + + return html.div( + # Two containers that always exist, so each player's counter has its + # own position in the tree. Only one holds a counter at a time, and the + # container that loses it destroys that state. + # + # Do not "simplify" this to *([] if cond else [counter(...)]): an empty + # expansion contributes no children, so both counters end up at + # position 0 and ReactPy treats them as one component -- no reset. + html.div(counter("Taylor") if is_player_a else None), + html.div(counter("Sarah") if not is_player_a else None), + html.button( + {"onClick": handle_is_player_a_click}, + "Next player!", + ), + ) diff --git a/docs/examples/preserving_and_resetting_state/different_positions/styles.css b/docs/examples/preserving_and_resetting_state/different_positions/styles.css new file mode 100644 index 000000000..9ec4ab588 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/different_positions/styles.css @@ -0,0 +1,18 @@ +.counter { + width: 130px; + text-align: center; + border: 1px solid #aaa; + border-radius: 4px; + padding: 12px; + margin: 0 12px 12px 0; +} + +.counter h1 { + font-size: 18px; + margin: 0 0 6px; +} + +.fancy { + background: #ffffd8; + border-color: #e0b400; +} diff --git a/docs/examples/preserving_and_resetting_state/messenger/app.py b/docs/examples/preserving_and_resetting_state/messenger/app.py new file mode 100644 index 000000000..d729a27eb --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/messenger/app.py @@ -0,0 +1,59 @@ +from data import contacts + +from reactpy import component, html, use_state + + +# start +@component +def chat(contact): + draft, set_draft = use_state("") + + async def handle_draft_click(event): + set_draft("") + + return html.section( + {"className": "chat"}, + html.h3(f"To: {contact['name']} <{contact['email']}>"), + html.textarea({"placeholder": "Write a message", "value": draft}), + html.button({"onClick": handle_draft_click}, "Clear"), + ) + + +@component +def contact_list(selected_contact, on_select): + def make_select(contact): + # A factory, not a lambda: a handler must be a coroutine, and the + # contact it acts on is bound by the closure. + async def choose(event): + on_select(contact) + + return choose + + return html.section( + {"className": "contact-list"}, + html.ul( + [ + html.li( + {"key": contact["id"]}, + html.button( + {"onClick": make_select(contact)}, + contact["name"], + ), + ) + for contact in contacts + ] + ), + ) + + +@component +def messenger(): + to, set_to = use_state(contacts[0]) + + return html.div( + contact_list(to, set_to), + # Without the key, switching contacts keeps the half-written message, + # because chat() stays at the same position. The key ties the draft to + # the contact instead of the slot. + chat(to, key=f"chat-{to['id']}"), + ) diff --git a/docs/examples/preserving_and_resetting_state/messenger/data.py b/docs/examples/preserving_and_resetting_state/messenger/data.py new file mode 100644 index 000000000..afa31ffb9 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/messenger/data.py @@ -0,0 +1,5 @@ +contacts = [ + {"id": 0, "name": "Taylor", "email": "taylor@mail.com"}, + {"id": 1, "name": "Alice", "email": "alice@mail.com"}, + {"id": 2, "name": "Bob", "email": "bob@mail.com"}, +] diff --git a/docs/examples/preserving_and_resetting_state/messenger/styles.css b/docs/examples/preserving_and_resetting_state/messenger/styles.css new file mode 100644 index 000000000..5a0e445c4 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/messenger/styles.css @@ -0,0 +1,19 @@ +.contact-list ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; +} + +.chat { + border: 1px solid #aaa; + border-radius: 4px; + padding: 10px; + margin-top: 10px; +} + +.chat textarea { + display: block; + width: 100%; + min-height: 60px; +} diff --git a/docs/examples/preserving_and_resetting_state/reset_with_key/app.py b/docs/examples/preserving_and_resetting_state/reset_with_key/app.py new file mode 100644 index 000000000..ff4f2af24 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/reset_with_key/app.py @@ -0,0 +1,40 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(person): + score, set_score = use_state(0) + + async def handle_score_click(event): + set_score(score + 1) + + return html.div( + {"className": "counter"}, + html.h1(f"{person}'s score: {score}"), + html.button({"onClick": handle_score_click}, "Add one"), + ) + + +@component +def scoreboard(): + is_player_a, set_is_player_a = use_state(True) + + async def handle_is_player_a_click(event): + set_is_player_a(not is_player_a) + + return html.div( + # Both branches render counter() at the same position, so without a key + # ReactPy would treat them as the same component and carry the score + # from one player to the next. The key gives each player their own + # identity, so switching players resets the score. + ( + counter("Taylor", key="Taylor") + if is_player_a + else counter("Sarah", key="Sarah") + ), + html.button( + {"onClick": handle_is_player_a_click}, + "Next player!", + ), + ) diff --git a/docs/examples/preserving_and_resetting_state/reset_with_key/styles.css b/docs/examples/preserving_and_resetting_state/reset_with_key/styles.css new file mode 100644 index 000000000..9ec4ab588 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/reset_with_key/styles.css @@ -0,0 +1,18 @@ +.counter { + width: 130px; + text-align: center; + border: 1px solid #aaa; + border-radius: 4px; + padding: 12px; + margin: 0 12px 12px 0; +} + +.counter h1 { + font-size: 18px; + margin: 0 0 6px; +} + +.fancy { + background: #ffffd8; + border-color: #e0b400; +} diff --git a/docs/examples/preserving_and_resetting_state/same_position/app.py b/docs/examples/preserving_and_resetting_state/same_position/app.py new file mode 100644 index 000000000..a642d0cdf --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/same_position/app.py @@ -0,0 +1,41 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(is_fancy): + score, set_score = use_state(0) + + async def handle_score_click(event): + set_score(score + 1) + + return html.div( + {"className": f"counter{' fancy' if is_fancy else ''}"}, + html.h1(score), + html.button({"onClick": handle_score_click}, "Add one"), + ) + + +@component +def app(): + is_fancy, set_is_fancy = use_state(False) + + async def handle_is_fancy_change(event): + set_is_fancy(await event.target.checked) + + return html.div( + # Only one counter() is ever rendered at this position, and it is the + # same component function either way, so ReactPy keeps its state across + # the toggle; only the is_fancy prop changes. + counter(is_fancy), + html.label( + html.input( + { + "type": "checkbox", + "checked": is_fancy, + "onChange": handle_is_fancy_change, + } + ), + "Use fancy styling", + ), + ) diff --git a/docs/examples/preserving_and_resetting_state/same_position/styles.css b/docs/examples/preserving_and_resetting_state/same_position/styles.css new file mode 100644 index 000000000..9ec4ab588 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/same_position/styles.css @@ -0,0 +1,18 @@ +.counter { + width: 130px; + text-align: center; + border: 1px solid #aaa; + border-radius: 4px; + padding: 12px; + margin: 0 12px 12px 0; +} + +.counter h1 { + font-size: 18px; + margin: 0 0 6px; +} + +.fancy { + background: #ffffd8; + border-color: #e0b400; +} diff --git a/docs/examples/preserving_and_resetting_state/two_positions/app.py b/docs/examples/preserving_and_resetting_state/two_positions/app.py new file mode 100644 index 000000000..534f37f62 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/two_positions/app.py @@ -0,0 +1,24 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + score, set_score = use_state(0) + + async def handle_score_click(event): + set_score(score + 1) + + return html.div( + {"className": "counter"}, + html.h1(score), + html.button({"onClick": handle_score_click}, "Add one"), + ) + + +@component +def app(): + # The same component function, rendered at two different positions in the + # tree. ReactPy tracks state by position, so these two counters each get + # their own score. + return html.div(counter(), counter()) diff --git a/docs/examples/preserving_and_resetting_state/two_positions/styles.css b/docs/examples/preserving_and_resetting_state/two_positions/styles.css new file mode 100644 index 000000000..9ec4ab588 --- /dev/null +++ b/docs/examples/preserving_and_resetting_state/two_positions/styles.css @@ -0,0 +1,18 @@ +.counter { + width: 130px; + text-align: center; + border: 1px solid #aaa; + border-radius: 4px; + padding: 12px; + margin: 0 12px 12px 0; +} + +.counter h1 { + font-size: 18px; + margin: 0 0 6px; +} + +.fancy { + background: #ffffd8; + border-color: #e0b400; +} diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/set_counter_3_times.py b/docs/examples/queueing_a_series_of_state_updates/batched/app.py similarity index 53% rename from docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/set_counter_3_times.py rename to docs/examples/queueing_a_series_of_state_updates/batched/app.py index fe97351af..a1c8fb153 100644 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/set_counter_3_times.py +++ b/docs/examples/queueing_a_series_of_state_updates/batched/app.py @@ -1,19 +1,17 @@ -from reactpy import component, html, run, use_state +from reactpy import component, html, use_state +# start @component -def Counter(): +def counter(): number, set_number = use_state(0) - def handle_click(event): + async def handle_click(event): set_number(number + 1) set_number(number + 1) set_number(number + 1) return html.div( html.h1(number), - html.button({"on_click": handle_click}, "Increment"), + html.button({"onClick": handle_click}, "+3"), ) - - -run(Counter) diff --git a/docs/examples/queueing_a_series_of_state_updates/batched/styles.css b/docs/examples/queueing_a_series_of_state_updates/batched/styles.css new file mode 100644 index 000000000..29de42386 --- /dev/null +++ b/docs/examples/queueing_a_series_of_state_updates/batched/styles.css @@ -0,0 +1,12 @@ +button { + display: inline-block; + margin: 10px; + font-size: 20px; +} + +h1 { + display: inline-block; + margin: 10px; + width: 30px; + text-align: center; +} diff --git a/docs/examples/queueing_a_series_of_state_updates/replace_then_update/app.py b/docs/examples/queueing_a_series_of_state_updates/replace_then_update/app.py new file mode 100644 index 000000000..aa2ba03c9 --- /dev/null +++ b/docs/examples/queueing_a_series_of_state_updates/replace_then_update/app.py @@ -0,0 +1,17 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + number, set_number = use_state(0) + + async def handle_click(event): + # number is 0 for this render, so this reads as set_number(5). + set_number(number + 5) + set_number(lambda n: n + 1) + + return html.div( + html.h1(number), + html.button({"onClick": handle_click}, "Increase the number"), + ) diff --git a/docs/examples/queueing_a_series_of_state_updates/replace_then_update/styles.css b/docs/examples/queueing_a_series_of_state_updates/replace_then_update/styles.css new file mode 100644 index 000000000..29de42386 --- /dev/null +++ b/docs/examples/queueing_a_series_of_state_updates/replace_then_update/styles.css @@ -0,0 +1,12 @@ +button { + display: inline-block; + margin: 10px; + font-size: 20px; +} + +h1 { + display: inline-block; + margin: 10px; + width: 30px; + text-align: center; +} diff --git a/docs/examples/queueing_a_series_of_state_updates/update_then_replace/app.py b/docs/examples/queueing_a_series_of_state_updates/update_then_replace/app.py new file mode 100644 index 000000000..0b1132290 --- /dev/null +++ b/docs/examples/queueing_a_series_of_state_updates/update_then_replace/app.py @@ -0,0 +1,17 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + number, set_number = use_state(0) + + async def handle_click(event): + set_number(number + 5) + set_number(lambda n: n + 1) + set_number(42) + + return html.div( + html.h1(number), + html.button({"onClick": handle_click}, "Increase the number"), + ) diff --git a/docs/examples/queueing_a_series_of_state_updates/update_then_replace/styles.css b/docs/examples/queueing_a_series_of_state_updates/update_then_replace/styles.css new file mode 100644 index 000000000..29de42386 --- /dev/null +++ b/docs/examples/queueing_a_series_of_state_updates/update_then_replace/styles.css @@ -0,0 +1,12 @@ +button { + display: inline-block; + margin: 10px; + font-size: 20px; +} + +h1 { + display: inline-block; + margin: 10px; + width: 30px; + text-align: center; +} diff --git a/docs/examples/queueing_a_series_of_state_updates/updaters/app.py b/docs/examples/queueing_a_series_of_state_updates/updaters/app.py new file mode 100644 index 000000000..06bb2fa39 --- /dev/null +++ b/docs/examples/queueing_a_series_of_state_updates/updaters/app.py @@ -0,0 +1,17 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + number, set_number = use_state(0) + + async def handle_click(event): + set_number(lambda n: n + 1) + set_number(lambda n: n + 1) + set_number(lambda n: n + 1) + + return html.div( + html.h1(number), + html.button({"onClick": handle_click}, "+3"), + ) diff --git a/docs/examples/queueing_a_series_of_state_updates/updaters/styles.css b/docs/examples/queueing_a_series_of_state_updates/updaters/styles.css new file mode 100644 index 000000000..29de42386 --- /dev/null +++ b/docs/examples/queueing_a_series_of_state_updates/updaters/styles.css @@ -0,0 +1,12 @@ +button { + display: inline-block; + margin: 10px; + font-size: 20px; +} + +h1 { + display: inline-block; + margin: 10px; + width: 30px; + text-align: center; +} diff --git a/docs/examples/quick_start/adding_styles.css b/docs/examples/quick_start/adding_styles.css new file mode 100644 index 000000000..4d02060e1 --- /dev/null +++ b/docs/examples/quick_start/adding_styles.css @@ -0,0 +1,4 @@ +/* In your CSS */ +.avatar { + border-radius: 50%; +} diff --git a/docs/examples/quick_start/adding_styles.py b/docs/examples/quick_start/adding_styles.py new file mode 100644 index 000000000..8e1c7931b --- /dev/null +++ b/docs/examples/quick_start/adding_styles.py @@ -0,0 +1,4 @@ +from reactpy import html + +# start +html.img({"className": "avatar"}) diff --git a/docs/examples/quick_start/conditional_rendering.py b/docs/examples/quick_start/conditional_rendering.py new file mode 100644 index 000000000..d89e097a4 --- /dev/null +++ b/docs/examples/quick_start/conditional_rendering.py @@ -0,0 +1,22 @@ +from reactpy import component, html + + +def admin_panel(): + return [] + + +def login_form(): + return [] + + +is_logged_in = True + + +# start +@component +def my_component(): + if is_logged_in: + content = admin_panel() + else: + content = login_form() + return html.div(content) diff --git a/docs/examples/quick_start/conditional_rendering_no_else.py b/docs/examples/quick_start/conditional_rendering_no_else.py new file mode 100644 index 000000000..09d66f492 --- /dev/null +++ b/docs/examples/quick_start/conditional_rendering_no_else.py @@ -0,0 +1,18 @@ +from reactpy import component, html + + +def admin_panel(): + return html.div("Admin") + + +is_logged_in = True + + +# start +@component +def my_component(): + # No `else` branch needed: unpack a one-item list, or an empty one. + # Unpack the *component*, never the dict it returns -- `*admin_panel()` + # would iterate the dict and emit its keys as text. + # `is_logged_in and admin_panel()` would render the word "False". + return html.div(*([admin_panel()] if is_logged_in else [])) diff --git a/docs/examples/quick_start/conditional_rendering_ternary.py b/docs/examples/quick_start/conditional_rendering_ternary.py new file mode 100644 index 000000000..2490cb5f9 --- /dev/null +++ b/docs/examples/quick_start/conditional_rendering_ternary.py @@ -0,0 +1,18 @@ +from reactpy import component, html + + +def admin_panel(): + return [] + + +def login_form(): + return [] + + +is_logged_in = True + + +# start +@component +def my_component(): + return html.div(admin_panel() if is_logged_in else login_form()) diff --git a/docs/examples/quick_start/creating_and_nesting_components.py b/docs/examples/quick_start/creating_and_nesting_components.py new file mode 100644 index 000000000..99ff909a2 --- /dev/null +++ b/docs/examples/quick_start/creating_and_nesting_components.py @@ -0,0 +1,14 @@ +from reactpy import component, html + + +@component +def my_button(): + return html.button("I'm a button!") + + +@component +def my_app(): + return html.div( + html.h1("Welcome to my app"), + my_button(), + ) diff --git a/docs/examples/quick_start/displaying_data.css b/docs/examples/quick_start/displaying_data.css new file mode 100644 index 000000000..9f5a392c1 --- /dev/null +++ b/docs/examples/quick_start/displaying_data.css @@ -0,0 +1,7 @@ +.avatar { + border-radius: 50%; +} + +.large { + border: 4px solid gold; +} diff --git a/docs/examples/quick_start/displaying_data.py b/docs/examples/quick_start/displaying_data.py new file mode 100644 index 000000000..dd229abda --- /dev/null +++ b/docs/examples/quick_start/displaying_data.py @@ -0,0 +1,25 @@ +from reactpy import component, html + +user = { + "name": "Hedy Lamarr", + "image_url": "../../assets/images/demos/yXOvdOSs.jpg", + "image_size": 90, +} + + +@component +def profile(): + return html.div( + html.h3(user["name"]), + html.img( + { + "className": "avatar", + "src": user["image_url"], + "alt": f"Photo of {user['name']}", + "style": { + "width": user["image_size"], + "height": user["image_size"], + }, + } + ), + ) diff --git a/docs/examples/quick_start/my_app.py b/docs/examples/quick_start/my_app.py new file mode 100644 index 000000000..1616d54d1 --- /dev/null +++ b/docs/examples/quick_start/my_app.py @@ -0,0 +1,12 @@ +from reactpy import component, html + +from .my_button import my_button + + +# start +@component +def my_app(): + return html.div( + html.h1("Welcome to my app"), + my_button(), + ) diff --git a/docs/examples/quick_start/my_button.py b/docs/examples/quick_start/my_button.py new file mode 100644 index 000000000..0016632ca --- /dev/null +++ b/docs/examples/quick_start/my_button.py @@ -0,0 +1,7 @@ +from reactpy import component, html + + +# start +@component +def my_button(): + return html.button("I'm a button!") diff --git a/docs/examples/quick_start/rendering_lists.py b/docs/examples/quick_start/rendering_lists.py new file mode 100644 index 000000000..be75f8ce2 --- /dev/null +++ b/docs/examples/quick_start/rendering_lists.py @@ -0,0 +1,23 @@ +from reactpy import component, html + +products = [ + {"title": "Cabbage", "is_fruit": False, "id": 1}, + {"title": "Garlic", "is_fruit": False, "id": 2}, + {"title": "Apple", "is_fruit": True, "id": 3}, +] + + +@component +def shopping_list(): + list_items = [ + html.li( + { + "key": product["id"], + "style": {"color": "magenta" if product["is_fruit"] else "darkgreen"}, + }, + product["title"], + ) + for product in products + ] + + return html.ul(list_items) diff --git a/docs/examples/quick_start/rendering_lists_list_items.py b/docs/examples/quick_start/rendering_lists_list_items.py new file mode 100644 index 000000000..43e108f36 --- /dev/null +++ b/docs/examples/quick_start/rendering_lists_list_items.py @@ -0,0 +1,6 @@ +from reactpy import html + +from .rendering_lists_products import products + +# start +list_items = [html.li({"key": product["id"]}, product["title"]) for product in products] diff --git a/docs/examples/quick_start/rendering_lists_products.py b/docs/examples/quick_start/rendering_lists_products.py new file mode 100644 index 000000000..c2d6a5e31 --- /dev/null +++ b/docs/examples/quick_start/rendering_lists_products.py @@ -0,0 +1,5 @@ +products = [ + {"title": "Cabbage", "id": 1}, + {"title": "Garlic", "id": 2}, + {"title": "Apple", "id": 3}, +] diff --git a/docs/examples/quick_start/responding_to_events.py b/docs/examples/quick_start/responding_to_events.py new file mode 100644 index 000000000..083818e54 --- /dev/null +++ b/docs/examples/quick_start/responding_to_events.py @@ -0,0 +1,13 @@ +from reactpy import component, html + + +# start +@component +def my_button(): + async def handle_click(event): + print("You clicked me!") + + return html.button( + {"onClick": handle_click}, + "Click me", + ) diff --git a/docs/examples/quick_start/sharing_data_between_components.css b/docs/examples/quick_start/sharing_data_between_components.css new file mode 100644 index 000000000..6081b7ef4 --- /dev/null +++ b/docs/examples/quick_start/sharing_data_between_components.css @@ -0,0 +1,4 @@ +button { + display: block; + margin-bottom: 5px; +} diff --git a/docs/examples/quick_start/sharing_data_between_components.py b/docs/examples/quick_start/sharing_data_between_components.py new file mode 100644 index 000000000..7114acc4d --- /dev/null +++ b/docs/examples/quick_start/sharing_data_between_components.py @@ -0,0 +1,20 @@ +from reactpy import component, html, use_state + + +@component +def my_app(): + count, set_count = use_state(0) + + async def handle_click(event): + set_count(count + 1) + + return html.div( + html.h1("Counters that update together"), + my_button(count, handle_click), + my_button(count, handle_click), + ) + + +@component +def my_button(count, on_click): + return html.button({"onClick": on_click}, f"Clicked {count} times") diff --git a/docs/examples/quick_start/sharing_data_between_components_button.py b/docs/examples/quick_start/sharing_data_between_components_button.py new file mode 100644 index 000000000..a9a4911f2 --- /dev/null +++ b/docs/examples/quick_start/sharing_data_between_components_button.py @@ -0,0 +1,7 @@ +from reactpy import component, html + + +# start +@component +def my_button(count, on_click): + return html.button({"onClick": on_click}, f"Clicked {count} times") diff --git a/docs/examples/quick_start/sharing_data_between_components_move_state.py b/docs/examples/quick_start/sharing_data_between_components_move_state.py new file mode 100644 index 000000000..dee394a03 --- /dev/null +++ b/docs/examples/quick_start/sharing_data_between_components_move_state.py @@ -0,0 +1,20 @@ +from reactpy import component, html, use_state + + +# start +@component +def my_app(): + count, set_count = use_state(0) + + async def handle_click(event): + set_count(count + 1) + + return html.div( + html.h1("Counters that update separately"), + my_button(), + my_button(), + ) + + +@component +def my_button(): ... # this code is being moved up into `my_app` diff --git a/docs/examples/quick_start/sharing_data_between_components_props.py b/docs/examples/quick_start/sharing_data_between_components_props.py new file mode 100644 index 000000000..10682ff6b --- /dev/null +++ b/docs/examples/quick_start/sharing_data_between_components_props.py @@ -0,0 +1,21 @@ +from reactpy import component, html, use_state + + +# start +@component +def my_app(): + count, set_count = use_state(0) + + async def handle_click(event): + set_count(count + 1) + + return html.div( + html.h1("Counters that update together"), + my_button(count, handle_click), + my_button(count, handle_click), + ) + # end + + +@component +def my_button(count, on_click): ... diff --git a/docs/examples/quick_start/updating_the_screen.css b/docs/examples/quick_start/updating_the_screen.css new file mode 100644 index 000000000..6081b7ef4 --- /dev/null +++ b/docs/examples/quick_start/updating_the_screen.css @@ -0,0 +1,4 @@ +button { + display: block; + margin-bottom: 5px; +} diff --git a/docs/examples/quick_start/updating_the_screen.py b/docs/examples/quick_start/updating_the_screen.py new file mode 100644 index 000000000..6120babeb --- /dev/null +++ b/docs/examples/quick_start/updating_the_screen.py @@ -0,0 +1,20 @@ +from reactpy import component, html, use_state + + +@component +def my_app(): + return html.div( + html.h1("Counters that update separately"), + my_button(), + my_button(), + ) + + +@component +def my_button(): + count, set_count = use_state(0) + + async def handle_click(event): + set_count(count + 1) + + return html.button({"onClick": handle_click}, f"Clicked {count} times") diff --git a/docs/examples/quick_start/updating_the_screen_event.py b/docs/examples/quick_start/updating_the_screen_event.py new file mode 100644 index 000000000..744d3f521 --- /dev/null +++ b/docs/examples/quick_start/updating_the_screen_event.py @@ -0,0 +1,12 @@ +from reactpy import component, html, use_state + + +# start +@component +def my_button(): + count, set_count = use_state(0) + + async def handle_click(event): + set_count(count + 1) + + return html.button({"onClick": handle_click}, f"Clicked {count} times") diff --git a/docs/examples/quick_start/updating_the_screen_use_state.py b/docs/examples/quick_start/updating_the_screen_use_state.py new file mode 100644 index 000000000..3570c359c --- /dev/null +++ b/docs/examples/quick_start/updating_the_screen_use_state.py @@ -0,0 +1,7 @@ +from reactpy import use_state + +# end + +# Everything above `# end` is what the docs render. This call keeps the file +# valid for linters and type checkers without appearing on the page. +use_state(0) diff --git a/docs/examples/quick_start/updating_the_screen_use_state_button.py b/docs/examples/quick_start/updating_the_screen_use_state_button.py new file mode 100644 index 000000000..c331ee6c9 --- /dev/null +++ b/docs/examples/quick_start/updating_the_screen_use_state_button.py @@ -0,0 +1,8 @@ +from reactpy import component, use_state + + +# start +@component +def my_button(): + count, set_count = use_state(0) + # ... diff --git a/docs/examples/reacting_to_input_with_state/final_form/app.py b/docs/examples/reacting_to_input_with_state/final_form/app.py new file mode 100644 index 000000000..8395302dc --- /dev/null +++ b/docs/examples/reacting_to_input_with_state/final_form/app.py @@ -0,0 +1,67 @@ +import asyncio + +from reactpy import component, html, use_state + + +async def submit_form(answer): + """Pretend to hit the network; only "Lima" is correct.""" + await asyncio.sleep(1.5) + if answer.lower() != "lima": + msg = "Good guess but a wrong answer. Try again!" + raise ValueError(msg) + + +# start +@component +def form(): + answer, set_answer = use_state("") + error, set_error = use_state("") + status, set_status = use_state("typing") # 'typing', 'submitting', or 'success' + + if status == "success": + return html.h1("That's right!") + + async def handle_submit(event): + # Awaiting inside the handler is fine: ReactPy still suppresses the + # form's default navigation, because it detects the preventDefault() + # call by reading this function's bytecode -- whether or not the handler + # ever finishes does not matter. The call must appear in this function + # itself, though: hiding it in a helper defeats the inspection. + event.preventDefault() + set_status("submitting") + try: + await submit_form(answer) + except ValueError as exc: + set_status("typing") + set_error(str(exc)) + else: + set_status("success") + + async def handle_textarea_change(event): + set_answer(await event.target.value) + + return html.div( + html.h2("City quiz"), + html.p( + "In which city is there a billboard that turns air into drinkable water?" + ), + html.form( + {"onSubmit": handle_submit}, + html.textarea( + { + "value": answer, + "onChange": handle_textarea_change, + "disabled": status == "submitting", + } + ), + html.br(), + html.button( + { + "disabled": len(answer) == 0 or status == "submitting", + "type": "submit", + }, + "Submit", + ), + html.p({"className": "Error"}, error) if error else None, + ), + ) diff --git a/docs/examples/reacting_to_input_with_state/final_form/styles.css b/docs/examples/reacting_to_input_with_state/final_form/styles.css new file mode 100644 index 000000000..e2aebc265 --- /dev/null +++ b/docs/examples/reacting_to_input_with_state/final_form/styles.css @@ -0,0 +1,19 @@ +.Error { + color: red; +} + +textarea { + width: 260px; + height: 60px; +} + +section { + border-bottom: 1px solid #ddd; + padding-bottom: 10px; + margin-bottom: 10px; +} + +h4 { + margin: 4px 0; + color: #888; +} diff --git a/docs/examples/reacting_to_input_with_state/status_mock/app.py b/docs/examples/reacting_to_input_with_state/status_mock/app.py new file mode 100644 index 000000000..698b8f2ad --- /dev/null +++ b/docs/examples/reacting_to_input_with_state/status_mock/app.py @@ -0,0 +1,35 @@ +from reactpy import component, html + + +# start +@component +def form(status="empty"): + """A mock: the visual states are driven by a prop, not by real logic. + + Rendering this with each status is the fastest way to enumerate the states + before writing any interaction code. + """ + if status == "success": + return html.h1("That's right!") + + return html.div( + html.h2("City quiz"), + html.p( + "In which city is there a billboard that turns air into drinkable water?" + ), + html.form( + html.textarea({"disabled": status == "submitting"}), + html.br(), + html.button( + {"disabled": status in {"empty", "submitting"}}, + "Submit", + ), + ( + html.p( + {"className": "Error"}, "Good guess but a wrong answer. Try again!" + ) + if status == "error" + else None + ), + ), + ) diff --git a/docs/examples/reacting_to_input_with_state/status_mock/styles.css b/docs/examples/reacting_to_input_with_state/status_mock/styles.css new file mode 100644 index 000000000..e2aebc265 --- /dev/null +++ b/docs/examples/reacting_to_input_with_state/status_mock/styles.css @@ -0,0 +1,19 @@ +.Error { + color: red; +} + +textarea { + width: 260px; + height: 60px; +} + +section { + border-bottom: 1px solid #ddd; + padding-bottom: 10px; + margin-bottom: 10px; +} + +h4 { + margin: 4px 0; + color: #888; +} diff --git a/docs/examples/reacting_to_input_with_state/styleguide/app.py b/docs/examples/reacting_to_input_with_state/styleguide/app.py new file mode 100644 index 000000000..aa1a54df5 --- /dev/null +++ b/docs/examples/reacting_to_input_with_state/styleguide/app.py @@ -0,0 +1,50 @@ +from reactpy import component, html + + +# start +@component +def form(status="empty"): + if status == "success": + return html.h1("That's right!") + + return html.div( + html.h2("City quiz"), + html.p( + "In which city is there a billboard that turns air into drinkable water?" + ), + html.form( + html.textarea({"disabled": status == "submitting"}), + html.br(), + html.button( + {"disabled": status in {"empty", "submitting"}}, + "Submit", + ), + ( + html.p( + {"className": "Error"}, "Good guess but a wrong answer. Try again!" + ) + if status == "error" + else None + ), + ), + ) + + +@component +def app(): + """Render every visual state at once -- a "living styleguide". + + Because the mock is driven by a prop, all five states can sit side by side + in one page and be compared without clicking anything. + """ + statuses = ["empty", "typing", "submitting", "success", "error"] + return html.div( + [ + html.section( + {"key": status}, + html.h4(status), + form(status=status), + ) + for status in statuses + ] + ) diff --git a/docs/examples/reacting_to_input_with_state/styleguide/styles.css b/docs/examples/reacting_to_input_with_state/styleguide/styles.css new file mode 100644 index 000000000..e2aebc265 --- /dev/null +++ b/docs/examples/reacting_to_input_with_state/styleguide/styles.css @@ -0,0 +1,19 @@ +.Error { + color: red; +} + +textarea { + width: 260px; + height: 60px; +} + +section { + border-bottom: 1px solid #ddd; + padding-bottom: 10px; + margin-bottom: 10px; +} + +h4 { + margin: 4px 0; + color: #888; +} diff --git a/docs/examples/reference_client_api/hand_written_bootstrap/main.py b/docs/examples/reference_client_api/hand_written_bootstrap/main.py new file mode 100644 index 000000000..5c089aa98 --- /dev/null +++ b/docs/examples/reference_client_api/hand_written_bootstrap/main.py @@ -0,0 +1,49 @@ +# start + +from starlette.applications import Starlette +from starlette.responses import HTMLResponse +from starlette.routing import Route + +from reactpy import component, html, use_state +from reactpy.executors.asgi import ReactPyMiddleware + +PATH_PREFIX = "/reactpy/" + + +@component +def counter(): + count, set_count = use_state(0) + + async def increment(_event): + set_count(count + 1) + + return html.div( + html.h1(f"count is {count}"), + html.button({"onClick": increment}, "Add"), + ) + + +async def home(request): + # pathPrefix and the bundle's URL both have to agree with the server's, + # so both are interpolated from one value rather than typed twice. + return HTMLResponse(f""" + + +
+ + + """) + + +my_app = ReactPyMiddleware( + Starlette(routes=[Route("/", home)]), + root_components=["main.counter"], +) diff --git a/docs/examples/reference_client_api/import_map/main.py b/docs/examples/reference_client_api/import_map/main.py new file mode 100644 index 000000000..c4bafc8b2 --- /dev/null +++ b/docs/examples/reference_client_api/import_map/main.py @@ -0,0 +1,33 @@ +# start + +from pathlib import Path + +from starlette.applications import Starlette +from starlette.responses import FileResponse +from starlette.routing import Route + +from reactpy import component, html, use_state +from reactpy.executors.asgi import ReactPyMiddleware + + +@component +def counter(): + count, set_count = use_state(0) + + async def increment(_event): + set_count(count + 1) + + return html.div( + html.h1(f"count is {count}"), + html.button({"onClick": increment}, "Add"), + ) + + +async def home(request): + return FileResponse(Path(__file__).parent / "page.html") + + +my_app = ReactPyMiddleware( + Starlette(routes=[Route("/", home)]), + root_components=["main.counter"], +) diff --git a/docs/examples/reference_client_api/import_map/page.html b/docs/examples/reference_client_api/import_map/page.html new file mode 100644 index 000000000..bfed444e1 --- /dev/null +++ b/docs/examples/reference_client_api/import_map/page.html @@ -0,0 +1,28 @@ + + + + Counter + + + +
+ + + diff --git a/docs/examples/reference_common_props/attributes/app.py b/docs/examples/reference_common_props/attributes/app.py new file mode 100644 index 000000000..929d39587 --- /dev/null +++ b/docs/examples/reference_common_props/attributes/app.py @@ -0,0 +1,48 @@ +from reactpy import component, html + +# start + +ROWS = [ + {"id": 1, "name": "Dana", "role": "admin", "active": True}, + {"id": 2, "name": "Sam", "role": "viewer", "active": False}, +] + + +@component +def row(person): + # `key` lives in attributes, `data-*` and `aria-*` are forwarded verbatim, + # and `class` works exactly like `className`. Style values carry their own + # units, because a bare number is not portable across executors. + return html.tr( + { + "key": person["id"], + "class": "is-active" if person["active"] else "is-inactive", + "data-role": person["role"], + "aria-label": f"{person['name']}, {person['role']}", + "style": {"opacity": "1" if person["active"] else "0.55"}, + }, + html.td(person["name"]), + html.td(person["role"]), + html.td( + {"style": {"text-align": "right", "font-variant-numeric": "tabular-nums"}}, + "yes" if person["active"] else "no", + ), + ) + + +@component +def people_table(): + return html.table( + None, + html.thead( + html.tr( + None, + html.th("Name"), + html.th("Role"), + html.th({"scope": "col"}, "Active"), + ) + ), + # A list of children: every row needs a key or state will follow the + # row's position rather than the person. + html.tbody(None, [row(person) for person in ROWS]), + ) diff --git a/docs/examples/reference_jinja/component_tag/home.html b/docs/examples/reference_jinja/component_tag/home.html new file mode 100644 index 000000000..0ce4c0546 --- /dev/null +++ b/docs/examples/reference_jinja/component_tag/home.html @@ -0,0 +1,18 @@ + + + + + Accounts + + + +

Welcome

+ + {# One tag per component; ReactPy gives each mount element its own id. #} + {% component "pages.counter" %} + + {# `class` must follow a comma -- the tag parses its arguments as Python. #} + {% component "pages.badge", class="badge" %} + + + diff --git a/docs/examples/reference_jinja/component_tag/main.py b/docs/examples/reference_jinja/component_tag/main.py new file mode 100644 index 000000000..588d0c532 --- /dev/null +++ b/docs/examples/reference_jinja/component_tag/main.py @@ -0,0 +1,32 @@ +from jinja2 import Environment, FileSystemLoader +from starlette.applications import Starlette +from starlette.routing import Route +from starlette.templating import Jinja2Templates + +from reactpy.executors.asgi import ReactPyMiddleware +from reactpy.templatetags import ReactPyJinja + +# start +templates = Jinja2Templates( + env=Environment( + loader=FileSystemLoader("templates"), + extensions=[ReactPyJinja], + autoescape=True, + ) +) + + +async def home(request): + return templates.TemplateResponse(request, "home.html") + + +starlette_app = Starlette(routes=[Route("/", home)]) + +# The dotted path in the template is resolved on the server, not in the +# template: the browser asks ReactPy to render `pages.counter`, and ReactPy +# imports it to satisfy that request. Register exactly the paths the template +# passes to `{% component %}` -- here the `counter` and `badge` functions in +# the sibling `pages.py`. +my_app = ReactPyMiddleware( + starlette_app, root_components=["pages.counter", "pages.badge"] +) diff --git a/docs/examples/reference_jinja/component_tag/pages.py b/docs/examples/reference_jinja/component_tag/pages.py new file mode 100644 index 000000000..bf2f58307 --- /dev/null +++ b/docs/examples/reference_jinja/component_tag/pages.py @@ -0,0 +1,24 @@ +from reactpy import component, html, use_state + + +@component +def counter(): + count, set_count = use_state(0) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.button({"onClick": handle_count_click}, "Add 1"), + ) + + +@component +def badge(): + # The tag's `class` argument styles the mount element, not this markup, so + # the component is free to render whatever it likes inside it. + return html.div( + html.span({"class": "badge-label"}, "Status:"), + html.span({"class": "badge-value"}, "active"), + ) diff --git a/docs/examples/reference_jinja/pyscript_component/counter.py b/docs/examples/reference_jinja/pyscript_component/counter.py new file mode 100644 index 000000000..087e3be83 --- /dev/null +++ b/docs/examples/reference_jinja/pyscript_component/counter.py @@ -0,0 +1,19 @@ +from reactpy import component, html, use_state + +# start + + +@component +def root(): + # `root` is the name the tag looks for by default; pass root="other" to + # point it somewhere else. This file is inlined into the page at render + # time and runs in the browser, so treat all of it as public. + count, set_count = use_state(0) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.button({"onClick": handle_count_click}, "Add 1"), + ) diff --git a/docs/examples/reference_protocol_structure/counter_app/main.py b/docs/examples/reference_protocol_structure/counter_app/main.py new file mode 100644 index 000000000..e443cb29d --- /dev/null +++ b/docs/examples/reference_protocol_structure/counter_app/main.py @@ -0,0 +1,37 @@ +# start + +from starlette.applications import Starlette +from starlette.responses import HTMLResponse +from starlette.routing import Route + +from reactpy import component, html, use_state +from reactpy.executors.asgi import ReactPyMiddleware +from reactpy.executors.utils import server_side_component_html + + +@component +def counter(): + count, set_count = use_state(0) + + async def increment(_event): + set_count(count + 1) + + return html.div( + html.h1(f"count is {count}"), + html.button({"onClick": increment}, "Add"), + ) + + +async def home(request): + return HTMLResponse( + "" + "" + + server_side_component_html("root", "", "main.counter/") + + "" + ) + + +my_app = ReactPyMiddleware( + Starlette(routes=[Route("/", home)]), + root_components=["main.counter"], +) diff --git a/docs/examples/reference_protocol_structure/observe_protocol/client.py b/docs/examples/reference_protocol_structure/observe_protocol/client.py new file mode 100644 index 000000000..18ce629b9 --- /dev/null +++ b/docs/examples/reference_protocol_structure/observe_protocol/client.py @@ -0,0 +1,101 @@ +# start + +import asyncio +import json + +import websockets + +URL = "ws://127.0.0.1:8000/reactpy/main.counter/?path=%2F" + + +async def main(): + patches = asyncio.Queue() + + async with websockets.connect(URL) as socket: + mount = json.loads(await socket.recv()) + print("1. the mount update:", mount["path"] or "(root)") + print(json.dumps(mount["model"], indent=2)) + + target = find_handler_target(mount["model"]) + print("\n2. sending a click to", target) + await socket.send( + json.dumps( + { + "type": "layout-event", + "target": target, + # The client keeps the DOM event and sends a handle, not a copy + # of it. "type" is the one plain value sent along up front. + "data": [ + {"__reactpy_event__": {"id": "lzk4q1-0"}, "type": "click"} + ], + "seq": 1, + } + ) + ) + + # One reader for the whole connection: answer proxy requests as they arrive + # and hand everything else to the caller. Two consumers on one websocket + # would race for each frame. With a handler which only calls a state setter + # — the counter this observes — no proxy request is ever sent, and this task + # simply forwards patches. + reading = asyncio.create_task(read(socket, patches)) + try: + patch = await asyncio.wait_for(patches.get(), timeout=10) + print("\n3. the reply is a patch at path:", repr(patch["path"])) + print(json.dumps(patch["model"], indent=2)) + finally: + reading.cancel() + + +async def read(socket, patches): + async for raw in socket: + message = json.loads(raw) + if message.get("type") == "event-proxy-request": + await answer_requests(socket, message) + else: + await patches.put(message) + + +async def answer_requests(socket, message): + """Reply to every property path the server is asking about""" + requests = message.get("requests", [message]) + for request in requests: + print(" the server asks for", request["path"]) + if "requests" in message: + await socket.send( + json.dumps( + { + "type": "event-proxy-response", + "responses": [answer(request) for request in requests], + } + ) + ) + else: + await socket.send(json.dumps(answer(requests[0]))) + + +def answer(request): + """The value behind one property path, in the shape the client must use""" + return { + "type": "event-proxy-response", + "requestId": request["requestId"], + "ok": True, + "value": "typed text" if request["path"][-1] == "value" else None, + "error": None, + "code": None, + } + + +def find_handler_target(model): + """Return the target of the first event handler found in a model.""" + if isinstance(model, dict): + for spec in (model.get("eventHandlers") or {}).values(): + return spec["target"] + for child in model.get("children", []): + found = find_handler_target(child) + if found: + return found + return None + + +asyncio.run(main()) diff --git a/docs/examples/reference_reactpy/config/app.py b/docs/examples/reference_reactpy/config/app.py new file mode 100644 index 000000000..08cbaa4e1 --- /dev/null +++ b/docs/examples/reference_reactpy/config/app.py @@ -0,0 +1,19 @@ +from reactpy import component, html +from reactpy.executors.asgi import ReactPy + + +@component +def page(): + return html.div(html.h1("Hello"), html.p("Welcome to the app.")) + + +# start +my_app = ReactPy( + page, + path_prefix="/apps/reactpy/", + reconnect_interval=500, + reconnect_max_interval=30000, + reconnect_max_retries=50, + async_rendering=True, + debug=False, +) diff --git a/docs/examples/reference_reactpy/head_and_lang/app.py b/docs/examples/reference_reactpy/head_and_lang/app.py new file mode 100644 index 000000000..05a379931 --- /dev/null +++ b/docs/examples/reference_reactpy/head_and_lang/app.py @@ -0,0 +1,22 @@ +from reactpy import component, html +from reactpy.executors.asgi import ReactPy + + +@component +def page(): + return html.div(html.h1("Hello"), html.p("Welcome to the app.")) + + +# start +my_app = ReactPy( + page, + # Must be built with html.head(); any other tag raises ValueError. + html_head=html.head( + html.title("My ReactPy App"), + html.meta({"name": "description", "content": "A ReactPy app"}), + ), + html_lang="fr", + http_headers={"X-Frame-Options": "DENY"}, + # Rendered into the before the mount point, on the server. + prepend_body=html.header({"id": "nav"}, "Server-rendered navigation"), +) diff --git a/docs/examples/reference_reactpy/standalone/app.py b/docs/examples/reference_reactpy/standalone/app.py new file mode 100644 index 000000000..1a853c525 --- /dev/null +++ b/docs/examples/reference_reactpy/standalone/app.py @@ -0,0 +1,19 @@ +from reactpy import component, html, use_state +from reactpy.executors.asgi import ReactPy + + +@component +def counter(): + count, set_count = use_state(0) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.button({"onClick": handle_count_click}, "Add 1"), + ) + + +# start +my_app = ReactPy(counter) diff --git a/docs/examples/reference_reactpy_csr/csr/main.py b/docs/examples/reference_reactpy_csr/csr/main.py new file mode 100644 index 000000000..b649e2d28 --- /dev/null +++ b/docs/examples/reference_reactpy_csr/csr/main.py @@ -0,0 +1,10 @@ +from pathlib import Path + +from reactpy.executors.asgi import ReactPyCsr + +# start +my_app = ReactPyCsr( + Path(__file__).parent / "root.py", + initial="Loading...", + root_name="root", +) diff --git a/docs/examples/reference_reactpy_csr/csr_root/root.py b/docs/examples/reference_reactpy_csr/csr_root/root.py new file mode 100644 index 000000000..da0a9a33d --- /dev/null +++ b/docs/examples/reference_reactpy_csr/csr_root/root.py @@ -0,0 +1,14 @@ +from reactpy import component, html, use_state + + +@component +def root(): + count, set_count = use_state(0) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.button({"onClick": handle_count_click}, "Add 1"), + ) diff --git a/docs/examples/reference_reactpy_middleware/mount_snippet/main.py b/docs/examples/reference_reactpy_middleware/mount_snippet/main.py new file mode 100644 index 000000000..50177c66b --- /dev/null +++ b/docs/examples/reference_reactpy_middleware/mount_snippet/main.py @@ -0,0 +1,37 @@ +from starlette.applications import Starlette +from starlette.responses import HTMLResponse +from starlette.routing import Route + +from reactpy import component, html, use_state +from reactpy.executors.asgi import ReactPyMiddleware +from reactpy.executors.utils import server_side_component_html + + +@component +def counter(): + count, set_count = use_state(0) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.button({"onClick": handle_count_click}, "Add 1"), + ) + + +# start +async def home(request): + # The mount point plus the client bootstrap, for one registered component. + mount = server_side_component_html( + element_id="app", + class_="", + # Trailing slash required: the websocket route matches `/`. + component_path="main.counter/", + ) + return HTMLResponse(f'
{mount}
') + + +starlette_app = Starlette(routes=[Route("/", home)]) + +my_app = ReactPyMiddleware(starlette_app, root_components=["main.counter"]) diff --git a/docs/examples/reference_reactpy_middleware/wrapper/main.py b/docs/examples/reference_reactpy_middleware/wrapper/main.py new file mode 100644 index 000000000..d7bd6b66f --- /dev/null +++ b/docs/examples/reference_reactpy_middleware/wrapper/main.py @@ -0,0 +1,29 @@ +from starlette.applications import Starlette +from starlette.responses import PlainTextResponse +from starlette.routing import Route + +from reactpy import component, html, use_state +from reactpy.executors.asgi import ReactPyMiddleware + + +@component +def counter(): + count, set_count = use_state(0) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.button({"onClick": handle_count_click}, "Add 1"), + ) + + +async def health(request): + return PlainTextResponse("ok") + + +starlette_app = Starlette(routes=[Route("/healthz", health)]) + +# start +my_app = ReactPyMiddleware(starlette_app, root_components=["main.counter"]) diff --git a/docs/examples/reference_rules_of_hooks/conditional_hook/app.py b/docs/examples/reference_rules_of_hooks/conditional_hook/app.py new file mode 100644 index 000000000..59db65e83 --- /dev/null +++ b/docs/examples/reference_rules_of_hooks/conditional_hook/app.py @@ -0,0 +1,59 @@ +from reactpy import component, html, use_state + +# start + + +@component +def broken_toggle(): + """Do not copy this: a hook behind a condition that can change. + + `hidden` occupies slot 1 only while `show` is true. Clicking Toggle makes + that call disappear, so every hook after it shifts up one slot and starts + reading somebody else's value -- `count` begins showing the string that + belongs to `hidden`. No error is raised, on the server or in the browser. + """ + show, set_show = use_state(True) # slot 0 + + if show: + hidden, set_hidden = use_state("NOT-A-NUMBER") # slot 1, conditionally + + count, set_count = use_state(0) # slot 1 when `show` is False + + async def handle_show_click(event): + set_show(not show) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.p(f"show={show}"), + html.button({"onClick": handle_show_click}, "Toggle"), + html.button({"onClick": handle_count_click}, "Add 1"), + ) + + +@component +def fixed_toggle(): + """The same component with the hook order held constant. + + Every hook runs on every render, so the slots stay aligned with the + variables they belong to. Branching happens in the *return value*, which is + what rendering is for. + """ + show, set_show = use_state(True) + hidden, set_hidden = use_state("NOT-A-NUMBER") + count, set_count = use_state(0) + + async def handle_show_click(event): + set_show(not show) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.p(f"show={show} hidden={hidden}"), + html.button({"onClick": handle_show_click}, "Toggle"), + html.button({"onClick": handle_count_click}, "Add 1"), + ) diff --git a/docs/examples/reference_rules_of_react/impure_render/app.py b/docs/examples/reference_rules_of_react/impure_render/app.py new file mode 100644 index 000000000..0fc3b6696 --- /dev/null +++ b/docs/examples/reference_rules_of_react/impure_render/app.py @@ -0,0 +1,29 @@ +from reactpy import component, html, use_state + +# start + +# Module-level state is shared by every connection and survives across renders, +# so `total` is not part of the component's inputs at all. Two browsers, or two +# renders of the same component, see the same counter. +total = 0 + + +@component +def impure_counter(): + global total + count, set_count = use_state(0) + + # Render mutates module state: an outside effect, and a value that changes + # what the next render returns even though nothing the component received + # changed. Nothing warns about this -- the symptom is a number that does not + # match the button presses. + total += 1 + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.p(f"renders (module-wide): {total}"), + html.button({"onClick": handle_count_click}, "Add 1"), + ) diff --git a/docs/examples/reference_rules_of_react/pure_render/app.py b/docs/examples/reference_rules_of_react/pure_render/app.py new file mode 100644 index 000000000..7b547b02c --- /dev/null +++ b/docs/examples/reference_rules_of_react/pure_render/app.py @@ -0,0 +1,20 @@ +from reactpy import component, html, use_state + +# start + + +@component +def counter(): + count, set_count = use_state(0) + + # Nothing outside this component is touched. `count` comes from use_state, + # which ReactPy keeps per component instance -- so two connections to this + # same component each have their own number, and re-running the render + # changes nothing but the value returned. + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.h1(count), + html.button({"onClick": handle_count_click}, "Add 1"), + ) diff --git a/docs/examples/reference_usage/custom_elements/app.py b/docs/examples/reference_usage/custom_elements/app.py new file mode 100644 index 000000000..eb08dd37c --- /dev/null +++ b/docs/examples/reference_usage/custom_elements/app.py @@ -0,0 +1,33 @@ +from reactpy import component, html + +# start + +# Any name is a tag: underscores become dashes, so this is . +user_badge = html.user_badge + + +@component +def profile(name, role): + return html.section( + {"class": "profile", "data-role": role}, + html.h2(name), + # A plain string is a text child; an attribute dict is always first. + html.p({"class": "muted"}, "Signed in as ", html.strong(name)), + user_badge({"role": role, "label": role.title()}), + html.footer( + html.small( + {"style": {"font-size": "12px", "color": "#666"}}, + "Last seen today", + ) + ), + ) + + +@component +def app(): + return html.main( + None, + html.h1("Accounts"), + profile("Ada", "admin"), + profile("Linus", "viewer"), + ) diff --git a/docs/examples/reference_usage/tags/app.py b/docs/examples/reference_usage/tags/app.py new file mode 100644 index 000000000..ec3c26e25 --- /dev/null +++ b/docs/examples/reference_usage/tags/app.py @@ -0,0 +1,35 @@ +from reactpy import component, html + +# start + + +@component +def article_card(): + # Children are positional. The first argument is attributes only if it is a + # plain dict, which is why a leading `None` is needed when there are no + # attributes but the first child could look like one. + return html.article( + {"class": "card"}, + html.h2("Naming rules"), + html.p( + html.em("html.del_"), + " is ", + html.code(""), + ", ", + html.code("html.data_table"), + " is ", + html.code(""), + ".", + ), + html.ul( + None, + [html.li({"key": word}, word) for word in ("class", "style", "key")], + ), + html.hr(), + html.p({"class": "muted"}, "Void tags take no children."), + ) + + +@component +def app(): + return html.main(None, article_card(), html.br(), html.p("End of page.")) diff --git a/docs/examples/reference_use_callback/stable_identity/app.py b/docs/examples/reference_use_callback/stable_identity/app.py new file mode 100644 index 000000000..5bab8fb4a --- /dev/null +++ b/docs/examples/reference_use_callback/stable_identity/app.py @@ -0,0 +1,39 @@ +from data import bus + +from reactpy import component, html, use_callback, use_effect, use_state + +# start + + +@component +def listener(): + renders, set_renders = use_state(0) + heard, set_heard = use_state(0) + + # The registry lives outside ReactPy, so nothing deduplicates it for you. + # A plain `def` here would be a brand new function object on every render, + # and the registry would therefore collect another handler each time -- + # all of which receive every message. Pinning the identity keeps exactly + # one handler, however often this component renders. + handler = use_callback( + lambda message: set_heard(lambda previous: previous + 1), dependencies=[] + ) + + def subscribe(): + bus.subscribe(handler) + + use_effect(subscribe, dependencies=None) + + async def handle_renders_click(event): + set_renders(renders + 1) + + async def handle_click(event): + bus.publish("ping") + + return html.div( + html.p(f"renders: {renders}"), + html.p(f"handlers registered: {len(bus.handlers)}"), + html.p(f"this component heard: {heard}"), + html.button({"onClick": handle_renders_click}, "Re-render"), + html.button({"onClick": handle_click}, "Publish"), + ) diff --git a/docs/examples/reference_use_callback/stable_identity/data.py b/docs/examples/reference_use_callback/stable_identity/data.py new file mode 100644 index 000000000..5cd31b789 --- /dev/null +++ b/docs/examples/reference_use_callback/stable_identity/data.py @@ -0,0 +1,26 @@ +"""A stand-in for any registry that lives outside ReactPy. + +This is the shape of code where a callback's identity actually matters: the +registry is ordinary Python, so nothing deduplicates it for you -- it stores +handlers by identity, and a *new* function object on each render means a new +entry each render. +""" + + +class Bus: + def __init__(self): + self.handlers = [] + self.handled = [] + + def subscribe(self, handler): + # `in` on a list of plain functions compares by identity, which is + # exactly what a new `def` per render changes. + if handler not in self.handlers: + self.handlers.append(handler) + + def publish(self, message): + for handler in list(self.handlers): + handler(message) + + +bus = Bus() diff --git a/docs/examples/reference_use_callback/stale_handler/app.py b/docs/examples/reference_use_callback/stale_handler/app.py new file mode 100644 index 000000000..208843a29 --- /dev/null +++ b/docs/examples/reference_use_callback/stale_handler/app.py @@ -0,0 +1,38 @@ +from reactpy import component, html, use_callback, use_state + +# start + + +@component +def notes(): + notes_list, set_notes_list = use_state([]) + draft, set_draft = use_state("") + + # `draft` is read here, but `dependencies=[]` promises the function never + # changes -- so ReactPy keeps handing back the *first* function, which + # closed over an empty `draft`. Each click adds a blank note no matter what + # you typed. Listing what the handler reads is what fixes it. + async def add_note_handler(event): + set_notes_list([*notes_list, draft]) + + add_note = use_callback(add_note_handler, dependencies=[]) + + async def handle_draft_change(event): + set_draft(await event.target.value) + + return html.div( + html.h1("Notes"), + html.ul( + [ + html.li({"key": str(i)}, note or "(blank)") + for i, note in enumerate(notes_list) + ] + ), + html.input( + { + "value": draft, + "onChange": handle_draft_change, + } + ), + html.button({"onClick": add_note}, "Add note"), + ) diff --git a/docs/examples/reference_use_callback/tracked_handler/app.py b/docs/examples/reference_use_callback/tracked_handler/app.py new file mode 100644 index 000000000..0b0e0d88f --- /dev/null +++ b/docs/examples/reference_use_callback/tracked_handler/app.py @@ -0,0 +1,37 @@ +from reactpy import component, html, use_callback, use_state + +# start + + +@component +def notes(): + notes_list, set_notes_list = use_state([]) + draft, set_draft = use_state("") + + # List what the handler reads and the cached function is replaced exactly + # when those values change: the click always sees the current draft, while + # the identity still stays put between changes. + async def add_note_handler(event): + set_notes_list([*notes_list, draft]) + + add_note = use_callback(add_note_handler, dependencies=[notes_list, draft]) + + async def handle_draft_change(event): + set_draft(await event.target.value) + + return html.div( + html.h1("Notes"), + html.ul( + [ + html.li({"key": str(i)}, note or "(blank)") + for i, note in enumerate(notes_list) + ] + ), + html.input( + { + "value": draft, + "onChange": handle_draft_change, + } + ), + html.button({"onClick": add_note}, "Add note"), + ) diff --git a/docs/examples/reference_use_connection/inspect_client/app.py b/docs/examples/reference_use_connection/inspect_client/app.py new file mode 100644 index 000000000..4d29aac6b --- /dev/null +++ b/docs/examples/reference_use_connection/inspect_client/app.py @@ -0,0 +1,19 @@ +from reactpy import component, html, use_connection + +# start + + +@component +def connection_report(): + connection = use_connection() + + # Three things, three sources: `scope` is what the ASGI server received, + # `location` is the URL the browser reported, `carrier` is the object + # mediating the connection (a websocket for the ASGI executors). + return html.div( + html.h1("Who is connected"), + html.p(f"peer: {connection.scope['client']}"), + html.p(f"page: {connection.location.path}"), + html.p(f"protocol: {connection.scope['type']}"), + html.p(f"carrier: {type(connection.carrier).__name__}"), + ) diff --git a/docs/examples/reference_use_context/default_value/app.py b/docs/examples/reference_use_context/default_value/app.py new file mode 100644 index 000000000..118a7ccc0 --- /dev/null +++ b/docs/examples/reference_use_context/default_value/app.py @@ -0,0 +1,29 @@ +# start +from reactpy import component, create_context, html, use_context +from reactpy.types import Context + +# The annotation is what lets the value be a list *or* the None default. Without +# it, create_context(None) fixes the value type to None and a type checker then +# rejects `value=[...]`. +Cart: Context[list[str] | None] = create_context(None) + + +@component +def cart_badge(): + # With no provider above, use_context returns the default given to + # create_context. Choosing that default well is what lets a component + # render -- or be tested -- on its own, with no provider wrapper. + items = use_context(Cart) + if items is None: + return html.p("cart: not available") + return html.p(f"cart: {len(items)} item(s)") + + +@component +def shop(): + return html.div(cart_badge()) + + +@component +def shop_with_cart(): + return Cart(shop(), value=["widget", "gadget", "gizmo"]) diff --git a/docs/examples/reference_use_context/reading_in_an_effect/app.py b/docs/examples/reference_use_context/reading_in_an_effect/app.py new file mode 100644 index 000000000..df3de9beb --- /dev/null +++ b/docs/examples/reference_use_context/reading_in_an_effect/app.py @@ -0,0 +1,20 @@ +from reactpy import component, create_context, html, use_context, use_effect + +# start + +Config = create_context({"endpoint": "https://api.example"}) + + +@component +def status(): + # Read the context HERE, during the render. An effect body runs later, in + # its own task, where no hook is active -- calling use_context there fails + # with `RuntimeError: No life cycle hook is active.` Capture the value in a + # local and let the closure carry it into the effect. + config = use_context(Config) + + def on_mount(): + print(f"polling {config['endpoint']}") + + use_effect(on_mount, dependencies=[config]) + return html.p(f"endpoint: {config['endpoint']}") diff --git a/docs/examples/reference_use_context/theme_provider/app.py b/docs/examples/reference_use_context/theme_provider/app.py new file mode 100644 index 000000000..9bf5cb762 --- /dev/null +++ b/docs/examples/reference_use_context/theme_provider/app.py @@ -0,0 +1,42 @@ +from reactpy import component, create_context, html, use_context, use_state + +# start + +# One context per concept, created once at module level. The argument is the +# value descendants get when nothing provides one -- a sensible default means +# a component can render before any provider exists. +Theme = create_context("light") + + +@component +def swatch(): + # Reads whatever the nearest provider above it supplied. It takes no props: + # the value arrives without anything in between having to pass it along. + theme = use_context(Theme) + return html.div( + html.p(f"theme: {theme}"), + html.div({"style": {"background": theme, "height": "2rem"}}, ""), + ) + + +@component +def toolbar(): + # A component in the middle. It neither receives nor cares about `theme`; + # it only has to render its children for them to reach the provider above. + return html.div(html.h2("Toolbar"), swatch()) + + +@component +def app(): + theme, set_theme = use_state("light") + + async def handle_theme_click(event): + set_theme("dark" if theme == "light" else "light") + + return html.div( + Theme(toolbar(), value=theme), + html.button( + {"onClick": handle_theme_click}, + f"Switch to {'dark' if theme == 'light' else 'light'}", + ), + ) diff --git a/docs/examples/reference_use_debug_value/custom_hook/app.py b/docs/examples/reference_use_debug_value/custom_hook/app.py new file mode 100644 index 000000000..98fe30d84 --- /dev/null +++ b/docs/examples/reference_use_debug_value/custom_hook/app.py @@ -0,0 +1,28 @@ +from reactpy import component, html, use_debug_value, use_state + +# start + + +def use_cart(): + """A custom hook: it holds the cart and reports what it holds.""" + items, set_items = use_state([]) + + # Logged to the server as `use_cart's component(...) ` whenever the + # message differs from the last one logged. A plain string here would be + # built on every render; reading state makes it change only when it should. + use_debug_value(f"{len(items)} item(s): {', '.join(items) or '-'}") + + async def add(event=None): + set_items([*items, f"item {len(items) + 1}"]) + + return items, add + + +@component +def shop(): + items, add = use_cart() + return html.div( + html.h1(f"Cart ({len(items)})"), + html.ul([html.li({"key": i}, item) for i, item in enumerate(items)]), + html.button({"onClick": add}, "Add item"), + ) diff --git a/docs/examples/reference_use_debug_value/expensive_message/app.py b/docs/examples/reference_use_debug_value/expensive_message/app.py new file mode 100644 index 000000000..f574b515a --- /dev/null +++ b/docs/examples/reference_use_debug_value/expensive_message/app.py @@ -0,0 +1,25 @@ +from reactpy import component, html, use_debug_value, use_state + +# start + + +@component +def report(): + rows, set_rows = use_state([{"name": "ada", "score": 91}]) + + # Pass a function when turning the message into text is not free. ReactPy + # memoizes it like any other hook, so it re-runs only when what it reads + # changes -- and not at all on renders that would not have logged anything. + def describe(): + best = max(rows, key=lambda row: row["score"]) + return f"{len(rows)} rows, best={best['name']}:{best['score']}" + + use_debug_value(describe) + + async def add_row(event=None): + set_rows([*rows, {"name": f"row {len(rows)}", "score": len(rows) * 7}]) + + return html.div( + html.h1(f"{len(rows)} rows"), + html.button({"onClick": add_row}, "Add row"), + ) diff --git a/docs/examples/reference_use_effect/async_clock/app.py b/docs/examples/reference_use_effect/async_clock/app.py new file mode 100644 index 000000000..376189dfe --- /dev/null +++ b/docs/examples/reference_use_effect/async_clock/app.py @@ -0,0 +1,23 @@ +# start +import asyncio +from datetime import UTC, datetime + +from reactpy import component, html, use_async_effect, use_state + + +@component +def clock(): + now, set_now = use_state(datetime.now(UTC)) + + async def effect(): + # Zero arguments. A long-running effect loops and honours cancellation. + while True: + set_now(datetime.now(UTC)) + try: + await asyncio.sleep(1) + except asyncio.CancelledError: + break + + use_async_effect(effect) + + return html.h1(now.strftime("%H:%M:%S")) diff --git a/docs/examples/reference_use_effect/cleanup/app.py b/docs/examples/reference_use_effect/cleanup/app.py new file mode 100644 index 000000000..7aaa09667 --- /dev/null +++ b/docs/examples/reference_use_effect/cleanup/app.py @@ -0,0 +1,38 @@ +from reactpy import component, html, use_effect, use_state + + +@component +def connection(): + status, set_status = use_state("offline") + + def effect(): + # Returning a function registers it as clean-up. ReactPy runs it + # before the next effect for this component, and again on unmount. + set_status("online") + print("connected") + + def cleanup(): + print("disconnected") + + return cleanup + + use_effect(effect, dependencies=[]) + + return html.p(status) + + +# start +@component +def app(): + connected, set_connected = use_state(False) + + async def handle_connected_click(event): + set_connected(not connected) + + return html.div( + html.button( + {"onClick": handle_connected_click}, + "Connect" if not connected else "Disconnect", + ), + *([] if not connected else [connection()]), + ) diff --git a/docs/examples/reference_use_effect/dependencies/app.py b/docs/examples/reference_use_effect/dependencies/app.py new file mode 100644 index 000000000..b01dea3a3 --- /dev/null +++ b/docs/examples/reference_use_effect/dependencies/app.py @@ -0,0 +1,35 @@ +from data import ticker + +from reactpy import component, html, use_effect, use_state + +# start + + +@component +def live_feed(): + topic, set_topic = use_state("news") + latest, set_latest = use_state("") + + def effect(): + # Re-runs whenever `topic` changes; the returned clean-up unsubscribes + # the previous topic first, so you never keep two subscriptions. + unsubscribe = ticker.subscribe(topic, set_latest) + return unsubscribe + + use_effect(effect, dependencies=[topic]) + + async def handle_topic_click(event): + set_topic("sports" if topic == "news" else "news") + + async def handle_click(event): + ticker.publish(topic, "ping") + + return html.div( + html.p(f"Subscribed to: {topic}"), + html.p(latest or "(nothing yet)"), + html.button( + {"onClick": handle_topic_click}, + "Switch topic", + ), + html.button({"onClick": handle_click}, "Publish"), + ) diff --git a/docs/examples/reference_use_effect/dependencies/data.py b/docs/examples/reference_use_effect/dependencies/data.py new file mode 100644 index 000000000..487e7fb00 --- /dev/null +++ b/docs/examples/reference_use_effect/dependencies/data.py @@ -0,0 +1,24 @@ +"""A stand-in for any external system with subscribe/unsubscribe semantics.""" + + +class Ticker: + def __init__(self): + self.topics = {} + + def subscribe(self, topic, callback): + self.topics.setdefault(topic, []).append(callback) + + def unsubscribe(): + self.topics[topic].remove(callback) + + return unsubscribe + + def publish(self, topic, message): + for callback in list(self.topics.get(topic, [])): + callback(message) + + def count(self): + return sum(len(v) for v in self.topics.values()) + + +ticker = Ticker() diff --git a/docs/examples/reference_use_effect/inferred/app.py b/docs/examples/reference_use_effect/inferred/app.py new file mode 100644 index 000000000..c35f13af7 --- /dev/null +++ b/docs/examples/reference_use_effect/inferred/app.py @@ -0,0 +1,32 @@ +from reactpy import component, html, use_effect, use_state + + +# start +@component +def logger(): + count, set_count = use_state(0) + label, set_label = use_state("counter") + runs, set_runs = use_state([]) + + def effect(): + # No `dependencies` argument, so ReactPy infers them from the names + # this closure reads. Only `count` is a real dependency here -- the + # setter is stable, and `label` is not read at all. So pressing + # Rename re-renders the component but does NOT re-run this effect. + set_runs(lambda previous: [*previous, f"effect ran with count={count}"]) + + use_effect(effect) + + async def handle_count_click(event): + set_count(count + 1) + + async def handle_label_click(event): + set_label(label + "!") + + return html.div( + html.h1(count), + html.button({"onClick": handle_count_click}, "Add 1"), + html.button({"onClick": handle_label_click}, "Rename"), + html.p(f"Label: {label}"), + html.ul([html.li({"key": i}, line) for i, line in enumerate(runs)]), + ) diff --git a/docs/examples/reference_use_effect/minimal/app.py b/docs/examples/reference_use_effect/minimal/app.py new file mode 100644 index 000000000..36229f1bd --- /dev/null +++ b/docs/examples/reference_use_effect/minimal/app.py @@ -0,0 +1,28 @@ +from data import ticker + +from reactpy import component, html, use_effect, use_state + +# start + + +@component +def live_feed(): + latest, set_latest = use_state("waiting for a message...") + + def effect(): + # Runs once on mount; the returned function runs on unmount. + unsubscribe = ticker.subscribe("news", set_latest) + return unsubscribe + + use_effect(effect, dependencies=[]) + + async def handle_click(event): + ticker.publish("news", "Hello!") + + return html.div( + html.h1(latest), + html.button( + {"onClick": handle_click}, + "Publish", + ), + ) diff --git a/docs/examples/reference_use_effect/minimal/data.py b/docs/examples/reference_use_effect/minimal/data.py new file mode 100644 index 000000000..487e7fb00 --- /dev/null +++ b/docs/examples/reference_use_effect/minimal/data.py @@ -0,0 +1,24 @@ +"""A stand-in for any external system with subscribe/unsubscribe semantics.""" + + +class Ticker: + def __init__(self): + self.topics = {} + + def subscribe(self, topic, callback): + self.topics.setdefault(topic, []).append(callback) + + def unsubscribe(): + self.topics[topic].remove(callback) + + return unsubscribe + + def publish(self, topic, message): + for callback in list(self.topics.get(topic, [])): + callback(message) + + def count(self): + return sum(len(v) for v in self.topics.values()) + + +ticker = Ticker() diff --git a/docs/examples/reference_use_location/breadcrumb/app.py b/docs/examples/reference_use_location/breadcrumb/app.py new file mode 100644 index 000000000..1e997ed50 --- /dev/null +++ b/docs/examples/reference_use_location/breadcrumb/app.py @@ -0,0 +1,21 @@ +from reactpy import component, html, use_location + +# start + + +@component +def breadcrumb(): + location = use_location() + + # `path` mirrors `window.location.pathname`, so a component rendered on the + # ReactPy server can describe the page the browser is actually on -- useful + # for navigation that needs to know where it is. + parts = [part for part in location.path.split("/") if part] + + return html.nav( + html.a({"href": "/"}, "home"), + *[ + html.span(" / ", html.a({"href": "/" + "/".join(parts[: i + 1])}, part)) + for i, part in enumerate(parts) + ], + ) diff --git a/docs/examples/reference_use_location/query_params/app.py b/docs/examples/reference_use_location/query_params/app.py new file mode 100644 index 000000000..b1e0bb1c4 --- /dev/null +++ b/docs/examples/reference_use_location/query_params/app.py @@ -0,0 +1,24 @@ +from urllib.parse import parse_qs + +from reactpy import component, html, use_location + +# start + + +@component +def product_page(): + location = use_location() + + # `query_string` mirrors `window.location.search`, so it *keeps* the + # leading "?". Strip it first or the first parameter gets a mangled name: + # parse_qs("?tab=2&sort=price") yields the key "?tab", not "tab". + params = parse_qs(location.query_string.lstrip("?")) + tab = params.get("tab", ["overview"])[0] + sort = params.get("sort", ["name"])[0] + + return html.div( + html.h1(f"Products — {tab}"), + html.p(f"sorted by: {sort}"), + html.p(f"raw query string: {location.query_string!r}"), + html.p(f"path: {location.path}"), + ) diff --git a/docs/examples/reference_use_memo/dependencies_none/app.py b/docs/examples/reference_use_memo/dependencies_none/app.py new file mode 100644 index 000000000..f26adbd57 --- /dev/null +++ b/docs/examples/reference_use_memo/dependencies_none/app.py @@ -0,0 +1,36 @@ +from reactpy import component, html, use_memo, use_state + +# start + +# Module-level, so it is *not* a closure variable and ReactPy cannot infer it +# as a dependency. +config = {"prefix": "count"} + +calls = [] # demo-only + + +@component +def counter(): + count, set_count = use_state(0) + suffix, set_suffix = use_state("") + + def label(): + # Reads `config`, which ReactPy cannot see as a dependency. With + # inferred (i.e. empty) dependencies this would be computed once and + # then never notice an edited `config`. `dependencies=None` says: this + # depends on the render itself, so recompute every time. + calls.append(1) + return f"{config['prefix']}: {count}{suffix}" + + async def handle_count_click(event): + set_count(count + 1) + + async def handle_suffix_click(event): + set_suffix("!") + + return html.div( + html.h1(use_memo(label, dependencies=None)), + html.p(f"label() ran {len(calls)} time(s)"), + html.button({"onClick": handle_count_click}, "Count +1"), + html.button({"onClick": handle_suffix_click}, "Set suffix"), + ) diff --git a/docs/examples/reference_use_memo/deps_inferred/app.py b/docs/examples/reference_use_memo/deps_inferred/app.py new file mode 100644 index 000000000..120be620b --- /dev/null +++ b/docs/examples/reference_use_memo/deps_inferred/app.py @@ -0,0 +1,39 @@ +from reactpy import component, html, use_memo, use_state + +# start + +counts = [] # demo-only: lets you *see* how often `count` actually runs + + +@component +def word_count(): + text, set_text = use_state("reactpy is a python ui library") + upper, set_upper = use_state(False) + + # No `dependencies` argument at all. ReactPy inspects the function's + # closure and infers the free variables it reads -- here that is `text`, + # but not `upper`, so toggling the case does not redo the count. + def count(): + counts.append(1) + return len(text.split()) + + total = use_memo(count) + + async def handle_upper_click(event): + set_upper(not upper) + + async def handle_text_change(event): + set_text(await event.target.value) + + return html.div( + html.h1(f"{total} words"), + html.p(text.upper() if upper else text), + html.p(f"counted {len(counts)} time(s)"), + html.button({"onClick": handle_upper_click}, "Toggle case"), + html.input( + { + "value": text, + "onChange": handle_text_change, + } + ), + ) diff --git a/docs/examples/reference_use_memo/expensive_calc/app.py b/docs/examples/reference_use_memo/expensive_calc/app.py new file mode 100644 index 000000000..3e5c810a3 --- /dev/null +++ b/docs/examples/reference_use_memo/expensive_calc/app.py @@ -0,0 +1,53 @@ +from reactpy import component, html, use_memo, use_state + +# start + +ROSTER = [ + ("ada", 91), + ("grace", 120), + ("alan", 77), + ("edsger", 64), + ("katherine", 143), + ("dorothy", 88), + ("margaret", 155), +] + +sorts = [] # demo-only: lets you *see* how often `rank` actually runs + + +@component +def leaderboard(): + players, set_players = use_state( + [{"name": name, "score": score} for name, score in ROSTER] + ) + highlight, set_highlight = use_state("ada") + + def rank(): + # The work you want to skip: sorting the whole table. Listing + # `dependencies=[players]` runs this only when the roster changes, so + # changing the highlight leaves the sorted list alone. + sorts.append(1) + return sorted(players, key=lambda player: -player["score"]) + + ranked = use_memo(rank, dependencies=[players]) + + async def handle_highlight_click(event): + set_highlight("grace" if highlight == "ada" else "ada") + + async def handle_players_click(event): + set_players([{**player, "score": player["score"] + 1} for player in players]) + + return html.div( + html.h1("Leaderboard"), + html.ol([html.li(f"{player['name']}: {player['score']}") for player in ranked]), + html.p(f"sorted {len(sorts)} time(s)"), + html.p(f"highlighted: {highlight}"), + html.button( + {"onClick": handle_highlight_click}, + "Change highlight", + ), + html.button( + {"onClick": handle_players_click}, + "Everyone scores +1", + ), + ) diff --git a/docs/examples/reference_use_memo/missing_dependency/app.py b/docs/examples/reference_use_memo/missing_dependency/app.py new file mode 100644 index 000000000..a798ea71e --- /dev/null +++ b/docs/examples/reference_use_memo/missing_dependency/app.py @@ -0,0 +1,35 @@ +from reactpy import component, html, use_memo, use_state + +# start + + +@component +def totals(): + items, set_items = use_state([1, 2, 3]) + tax_rate, set_tax_rate = use_state(0.0) + + # `tax_rate` is read here but deliberately left out of `dependencies`. + # ReactPy does not complain. The cached total just keeps the rate from the + # render that last computed it, so "Add 10% tax" appears to do nothing -- + # until something that *is* a dependency changes, and the stale number + # jumps to the right one all at once. + def grand_total(): + return sum(items) * (1 + tax_rate) + + total = use_memo(grand_total, dependencies=[items]) + + async def handle_tax_rate_click(event): + set_tax_rate(tax_rate + 0.1) + + async def handle_items_click(event): + set_items([*items, len(items) + 1]) + + return html.div( + html.h1(f"total: {total:.2f}"), + html.p(f"tax rate: {tax_rate:.0%}"), + html.button({"onClick": handle_tax_rate_click}, "Add 10% tax"), + html.button( + {"onClick": handle_items_click}, + "Add an item", + ), + ) diff --git a/docs/examples/reference_use_reducer/expensive_init/app.py b/docs/examples/reference_use_reducer/expensive_init/app.py new file mode 100644 index 000000000..d7a2f68af --- /dev/null +++ b/docs/examples/reference_use_reducer/expensive_init/app.py @@ -0,0 +1,38 @@ +from reactpy import component, html, use_reducer + +# start + +loads = [] # demo-only: shows how often the initializer actually runs + + +def load_config(): + """Stand-in for anything slow: reading a file, querying a database.""" + loads.append(1) + return {"rows": [{"name": n} for n in ("alpha", "beta", "gamma")], "selected": None} + + +def reducer(state, action): + if action["type"] == "select": + return {**state, "selected": action["name"]} + return state + + +@component +def report(): + # Pass a function as the initial state and ReactPy calls it once, on the + # first render. Passing `load_config()` instead would call it on every + # render and throw every result but the first away. + state, dispatch = use_reducer(reducer, load_config) + + async def handle_click(event): + dispatch({"type": "select", "name": state["rows"][0]["name"]}) + + return html.div( + html.h1(f"{len(state['rows'])} rows"), + html.p(f"selected: {state['selected'] or '-'}"), + html.p(f"initializer ran {len(loads)} time(s)"), + html.button( + {"onClick": handle_click}, + "Select first", + ), + ) diff --git a/docs/examples/reference_use_reducer/replace_not_mutate/app.py b/docs/examples/reference_use_reducer/replace_not_mutate/app.py new file mode 100644 index 000000000..cec102917 --- /dev/null +++ b/docs/examples/reference_use_reducer/replace_not_mutate/app.py @@ -0,0 +1,25 @@ +from reactpy import component, html, use_reducer + +# start + + +def counter_reducer(state, action): + # Always return a NEW object. `use_reducer` is built on `use_state`, which + # compares what you return against what it already holds and schedules no + # render when the two are equal. Mutating `state` here and returning it + # would update the data, schedule nothing, and fail with no error and + # nothing in the browser console. + return {**state, "count": state["count"] + 1} + + +@component +def counter(): + state, dispatch = use_reducer(counter_reducer, {"count": 0}) + + async def handle_click(event): + dispatch({"type": "inc"}) + + return html.div( + html.h1(state["count"]), + html.button({"onClick": handle_click}, "Add 1"), + ) diff --git a/docs/examples/reference_use_reducer/stale_reducer/app.py b/docs/examples/reference_use_reducer/stale_reducer/app.py new file mode 100644 index 000000000..baea5286e --- /dev/null +++ b/docs/examples/reference_use_reducer/stale_reducer/app.py @@ -0,0 +1,29 @@ +from reactpy import component, html, use_reducer, use_state + +# start + + +@component +def stepper(): + step, set_step = use_state(1) + + def reducer(state, action): + # BUG: this reducer closes over `step`, but ReactPy builds the + # dispatcher once, on the first render, and keeps that reducer + # forever. Bump `step` to 3 and this keeps adding 1. + return {**state, "total": state["total"] + step} + + state, dispatch = use_reducer(reducer, {"total": 0}) + + async def handle_click(event): + dispatch({"type": "add"}) + + async def handle_step_click(event): + set_step(step + 1) + + return html.div( + html.p(f"total: {state['total']}"), + html.p(f"step: {step}"), + html.button({"onClick": handle_click}, "Add step"), + html.button({"onClick": handle_step_click}, "Bump step"), + ) diff --git a/docs/examples/reference_use_reducer/todos/app.py b/docs/examples/reference_use_reducer/todos/app.py new file mode 100644 index 000000000..00e70937f --- /dev/null +++ b/docs/examples/reference_use_reducer/todos/app.py @@ -0,0 +1,69 @@ +from reactpy import component, html, use_reducer + +# start + + +def todos_reducer(state, action): + """One function per behaviour, chosen by the action's type. + + Defined at module level on purpose: ReactPy builds the dispatcher once, on + the first render, and keeps that reducer -- a reducer defined inside the + component would keep its first-render closure forever. + + Every branch returns a NEW dict. Returning the mutated `state` would be + dropped without a word. + """ + if action["type"] == "add": + return { + **state, + "items": [*state["items"], {"text": action["text"], "done": False}], + } + if action["type"] == "toggle": + items = [ + {**item, "done": not item["done"]} if i == action["index"] else item + for i, item in enumerate(state["items"]) + ] + return {**state, "items": items} + if action["type"] == "clear_done": + return {**state, "items": [i for i in state["items"] if not i["done"]]} + return state + + +@component +def todo_list(): + state, dispatch = use_reducer(todos_reducer, {"items": []}) + + async def handle_add_click(event): + dispatch({"type": "add", "text": f"task {len(state['items']) + 1}"}) + + async def handle_clear_done_click(event): + dispatch({"type": "clear_done"}) + + def make_toggle(index): + # A factory rather than a bound-argument lambda: a handler must be a + # coroutine, and a coroutine cannot be a lambda. + async def toggle(event): + dispatch({"type": "toggle", "index": index}) + + return toggle + + return html.div( + html.h1(f"To-do ({sum(1 for i in state['items'] if not i['done'])} left)"), + html.ul( + [ + html.li( + {"key": i}, + html.button( + {"onClick": make_toggle(i)}, + state["items"][i]["text"], + ), + ) + for i in range(len(state["items"])) + ] + ), + html.button( + {"onClick": handle_add_click}, + "Add task", + ), + html.button({"onClick": handle_clear_done_click}, "Clear done"), + ) diff --git a/docs/examples/reference_use_ref/expensive_object/app.py b/docs/examples/reference_use_ref/expensive_object/app.py new file mode 100644 index 000000000..5185b4497 --- /dev/null +++ b/docs/examples/reference_use_ref/expensive_object/app.py @@ -0,0 +1,27 @@ +from reactpy import Ref, component, html, use_ref + +# start + + +def build_word_index(): + """Stand-in for something slow: reading a file, opening a connection, etc.""" + return {word: len(word) for word in "react py is a python ui library".split()} + + +@component +def word_counter(): + # Built once, on the first render, and then reused for the life of the + # component -- no `dependencies`, no invalidation, nothing to keep in sync. + index: Ref[dict[str, int] | None] = use_ref(None) + if index.current is None: + index.current = build_word_index() + + return html.div( + html.h1("Word lengths"), + html.ul( + [ + html.li(f"{word}: {length}") + for word, length in sorted(index.current.items()) + ] + ), + ) diff --git a/docs/examples/reference_use_ref/no_rerender/app.py b/docs/examples/reference_use_ref/no_rerender/app.py new file mode 100644 index 000000000..d5c0c7bdd --- /dev/null +++ b/docs/examples/reference_use_ref/no_rerender/app.py @@ -0,0 +1,24 @@ +from reactpy import component, html, use_ref, use_state + +# start + + +@component +def counter(): + # Two buttons, one counter each. Only the state one causes a render, so the + # numbers only come back into sync on a render triggered by something else. + state_count, set_state_count = use_state(0) + ref_count = use_ref(0) + + async def bump_state(event=None): + set_state_count(state_count + 1) + + async def bump_ref(event=None): + ref_count.current += 1 # writes in place: no render is scheduled + + return html.div( + html.p(f"state: {state_count}"), + html.p(f"ref: {ref_count.current}"), + html.button({"onClick": bump_state}, "Increment state"), + html.button({"onClick": bump_ref}, "Increment ref"), + ) diff --git a/docs/examples/reference_use_ref/set_current/app.py b/docs/examples/reference_use_ref/set_current/app.py new file mode 100644 index 000000000..72dd8d42d --- /dev/null +++ b/docs/examples/reference_use_ref/set_current/app.py @@ -0,0 +1,32 @@ +from reactpy import component, html, use_ref, use_state + +# start + + +@component +def theme_picker(): + # `set_current` is the call-shaped version of `ref.current = value`, and it + # hands back the value it replaced. The return value is what makes the swap + # below one expression: an assignment cannot be an argument to set_theme(). + previous = use_ref("dark") + theme, set_theme = use_state("light") + + def pick(name): + # A factory: it returns a fresh coroutine per button, so each button + # gets its own handler and the theme it belongs to is bound by closure. + async def choose_theme(event): + previous.set_current(theme) # record the theme being left + set_theme(name) + + return choose_theme + + async def swap_back(event): + set_theme(previous.set_current(theme)) + + return html.div( + html.h1(f"{theme} theme"), + html.p(f"you were just using: {previous.current}"), + html.button({"onClick": swap_back}, "Swap back"), + html.button({"onClick": pick("light")}, "Light"), + html.button({"onClick": pick("dark")}, "Dark"), + ) diff --git a/docs/examples/reference_use_ref/stopwatch/app.py b/docs/examples/reference_use_ref/stopwatch/app.py new file mode 100644 index 000000000..24a6ced96 --- /dev/null +++ b/docs/examples/reference_use_ref/stopwatch/app.py @@ -0,0 +1,52 @@ +import asyncio +import time + +from reactpy import Ref, component, html, use_async_effect, use_ref, use_state + +# start + + +@component +def stopwatch(): + # What the UI renders lives in state. What only the handlers and the + # ticking effect read -- and which must never trigger a render of its own + # -- lives in a ref. + elapsed, set_elapsed = use_state(0.0) + running, set_running = use_state(False) + started_at: Ref[float | None] = use_ref(None) + + async def tick(): + # The ref is the same object on every render, so this long-lived loop + # always sees whatever the button handlers wrote into it. + while started_at.current is not None: + await asyncio.sleep(0.1) + set_elapsed(time.monotonic() - started_at.current) + + use_async_effect(tick, dependencies=[running]) + + async def start(event=None): + started_at.current = time.monotonic() + set_running(True) + + async def stop(event=None): + if started_at.current is not None: + # Write the exact final value; the ticking effect only samples + # every 0.1s, so relying on it alone would leave the display + # slightly behind. + set_elapsed(time.monotonic() - started_at.current) + started_at.current = None + set_running(False) + + async def reset(event=None): + started_at.current = None + set_running(False) + set_elapsed(0.0) + + return html.div( + html.h1(f"{elapsed:.1f}s"), + html.button( + {"onClick": stop if running else start}, + "Stop" if running else "Start", + ), + html.button({"onClick": reset}, "Reset"), + ) diff --git a/docs/examples/reference_use_scope/client_headers/app.py b/docs/examples/reference_use_scope/client_headers/app.py new file mode 100644 index 000000000..b369084a5 --- /dev/null +++ b/docs/examples/reference_use_scope/client_headers/app.py @@ -0,0 +1,32 @@ +from reactpy import component, html, use_scope + +# start + + +def header(scope, name): + """Read one request header out of an ASGI scope. + + Headers arrive as a list of ``(bytes, bytes)`` pairs, lower-cased by the + server, so both halves need decoding before you can compare them. + """ + wanted = name.lower().encode() + for key, value in scope.get("headers", []): + if key == wanted: + return value.decode() + return None + + +@component +def visitor(): + scope = use_scope() + # Note this is the *websocket* scope. Its "path" is the ReactPy websocket + # path, not the page the visitor is on -- use use_location() for that. + # `client` is legitimately absent or None behind some proxies, so read it + # through a local and check it rather than subscripting the scope directly. + client = scope.get("client") + peer = client[0] if client else "unknown" + return html.div( + html.h1(f"Hello, {peer}"), + html.p(f"browser: {header(scope, 'user-agent') or 'unknown'}"), + html.p(f"connection type: {scope['type']}"), + ) diff --git a/docs/examples/reference_use_state/checkbox/app.py b/docs/examples/reference_use_state/checkbox/app.py new file mode 100644 index 000000000..6e97d9f5b --- /dev/null +++ b/docs/examples/reference_use_state/checkbox/app.py @@ -0,0 +1,21 @@ +from reactpy import component, html, use_state + + +# start +@component +def checkbox(): + enabled, set_enabled = use_state(False) + + async def handle_change(event): + # `checked` is read from the live element, like any other property. + set_enabled(await event.target.checked) + + return html.div( + html.label( + html.input( + {"type": "checkbox", "checked": enabled, "onChange": handle_change} + ), + "Enabled", + ), + html.p(f"Status: {'on' if enabled else 'off'}"), + ) diff --git a/docs/examples/reference_use_state/counter/app.py b/docs/examples/reference_use_state/counter/app.py new file mode 100644 index 000000000..a3fb74340 --- /dev/null +++ b/docs/examples/reference_use_state/counter/app.py @@ -0,0 +1,15 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + step, set_step = use_state(1) + + async def handle_add(event): + set_step(step + 1) + + return html.div( + html.h1(step), + html.button({"onClick": handle_add}, "Add 1"), + ) diff --git a/docs/examples/reference_use_state/form/app.py b/docs/examples/reference_use_state/form/app.py new file mode 100644 index 000000000..43402ec65 --- /dev/null +++ b/docs/examples/reference_use_state/form/app.py @@ -0,0 +1,36 @@ +from reactpy import component, html, use_state + + +# start +@component +def form(): + first_name, set_first_name = use_state("Ada") + last_name, set_last_name = use_state("Lovelace") + + async def handle_first_name_change(event): + set_first_name(await event.target.value) + + async def handle_last_name_change(event): + set_last_name(await event.target.value) + + return html.div( + html.label( + "First name: ", + html.input( + { + "value": first_name, + "onChange": handle_first_name_change, + } + ), + ), + html.label( + "Last name: ", + html.input( + { + "value": last_name, + "onChange": handle_last_name_change, + } + ), + ), + html.h1(f"Hello, {first_name} {last_name}!"), + ) diff --git a/docs/examples/reference_use_state/initializer/app.py b/docs/examples/reference_use_state/initializer/app.py new file mode 100644 index 000000000..0fe7dbf7a --- /dev/null +++ b/docs/examples/reference_use_state/initializer/app.py @@ -0,0 +1,17 @@ +from reactpy import component, html, use_state + + +def create_expensive_state(): + # Runs once, on the first render only -- not on every re-render. + return {"items": [f"item {i}" for i in range(1000)]} + + +# start +@component +def list_page(): + state, set_state = use_state(create_expensive_state) + + return html.div( + html.h1(f"{len(state['items'])} items"), + html.p(state["items"][0]), + ) diff --git a/docs/examples/reference_use_state/previous_render/app.py b/docs/examples/reference_use_state/previous_render/app.py new file mode 100644 index 000000000..6f825d604 --- /dev/null +++ b/docs/examples/reference_use_state/previous_render/app.py @@ -0,0 +1,20 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + count, set_count = use_state(0) + history, set_history = use_state([]) + + async def handle_add(event): + # Read history through an updater: `history` in this closure is the + # value from the render that created the handler, not the latest. + set_history(lambda previous: [*previous, count]) + set_count(count + 1) + + return html.div( + html.h1(count), + html.button({"onClick": handle_add}, "Add 1"), + html.p("Previous values: " + (", ".join(map(str, history)) or "-")), + ) diff --git a/docs/examples/reference_use_state/reset_by_key/app.py b/docs/examples/reference_use_state/reset_by_key/app.py new file mode 100644 index 000000000..cdd5032b5 --- /dev/null +++ b/docs/examples/reference_use_state/reset_by_key/app.py @@ -0,0 +1,32 @@ +from reactpy import component, html, use_state + + +@component +def counter(email): + # A key on this call resets the counter whenever the address changes. + count, set_count = use_state(0) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.p(f"{email} opened {count} times"), + html.button({"onClick": handle_count_click}, "Open"), + ) + + +# start +@component +def inbox(): + email, set_email = use_state("ada@example.com") + + async def handle_email_click(event): + set_email("grace@example.com") + + return html.div( + html.button( + {"onClick": handle_email_click}, + "Switch account", + ), + counter(email, key=email), + ) diff --git a/docs/examples/reference_use_state/text_field/app.py b/docs/examples/reference_use_state/text_field/app.py new file mode 100644 index 000000000..5f522134d --- /dev/null +++ b/docs/examples/reference_use_state/text_field/app.py @@ -0,0 +1,20 @@ +from reactpy import component, html, use_state + + +# start +@component +def text_field(): + first_name, set_first_name = use_state("Ada") + + async def handle_first_name_change(event): + set_first_name(await event.target.value) + + return html.div( + html.input( + { + "value": first_name, + "onChange": handle_first_name_change, + } + ), + html.h1(f"Hello, {first_name}!"), + ) diff --git a/docs/examples/reference_use_state/updater/app.py b/docs/examples/reference_use_state/updater/app.py new file mode 100644 index 000000000..24a36b407 --- /dev/null +++ b/docs/examples/reference_use_state/updater/app.py @@ -0,0 +1,18 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + count, set_count = use_state(0) + + async def handle_add_three(event): + # Updaters receive the current value, so all three increments land. + set_count(lambda previous: previous + 1) + set_count(lambda previous: previous + 1) + set_count(lambda previous: previous + 1) + + return html.div( + html.h1(count), + html.button({"onClick": handle_add_three}, "+3"), + ) diff --git a/docs/examples/referencing_values_with_refs/counter/app.py b/docs/examples/referencing_values_with_refs/counter/app.py new file mode 100644 index 000000000..b910157b0 --- /dev/null +++ b/docs/examples/referencing_values_with_refs/counter/app.py @@ -0,0 +1,15 @@ +from reactpy import component, html, use_ref + + +# start +@component +def counter(): + clicks = use_ref(0) + + async def handle_click(event): + clicks.current += 1 + # Nothing on screen changes, but the server console proves the ref is + # really incrementing between clicks. + print(f"You clicked {clicks.current} times!") + + return html.button({"onClick": handle_click}, "Click me!") diff --git a/docs/examples/referencing_values_with_refs/ref_counter/app.py b/docs/examples/referencing_values_with_refs/ref_counter/app.py new file mode 100644 index 000000000..751ec6cc9 --- /dev/null +++ b/docs/examples/referencing_values_with_refs/ref_counter/app.py @@ -0,0 +1,13 @@ +from reactpy import component, html, use_ref + + +# start +@component +def counter(): + count = use_ref(0) + + async def handle_click(event): + # This doesn't re-render the component! + count.current += 1 + + return html.button({"onClick": handle_click}, f"You clicked {count.current} times") diff --git a/docs/examples/referencing_values_with_refs/state_counter/app.py b/docs/examples/referencing_values_with_refs/state_counter/app.py new file mode 100644 index 000000000..17aab6680 --- /dev/null +++ b/docs/examples/referencing_values_with_refs/state_counter/app.py @@ -0,0 +1,12 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + count, set_count = use_state(0) + + async def handle_click(event): + set_count(count + 1) + + return html.button({"onClick": handle_click}, f"You clicked {count} times") diff --git a/docs/examples/removing_effect_dependencies/equality_deps/app.py b/docs/examples/removing_effect_dependencies/equality_deps/app.py new file mode 100644 index 000000000..52d6f4c8d --- /dev/null +++ b/docs/examples/removing_effect_dependencies/equality_deps/app.py @@ -0,0 +1,47 @@ +"""React says a rebuilt object is a new dependency and will loop. In ReactPy +that is false: dependencies compare with `==`, so a rebuilt-but-equal dict +never re-triggers the Effect. Only real content changes do. +""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def feed(topic): + page, set_page = use_state(1) + rows, set_rows = use_state([]) + + # rebuilt on every render, but compares equal to the previous one + options = {"topic": topic, "per_page": 20} + + def load(): + print(f"loading {options} page={page}") + set_rows([f"{topic} row {i} (page {page})" for i in range(3)]) + + use_effect(load, dependencies=[options, page]) + + async def handle_page_click(e): + set_page(page + 1) + + return html.div( + html.button({"onClick": handle_page_click}, f"Page {page}"), + html.ul([html.li({"key": row}, row) for row in rows]), + ) + + +@component +def app(): + topic, set_topic = use_state("news") + refreshes, set_refreshes = use_state(0) + + async def handle_refreshes_click(e): + set_refreshes(refreshes + 1) + + return html.div( + html.button( + {"onClick": handle_refreshes_click}, + f"Re-render ({refreshes})", + ), + feed(topic), + ) diff --git a/docs/examples/removing_effect_dependencies/hoisted_constant/app.py b/docs/examples/removing_effect_dependencies/hoisted_constant/app.py new file mode 100644 index 000000000..a6608db89 --- /dev/null +++ b/docs/examples/removing_effect_dependencies/hoisted_constant/app.py @@ -0,0 +1,55 @@ +"""Values that cannot change across renders are not dependencies: hoist the +constant and the imported helper out of the component. +""" + +from reactpy import component, html, use_effect, use_state + +# start +PAGE_SIZE = 20 +HEADERS = {"Accept": "application/json"} + + +def format_rows(topic, raw): + """Module-level helper: same object on every render and every connection.""" + return [f"{topic}: {name}" for name in raw] + + +@component +def directory(topic): + query, set_query = use_state("") + rows, set_rows = use_state([]) + + def load(): + raw = [f"user-{i}" for i in range(PAGE_SIZE // 4)] + print(f"load topic={topic} page_size={PAGE_SIZE} headers={HEADERS}") + set_rows(format_rows(topic, raw)) + + # neither PAGE_SIZE, HEADERS, nor format_rows belongs in this list + use_effect(load, dependencies=[topic]) + + async def handle_query_change(e): + set_query(await e.target.value) + + return html.div( + html.input( + { + "value": query, + "onChange": handle_query_change, + } + ), + html.p(f"query={query!r} rows={len(rows)}"), + html.ul([html.li({"key": row}, row) for row in rows]), + ) + + +@component +def app(): + topic, set_topic = use_state("news") + + async def handle_topic_click(e): + set_topic(topic + "x") + + return html.div( + html.button({"onClick": handle_topic_click}, f"Topic {topic}"), + directory(topic), + ) diff --git a/docs/examples/removing_effect_dependencies/next_state_updater/app.py b/docs/examples/removing_effect_dependencies/next_state_updater/app.py new file mode 100644 index 000000000..bfb10875b --- /dev/null +++ b/docs/examples/removing_effect_dependencies/next_state_updater/app.py @@ -0,0 +1,44 @@ +"""Reading state only to calculate the next state does not make it a +dependency: the functional updater removes it. Listing it instead would make +this Effect re-run itself. +""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def log_viewer(topic): + lines, set_lines = use_state([]) + visible, set_visible = use_state(3) + + def record(): + # `lines` is only used to build the next value, so it is not a + # dependency — and listing it would make this Effect re-trigger itself + def append(current): + return [*current, f"opened {topic}"] + + set_lines(append) + + use_effect(record, dependencies=[topic]) + + async def handle_visible_click(e): + set_visible(visible + 1) + + return html.div( + html.button({"onClick": handle_visible_click}, f"Show {visible}"), + html.ul([html.li({"key": i}, line) for i, line in enumerate(lines[-visible:])]), + ) + + +@component +def app(): + topic, set_topic = use_state("news") + + async def handle_topic_click(e): + set_topic(topic + "x") + + return html.div( + html.button({"onClick": handle_topic_click}, f"Topic {topic}"), + log_viewer(topic), + ) diff --git a/docs/examples/removing_effect_dependencies/restarting_interval/app.py b/docs/examples/removing_effect_dependencies/restarting_interval/app.py new file mode 100644 index 000000000..9507cf645 --- /dev/null +++ b/docs/examples/removing_effect_dependencies/restarting_interval/app.py @@ -0,0 +1,50 @@ +"""The delay is in the dependency list, so every adjustment tears the poll loop +down and starts a new one — measured, four restarts where two were needed. +""" + +import asyncio + +from reactpy import component, html, use_async_effect, use_state + + +# start +@component +def poller(topic): + delay, set_delay = use_state(0.2) + polls, set_polls = use_state(0) + + async def loop(): + print(f"start polling {topic} delay={delay:.1f}s") + try: + while True: + await asyncio.sleep(delay) + print(f"tick {topic} delay={delay:.1f}s") + # the loop is created once per run, so read state via an updater + set_polls(lambda current: current + 1) + except asyncio.CancelledError: + print(f"stop polling {topic} delay={delay:.1f}s") + raise + + # `delay` here means adjusting it cancels and recreates the whole loop + use_async_effect(loop, dependencies=[topic, delay]) + + async def handle_delay_click(e): + set_delay(delay + 0.2) + + return html.div( + html.p(f"{topic}: {polls} polls"), + html.button({"onClick": handle_delay_click}, f"Delay {delay:.1f}s"), + ) + + +@component +def app(): + topic, set_topic = use_state("news") + + async def handle_topic_click(e): + set_topic(topic + "x") + + return html.div( + html.button({"onClick": handle_topic_click}, f"Topic {topic}"), + poller(topic), + ) diff --git a/docs/examples/removing_effect_dependencies/stable_interval/app.py b/docs/examples/removing_effect_dependencies/stable_interval/app.py new file mode 100644 index 000000000..bbd0db322 --- /dev/null +++ b/docs/examples/removing_effect_dependencies/stable_interval/app.py @@ -0,0 +1,53 @@ +"""A user-adjustable delay must not restart the poll loop. Keep it in a ref +that is updated during render, and leave it out of the dependency list. +""" + +import asyncio + +from reactpy import component, html, use_async_effect, use_ref, use_state + + +# start +@component +def poller(topic): + delay, set_delay = use_state(0.2) + polls, set_polls = use_state(0) + # updated during render, so the loop always sleeps with the newest value + delay_ref = use_ref(delay) + delay_ref.current = delay + + async def loop(): + print(f"start polling {topic}") + try: + while True: + await asyncio.sleep(delay_ref.current) + print(f"tick {topic} delay={delay_ref.current:.1f}s") + # the loop is created once, so read state through an updater + set_polls(lambda current: current + 1) + except asyncio.CancelledError: + print(f"stop polling {topic}") + raise + + # `delay` is deliberately absent: adjusting it must not reconnect + use_async_effect(loop, dependencies=[topic]) + + async def handle_delay_click(e): + set_delay(delay + 0.2) + + return html.div( + html.p(f"{topic}: {polls} polls"), + html.button({"onClick": handle_delay_click}, f"Delay {delay:.1f}s"), + ) + + +@component +def app(): + topic, set_topic = use_state("news") + + async def handle_topic_click(e): + set_topic(topic + "x") + + return html.div( + html.button({"onClick": handle_topic_click}, f"Topic {topic}"), + poller(topic), + ) diff --git a/docs/examples/removing_effect_dependencies/unread_dep/app.py b/docs/examples/removing_effect_dependencies/unread_dep/app.py new file mode 100644 index 000000000..0052127c0 --- /dev/null +++ b/docs/examples/removing_effect_dependencies/unread_dep/app.py @@ -0,0 +1,49 @@ +"""An Effect that writes a summary into state, but lists only one of the values +it reads. The summary keeps describing an older render and nothing warns you. +""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def reporter(topic): + count, set_count = use_state(0) + theme, set_theme = use_state("light") + summary, set_summary = use_state("no report yet") + + def report(): + # reads `count` and `theme`, but neither is in the dependency list + set_summary(f"topic={topic} count={count} theme={theme}") + print(f"report topic={topic} count={count} theme={theme}") + + use_effect(report, dependencies=[topic]) + + async def handle_count_click(e): + set_count(count + 1) + + async def handle_theme_click(e): + set_theme("dark" if theme == "light" else "light") + + return html.div( + html.button({"onClick": handle_count_click}, f"Count {count}"), + html.button( + {"onClick": handle_theme_click}, + f"Theme {theme}", + ), + html.p(f"live: count={count} theme={theme}"), + html.p(f"summary: {summary}"), + ) + + +@component +def app(): + topic, set_topic = use_state("news") + + async def handle_topic_click(e): + set_topic(topic + "x") + + return html.div( + html.button({"onClick": handle_topic_click}, f"Topic {topic}"), + reporter(topic), + ) diff --git a/docs/examples/removing_effect_dependencies/unrelated_effects/app.py b/docs/examples/removing_effect_dependencies/unrelated_effects/app.py new file mode 100644 index 000000000..5fb74a47c --- /dev/null +++ b/docs/examples/removing_effect_dependencies/unrelated_effects/app.py @@ -0,0 +1,54 @@ +"""Two synchronizations crammed into one Effect: changing the page tears the +connection down, and changing the topic re-fetches the page. Split them and +each owns its own dependencies and clean-up. +""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def feed(topic): + page, set_page = use_state(1) + rows, set_rows = use_state([]) + online, set_online = use_state(True) + + def subscribe(): + # one Effect doing two unrelated jobs: connect AND load rows + if not online: + print("offline — nothing to do") + return + print(f"connect {topic} page={page}") + set_rows([f"{topic} row {i} (page {page})" for i in range(3)]) + + def close(): + print(f"disconnect {topic} page={page}") + + return close + + use_effect(subscribe, dependencies=[topic, page, online]) + + async def handle_page_click(e): + set_page(page + 1) + + async def handle_online_click(e): + set_online(not online) + + return html.div( + html.button({"onClick": handle_page_click}, f"Page {page}"), + html.button({"onClick": handle_online_click}, "Toggle online"), + html.ul([html.li({"key": row}, row) for row in rows]), + ) + + +@component +def app(): + topic, set_topic = use_state("news") + + async def handle_topic_click(e): + set_topic(topic + "x") + + return html.div( + html.button({"onClick": handle_topic_click}, f"Topic {topic}"), + feed(topic), + ) diff --git a/docs/examples/render_and_commit/initial_render/app.py b/docs/examples/render_and_commit/initial_render/app.py new file mode 100644 index 000000000..a8ea9c6ca --- /dev/null +++ b/docs/examples/render_and_commit/initial_render/app.py @@ -0,0 +1,20 @@ +from reactpy import component, html +from reactpy.executors.asgi import ReactPy + +# start + + +@component +def image(): + return html.img( + { + "src": "../../assets/images/demos/ZF6s192.jpg", + "alt": "'Floralis Genérica' by Eduardo Catalano: a gigantic " + "metallic flower sculpture with reflective petals", + } + ) + + +# Mounting is what triggers the initial render. Which line you use depends on +# how the app is served; see the executors reference. +my_app = ReactPy(image) diff --git a/docs/examples/render_and_commit/re_render/app.py b/docs/examples/render_and_commit/re_render/app.py new file mode 100644 index 000000000..0b754d151 --- /dev/null +++ b/docs/examples/render_and_commit/re_render/app.py @@ -0,0 +1,19 @@ +import asyncio +from datetime import UTC, datetime + +from reactpy import component, html, use_async_effect, use_state + + +# start +@component +def clock(): + now, set_now = use_state(datetime.now(UTC)) + + async def tick(): + while True: + await asyncio.sleep(1) + set_now(datetime.now(UTC)) + + use_async_effect(tick, dependencies=[]) + + return html.h1(now.strftime("%H:%M:%S")) diff --git a/docs/examples/render_and_commit/uncontrolled_input/app.py b/docs/examples/render_and_commit/uncontrolled_input/app.py new file mode 100644 index 000000000..0cb609484 --- /dev/null +++ b/docs/examples/render_and_commit/uncontrolled_input/app.py @@ -0,0 +1,17 @@ +from reactpy import component, html, use_state + + +# start +@component +def app(): + count, set_count = use_state(0) + + async def handle_count_click(event): + set_count(count + 1) + + return html.div( + html.button({"onClick": handle_count_click}, "Re-render"), + html.p(f"Rendered {count} times"), + # No `value`: the browser owns this field, so ReactPy never rewrites it. + html.input({"placeholder": "Type here and then click the button"}), + ) diff --git a/docs/examples/rendering_lists/filtering/app.py b/docs/examples/rendering_lists/filtering/app.py new file mode 100644 index 000000000..a3d06c5f3 --- /dev/null +++ b/docs/examples/rendering_lists/filtering/app.py @@ -0,0 +1,22 @@ +from data import get_image_url, people + +from reactpy import component, html + +# start + + +@component +def people_list(): + chemists = [person for person in people if person["profession"] == "chemist"] + list_items = [ + html.li( + {"key": person["id"]}, + html.img({"src": get_image_url(person), "alt": person["name"]}), + html.p( + html.b(person["name"]), + f" {person['profession']} known for {person['accomplishment']}", + ), + ) + for person in chemists + ] + return html.ul(list_items) diff --git a/docs/examples/rendering_lists/filtering/data.py b/docs/examples/rendering_lists/filtering/data.py new file mode 100644 index 000000000..9dcc01877 --- /dev/null +++ b/docs/examples/rendering_lists/filtering/data.py @@ -0,0 +1,41 @@ +people = [ + { + "id": 0, # used as the key + "name": "Creola Katherine Johnson", + "profession": "mathematician", + "accomplishment": "spaceflight calculations", + "image_id": "MK3eW3A", + }, + { + "id": 1, # used as the key + "name": "Mario José Molina-Pasquel Henríquez", + "profession": "chemist", + "accomplishment": "discovery of Arctic ozone hole", + "image_id": "mynHUSa", + }, + { + "id": 2, # used as the key + "name": "Mohammad Abdus Salam", + "profession": "physicist", + "accomplishment": "electromagnetism theory", + "image_id": "bE7W1ji", + }, + { + "id": 3, # used as the key + "name": "Percy Lavon Julian", + "profession": "chemist", + "accomplishment": "pioneering cortisone drugs, steroids and birth control pills", + "image_id": "IOjWm71", + }, + { + "id": 4, # used as the key + "name": "Subrahmanyan Chandrasekhar", + "profession": "astrophysicist", + "accomplishment": "white dwarf star mass calculations", + "image_id": "lrWQx8l", + }, +] + + +def get_image_url(person): + return f"../../assets/images/demos/{person['image_id']}s.jpg" diff --git a/docs/examples/rendering_lists/filtering/styles.css b/docs/examples/rendering_lists/filtering/styles.css new file mode 100644 index 000000000..e6c836b7b --- /dev/null +++ b/docs/examples/rendering_lists/filtering/styles.css @@ -0,0 +1,18 @@ +ul { + list-style-type: none; + padding: 0 10px; +} + +li { + margin-bottom: 10px; + display: grid; + grid-template-columns: auto 1fr; + gap: 20px; + align-items: center; +} + +img { + width: 100px; + height: 100px; + border-radius: 50%; +} diff --git a/docs/examples/rendering_lists/fragments/app.py b/docs/examples/rendering_lists/fragments/app.py new file mode 100644 index 000000000..3c29888f0 --- /dev/null +++ b/docs/examples/rendering_lists/fragments/app.py @@ -0,0 +1,18 @@ +from data import people + +from reactpy import component, html + +# start + + +@component +def people_list(): + list_items = [ + html( + {"key": person["id"]}, + html.h1(person["name"]), + html.p(person["profession"]), + ) + for person in people + ] + return html.div(list_items) diff --git a/docs/examples/rendering_lists/fragments/data.py b/docs/examples/rendering_lists/fragments/data.py new file mode 100644 index 000000000..9dcc01877 --- /dev/null +++ b/docs/examples/rendering_lists/fragments/data.py @@ -0,0 +1,41 @@ +people = [ + { + "id": 0, # used as the key + "name": "Creola Katherine Johnson", + "profession": "mathematician", + "accomplishment": "spaceflight calculations", + "image_id": "MK3eW3A", + }, + { + "id": 1, # used as the key + "name": "Mario José Molina-Pasquel Henríquez", + "profession": "chemist", + "accomplishment": "discovery of Arctic ozone hole", + "image_id": "mynHUSa", + }, + { + "id": 2, # used as the key + "name": "Mohammad Abdus Salam", + "profession": "physicist", + "accomplishment": "electromagnetism theory", + "image_id": "bE7W1ji", + }, + { + "id": 3, # used as the key + "name": "Percy Lavon Julian", + "profession": "chemist", + "accomplishment": "pioneering cortisone drugs, steroids and birth control pills", + "image_id": "IOjWm71", + }, + { + "id": 4, # used as the key + "name": "Subrahmanyan Chandrasekhar", + "profession": "astrophysicist", + "accomplishment": "white dwarf star mass calculations", + "image_id": "lrWQx8l", + }, +] + + +def get_image_url(person): + return f"../../assets/images/demos/{person['image_id']}s.jpg" diff --git a/docs/examples/rendering_lists/from_array/app.py b/docs/examples/rendering_lists/from_array/app.py new file mode 100644 index 000000000..331e1f32d --- /dev/null +++ b/docs/examples/rendering_lists/from_array/app.py @@ -0,0 +1,16 @@ +from reactpy import component, html + +# start +people = [ + "Creola Katherine Johnson: mathematician", + "Mario José Molina-Pasquel Henríquez: chemist", + "Mohammad Abdus Salam: physicist", + "Percy Lavon Julian: chemist", + "Subrahmanyan Chandrasekhar: astrophysicist", +] + + +@component +def people_list(): + list_items = [html.li(person) for person in people] + return html.ul(list_items) diff --git a/docs/examples/rendering_lists/from_array/styles.css b/docs/examples/rendering_lists/from_array/styles.css new file mode 100644 index 000000000..9b143fdca --- /dev/null +++ b/docs/examples/rendering_lists/from_array/styles.css @@ -0,0 +1,3 @@ +li { + margin-bottom: 10px; +} diff --git a/docs/examples/rendering_lists/keyed/app.py b/docs/examples/rendering_lists/keyed/app.py new file mode 100644 index 000000000..6f582041b --- /dev/null +++ b/docs/examples/rendering_lists/keyed/app.py @@ -0,0 +1,21 @@ +from data import get_image_url, people + +from reactpy import component, html + +# start + + +@component +def people_list(): + list_items = [ + html.li( + {"key": person["id"]}, + html.img({"src": get_image_url(person), "alt": person["name"]}), + html.p( + html.b(person["name"]), + f" {person['profession']} known for {person['accomplishment']}", + ), + ) + for person in people + ] + return html.ul(list_items) diff --git a/docs/examples/rendering_lists/keyed/data.py b/docs/examples/rendering_lists/keyed/data.py new file mode 100644 index 000000000..9dcc01877 --- /dev/null +++ b/docs/examples/rendering_lists/keyed/data.py @@ -0,0 +1,41 @@ +people = [ + { + "id": 0, # used as the key + "name": "Creola Katherine Johnson", + "profession": "mathematician", + "accomplishment": "spaceflight calculations", + "image_id": "MK3eW3A", + }, + { + "id": 1, # used as the key + "name": "Mario José Molina-Pasquel Henríquez", + "profession": "chemist", + "accomplishment": "discovery of Arctic ozone hole", + "image_id": "mynHUSa", + }, + { + "id": 2, # used as the key + "name": "Mohammad Abdus Salam", + "profession": "physicist", + "accomplishment": "electromagnetism theory", + "image_id": "bE7W1ji", + }, + { + "id": 3, # used as the key + "name": "Percy Lavon Julian", + "profession": "chemist", + "accomplishment": "pioneering cortisone drugs, steroids and birth control pills", + "image_id": "IOjWm71", + }, + { + "id": 4, # used as the key + "name": "Subrahmanyan Chandrasekhar", + "profession": "astrophysicist", + "accomplishment": "white dwarf star mass calculations", + "image_id": "lrWQx8l", + }, +] + + +def get_image_url(person): + return f"../../assets/images/demos/{person['image_id']}s.jpg" diff --git a/docs/examples/rendering_lists/keyed/styles.css b/docs/examples/rendering_lists/keyed/styles.css new file mode 100644 index 000000000..e6c836b7b --- /dev/null +++ b/docs/examples/rendering_lists/keyed/styles.css @@ -0,0 +1,18 @@ +ul { + list-style-type: none; + padding: 0 10px; +} + +li { + margin-bottom: 10px; + display: grid; + grid-template-columns: auto 1fr; + gap: 20px; + align-items: center; +} + +img { + width: 100px; + height: 100px; + border-radius: 50%; +} diff --git a/docs/examples/responding_to_events/handler_props/app.py b/docs/examples/responding_to_events/handler_props/app.py new file mode 100644 index 000000000..f44ff1ff4 --- /dev/null +++ b/docs/examples/responding_to_events/handler_props/app.py @@ -0,0 +1,38 @@ +from reactpy import component, html, use_state + + +# start +@component +def button(on_click, *children): + return html.button({"onClick": on_click}, *children) + + +@component +def play_button(movie_name): + shown, set_shown = use_state("") + + async def handle_play_click(event): + set_shown(f"Playing {movie_name}!") + + return html.div( + button(handle_play_click, f'Play "{movie_name}"'), + html.p(shown), + ) + + +@component +def upload_button(): + shown, set_shown = use_state("") + + async def handle_upload_click(event): + set_shown("Uploading!") + + return html.div( + button(handle_upload_click, "Upload Image"), + html.p(shown), + ) + + +@component +def toolbar(): + return html.div(play_button("Kiki's Delivery Service"), upload_button()) diff --git a/docs/examples/responding_to_events/handler_props/styles.css b/docs/examples/responding_to_events/handler_props/styles.css new file mode 100644 index 000000000..4fc2d52a0 --- /dev/null +++ b/docs/examples/responding_to_events/handler_props/styles.css @@ -0,0 +1,3 @@ +button { + margin-right: 10px; +} diff --git a/docs/examples/responding_to_events/naming_props/app.py b/docs/examples/responding_to_events/naming_props/app.py new file mode 100644 index 000000000..7ef078c7b --- /dev/null +++ b/docs/examples/responding_to_events/naming_props/app.py @@ -0,0 +1,24 @@ +from reactpy import component, html, use_state + + +# start +@component +def button(on_smash, *children): + return html.button({"onClick": on_smash}, *children) + + +@component +def app(): + status, set_status = use_state("Nothing selected") + + async def handle_play(event): + set_status("Playing!") + + async def handle_upload(event): + set_status("Uploading!") + + return html.div( + html.p(status), + button(handle_play, "Play Movie"), + button(handle_upload, "Upload Image"), + ) diff --git a/docs/examples/responding_to_events/naming_props/styles.css b/docs/examples/responding_to_events/naming_props/styles.css new file mode 100644 index 000000000..4fc2d52a0 --- /dev/null +++ b/docs/examples/responding_to_events/naming_props/styles.css @@ -0,0 +1,3 @@ +button { + margin-right: 10px; +} diff --git a/docs/examples/responding_to_events/prevent_default/app.py b/docs/examples/responding_to_events/prevent_default/app.py new file mode 100644 index 000000000..6a00cee92 --- /dev/null +++ b/docs/examples/responding_to_events/prevent_default/app.py @@ -0,0 +1,25 @@ +from reactpy import component, html, use_state + +# start + + +@component +def signup(): + signed_up, set_signed_up = use_state(0) + name, set_name = use_state("") + + async def handle_name_change(event): + set_name(await event.target.value) + + async def handle_submit(event): + # Without this line the browser would navigate away from the page. + event.preventDefault() + set_signed_up(signed_up + 1) + set_name("") + + return html.form( + {"onSubmit": handle_submit}, + html.input({"value": name, "onChange": handle_name_change}), + html.button({"type": "submit"}, "Sign up"), + html.p(f"{signed_up} signed up"), + ) diff --git a/docs/examples/responding_to_events/prevent_default/styles.css b/docs/examples/responding_to_events/prevent_default/styles.css new file mode 100644 index 000000000..4fc2d52a0 --- /dev/null +++ b/docs/examples/responding_to_events/prevent_default/styles.css @@ -0,0 +1,3 @@ +button { + margin-right: 10px; +} diff --git a/docs/examples/responding_to_events/propagation/app.py b/docs/examples/responding_to_events/propagation/app.py new file mode 100644 index 000000000..9fcb685b8 --- /dev/null +++ b/docs/examples/responding_to_events/propagation/app.py @@ -0,0 +1,18 @@ +from reactpy import component, html, use_state + + +# start +@component +def toolbar(): + messages, set_messages = use_state([]) + + async def handle_toolbar_click(event): + set_messages([*messages, "You clicked on the toolbar!"]) + + async def handle_play_click(event): + set_messages([*messages, "Playing!"]) + + return html.div( + {"className": "Toolbar", "onClick": handle_toolbar_click}, + html.button({"onClick": handle_play_click}, "Play Movie"), + ) diff --git a/docs/examples/responding_to_events/propagation/styles.css b/docs/examples/responding_to_events/propagation/styles.css new file mode 100644 index 000000000..9609f9316 --- /dev/null +++ b/docs/examples/responding_to_events/propagation/styles.css @@ -0,0 +1,8 @@ +.toolbar { + background: #aaa; + padding: 5px; +} + +button { + margin: 5px; +} diff --git a/docs/examples/responding_to_events/reading_props/app.py b/docs/examples/responding_to_events/reading_props/app.py new file mode 100644 index 000000000..bdb661ffa --- /dev/null +++ b/docs/examples/responding_to_events/reading_props/app.py @@ -0,0 +1,23 @@ +from reactpy import component, html, use_state + + +# start +@component +def alert_button(message, *children): + shown, set_shown = use_state("") + + async def handle_click(event): + set_shown(message) + + return html.div( + html.button({"onClick": handle_click}, *children), + html.p(shown), + ) + + +@component +def toolbar(): + return html.div( + alert_button("Playing!", "Play Movie"), + alert_button("Uploading!", "Upload Image"), + ) diff --git a/docs/examples/responding_to_events/reading_props/styles.css b/docs/examples/responding_to_events/reading_props/styles.css new file mode 100644 index 000000000..4fc2d52a0 --- /dev/null +++ b/docs/examples/responding_to_events/reading_props/styles.css @@ -0,0 +1,3 @@ +button { + margin-right: 10px; +} diff --git a/docs/examples/responding_to_events/simple_button.py b/docs/examples/responding_to_events/simple_button.py new file mode 100644 index 000000000..7753b4a70 --- /dev/null +++ b/docs/examples/responding_to_events/simple_button.py @@ -0,0 +1,7 @@ +from reactpy import component, html + + +# start +@component +def button(): + return html.button("I don't do anything") diff --git a/docs/examples/responding_to_events/simple_button_event.css b/docs/examples/responding_to_events/simple_button_event.css new file mode 100644 index 000000000..4fc2d52a0 --- /dev/null +++ b/docs/examples/responding_to_events/simple_button_event.css @@ -0,0 +1,3 @@ +button { + margin-right: 10px; +} diff --git a/docs/examples/responding_to_events/simple_button_event.py b/docs/examples/responding_to_events/simple_button_event.py new file mode 100644 index 000000000..e940d7b5b --- /dev/null +++ b/docs/examples/responding_to_events/simple_button_event.py @@ -0,0 +1,9 @@ +from reactpy import component, html + + +@component +def button(): + async def handle_click(event): + print("You clicked me!") + + return html.button({"onClick": handle_click}, "Click me") diff --git a/docs/examples/responding_to_events/stop_propagation/app.py b/docs/examples/responding_to_events/stop_propagation/app.py new file mode 100644 index 000000000..ac5035187 --- /dev/null +++ b/docs/examples/responding_to_events/stop_propagation/app.py @@ -0,0 +1,29 @@ +from reactpy import component, html, use_state + + +# start +@component +def button(on_click, *children): + async def handle_click(event): + # Detected from this line, so the client stops the event before + # it reaches the parent's handler. + event.stopPropagation() + on_click() + + return html.button({"onClick": handle_click}, *children) + + +@component +def toolbar(): + messages, set_messages = use_state([]) + + async def handle_toolbar_click(event): + set_messages([*messages, "You clicked on the toolbar!"]) + + def handle_play_click(): + set_messages([*messages, "Playing!"]) + + return html.div( + {"className": "Toolbar", "onClick": handle_toolbar_click}, + button(handle_play_click, "Play Movie"), + ) diff --git a/docs/examples/responding_to_events/stop_propagation/styles.css b/docs/examples/responding_to_events/stop_propagation/styles.css new file mode 100644 index 000000000..9609f9316 --- /dev/null +++ b/docs/examples/responding_to_events/stop_propagation/styles.css @@ -0,0 +1,8 @@ +.toolbar { + background: #aaa; + padding: 5px; +} + +button { + margin: 5px; +} diff --git a/docs/examples/reusing_logic_with_custom_hooks/callback_dep/app.py b/docs/examples/reusing_logic_with_custom_hooks/callback_dep/app.py new file mode 100644 index 000000000..4b0821775 --- /dev/null +++ b/docs/examples/reusing_logic_with_custom_hooks/callback_dep/app.py @@ -0,0 +1,46 @@ +"""Wrong: the custom Hook lists the callback it was given as its only +dependency. A local `def` compares code-equal across renders, so the Effect +never restarts and keeps reporting through the first render's closure. +""" + +import asyncio + +from reactpy import component, html, use_async_effect, use_ref, use_state + + +# start +def use_polling(on_result): + async def loop(): + while True: + await asyncio.sleep(0.2) + on_result("poll finished") + + # `on_result` looks like a sensible dependency; it never counts as changed + use_async_effect(loop, dependencies=[on_result]) + + +@component +def dashboard(): + topic, set_topic = use_state("news") + last, set_last = use_state("nothing yet") + tick = use_ref(0) + + def on_result(value): + tick.current += 1 + # reads `topic` from whichever render created this callback + set_last(f"{value} #{tick.current} (handler saw topic={topic})") + + use_polling(on_result) + + async def handle_topic_click(e): + set_topic(topic + "x") + + return html.div( + html.button({"onClick": handle_topic_click}, f"Topic {topic}"), + html.p(last), + ) + + +@component +def app(): + return dashboard() diff --git a/docs/examples/reusing_logic_with_custom_hooks/extract_hook/app.py b/docs/examples/reusing_logic_with_custom_hooks/extract_hook/app.py new file mode 100644 index 000000000..780103665 --- /dev/null +++ b/docs/examples/reusing_logic_with_custom_hooks/extract_hook/app.py @@ -0,0 +1,48 @@ +"""A plain function that calls hooks is a custom Hook: it shares the logic, and +each component that calls it gets its own state. +""" + +from reactpy import component, html, use_state + + +# start +def use_cart(items): + """Selection logic for a list of items — the state lives in the caller.""" + selected, set_selected = use_state([]) + + def handler_for(item): + async def toggle(event): + if item in selected: + set_selected([name for name in selected if name != item]) + else: + set_selected([*selected, item]) + + return toggle + + return selected, handler_for + + +@component +def checkout(items): + selected, handler_for = use_cart(items) + total = 10 * len(selected) + return html.div( + html.ul( + [ + html.li( + {"key": item}, + html.button( + {"onClick": handler_for(item)}, + f"{item} {'[x]' if item in selected else '[ ]'}", + ), + ) + for item in items + ] + ), + html.p(f"{len(selected)} selected, total ${total}"), + ) + + +@component +def app(): + return checkout(["apple", "bread", "cheese"]) diff --git a/docs/examples/reusing_logic_with_custom_hooks/handler_argument/app.py b/docs/examples/reusing_logic_with_custom_hooks/handler_argument/app.py new file mode 100644 index 000000000..d0b0c7ba7 --- /dev/null +++ b/docs/examples/reusing_logic_with_custom_hooks/handler_argument/app.py @@ -0,0 +1,48 @@ +"""A custom Hook that takes a callback and lists it as a dependency silently +freezes: a local `def` compares code-equal, so the Effect keeps the first +render's closure. Read the callback through a ref instead. +""" + +import asyncio + +from reactpy import component, html, use_async_effect, use_ref, use_state + + +# start +def use_polling(topic, on_result): + latest = use_ref(on_result) + # always the newest callback, without making it a dependency + latest.current = on_result + + async def loop(): + while True: + await asyncio.sleep(0.2) + latest.current(f"result for {topic}") + + # deliberately excludes on_result: listing it would never help anyway + use_async_effect(loop, dependencies=[topic]) + + +@component +def dashboard(): + topic, set_topic = use_state("news") + last, set_last = use_state("nothing yet") + + def on_result(value): + # reads `topic` from the render that created it + set_last(f"{value} (topic was {topic})") + + use_polling(topic, on_result) + + async def handle_topic_click(e): + set_topic(topic + "x") + + return html.div( + html.button({"onClick": handle_topic_click}, f"Topic {topic}"), + html.p(last), + ) + + +@component +def app(): + return dashboard() diff --git a/docs/examples/reusing_logic_with_custom_hooks/hook_order_safe/app.py b/docs/examples/reusing_logic_with_custom_hooks/hook_order_safe/app.py new file mode 100644 index 000000000..e5aaad45e --- /dev/null +++ b/docs/examples/reusing_logic_with_custom_hooks/hook_order_safe/app.py @@ -0,0 +1,40 @@ +"""Correct: the hook always calls `use_state` the same number of times. The +branch only changes what is returned, so the slots stay put. +""" + +from reactpy import component, html, use_state + + +# start +def use_profile(show_nickname): + name, set_name = use_state("name") + # the hook is called unconditionally; only the returned value is conditional + nickname, _set_nickname = use_state("nickname") + bio, set_bio = use_state("bio") + return name, (nickname if show_nickname else "-"), bio, set_bio + + +@component +def profile(show_nickname): + name, nickname, bio, set_bio = use_profile(show_nickname) + + async def extend_bio(event): + set_bio(f"{bio}!") + + return html.div( + html.button({"onClick": extend_bio}, f"bio={bio}"), + html.p(f"name={name} nickname={nickname}"), + ) + + +@component +def app(): + show, set_show = use_state(True) + + async def handle_show_click(e): + set_show(not show) + + return html.div( + html.button({"onClick": handle_show_click}, "toggle nickname"), + profile(show), + ) diff --git a/docs/examples/reusing_logic_with_custom_hooks/hook_order_violation/app.py b/docs/examples/reusing_logic_with_custom_hooks/hook_order_violation/app.py new file mode 100644 index 000000000..bd910bd20 --- /dev/null +++ b/docs/examples/reusing_logic_with_custom_hooks/hook_order_violation/app.py @@ -0,0 +1,45 @@ +"""Wrong: a hook called inside an `if` inside a custom Hook. Nothing raises — +the state slots below simply shift, and a value from one hook appears in +another. +""" + +from reactpy import component, html, use_state + + +# start +def use_profile(show_nickname): + name, set_name = use_state("name") + if show_nickname: + nickname, set_nickname = use_state("nickname") + else: + nickname = "-" + # `bio` is the third hook when the branch is taken and the second when it + # is not, so it inherits the slot that `nickname` used to own + bio, set_bio = use_state("bio") + return name, nickname, bio, set_bio + + +@component +def profile(show_nickname): + name, nickname, bio, set_bio = use_profile(show_nickname) + + async def extend_bio(event): + set_bio(f"{bio}!") + + return html.div( + html.button({"onClick": extend_bio}, f"bio={bio}"), + html.p(f"name={name} nickname={nickname}"), + ) + + +@component +def app(): + show, set_show = use_state(True) + + async def handle_show_click(e): + set_show(not show) + + return html.div( + html.button({"onClick": handle_show_click}, "toggle nickname"), + profile(show), + ) diff --git a/docs/examples/reusing_logic_with_custom_hooks/isolated_state/app.py b/docs/examples/reusing_logic_with_custom_hooks/isolated_state/app.py new file mode 100644 index 000000000..4a1128008 --- /dev/null +++ b/docs/examples/reusing_logic_with_custom_hooks/isolated_state/app.py @@ -0,0 +1,31 @@ +"""Calling a custom Hook twice in one component gives two independent pieces of +state: hooks are matched by call order, not by name. +""" + +from reactpy import component, html, use_state + + +# start +def use_toggle(initial=False): + on, set_on = use_state(initial) + + async def flip(event): + set_on(not on) + + return on, flip + + +@component +def panel(): + left, flip_left = use_toggle() + right, flip_right = use_toggle() + return html.div( + html.button({"onClick": flip_left}, f"left {'ON' if left else 'off'}"), + html.button({"onClick": flip_right}, f"right {'ON' if right else 'off'}"), + html.p(f"both on: {left and right}"), + ) + + +@component +def app(): + return panel() diff --git a/docs/examples/scaling_up_with_reducer_and_context/task_context/app.py b/docs/examples/scaling_up_with_reducer_and_context/task_context/app.py new file mode 100644 index 000000000..ca4561723 --- /dev/null +++ b/docs/examples/scaling_up_with_reducer_and_context/task_context/app.py @@ -0,0 +1,63 @@ +"""State and dispatch reach the leaves through context, not props.""" + +from tasks import INITIAL_STATE, TaskContext, tasks_reducer + +from reactpy import component, html, use_context, use_reducer + + +# start +@component +def task_form(task): + # Reads dispatch from context; no prop arrived. + _, dispatch = use_context(TaskContext) + + async def handle_toggle(event): + dispatch({"type": "toggled", "id": task["id"]}) + + return html.div( + {"style": {"textDecoration": "line-through" if task["done"] else "none"}}, + task["text"], + html.button({"onClick": handle_toggle}, "Toggle"), + ) + + +@component +def task(task): + _, dispatch = use_context(TaskContext) + + async def handle_delete(event): + dispatch({"type": "deleted", "id": task["id"]}) + + return html.li( + {"key": task["id"]}, + task_form(task), + html.button({"onClick": handle_delete}, "Delete"), + ) + + +@component +def task_list(): + # Reads only the state; dispatch never has to come through here. + state, _ = use_context(TaskContext) + return html.ul([task(item, key=item["id"]) for item in state["tasks"]]) + + +@component +def task_app(): + state, dispatch = use_reducer(tasks_reducer, INITIAL_STATE) + + async def handle_add(event): + dispatch({"type": "added"}) + + return html.div( + html.h3("Prague itinerary"), + html.button({"onClick": handle_add}, "Add task"), + # One provider for the whole subtree. + TaskContext(task_list(), value=(state, dispatch)), + html.p({"id": "summary"}, f"{len(state['tasks'])} tasks"), + ) + + +@component +def app(): + return task_app() diff --git a/docs/examples/scaling_up_with_reducer_and_context/task_context/tasks.py b/docs/examples/scaling_up_with_reducer_and_context/task_context/tasks.py new file mode 100644 index 000000000..08fa53988 --- /dev/null +++ b/docs/examples/scaling_up_with_reducer_and_context/task_context/tasks.py @@ -0,0 +1,52 @@ +"""The reducer, the context, and the initial state — all outside the UI.""" + +from collections.abc import Callable +from typing import Any + +from reactpy import create_context +from reactpy.types import Context + +TaskState = dict[str, Any] +TaskValue = tuple[TaskState, Callable[[dict[str, Any]], None]] + +# The reducer never mutates what it is given, so one shared constant is +# safe as the initial state for every instance. +INITIAL_TASKS = [ + {"id": 0, "text": "Packing", "done": False}, + {"id": 1, "text": "Passport", "done": True}, +] + +INITIAL_STATE: TaskState = {"tasks": INITIAL_TASKS, "next_id": 2} + +# One context carries both halves: the state to read and the dispatch to change +# it with. Annotate the name, or the default narrows the value type. +TaskContext: Context[TaskValue] = create_context((INITIAL_STATE, lambda action: None)) + + +def tasks_reducer(state: TaskState, action: dict[str, Any]) -> TaskState: + kind = action["type"] + if kind == "added": + task = {"id": state["next_id"], "text": "New task", "done": False} + return { + **state, + "tasks": [*state["tasks"], task], + "next_id": state["next_id"] + 1, + } + if kind == "toggled": + return { + **state, + "tasks": [ + ( + {**task, "done": not task["done"]} + if task["id"] == action["id"] + else task + ) + for task in state["tasks"] + ], + } + if kind == "deleted": + return { + **state, + "tasks": [task for task in state["tasks"] if task["id"] != action["id"]], + } + raise ValueError(f"unknown action type {kind!r}") diff --git a/docs/examples/scaling_up_with_reducer_and_context/task_state_props/app.py b/docs/examples/scaling_up_with_reducer_and_context/task_state_props/app.py new file mode 100644 index 000000000..e0bd1c9aa --- /dev/null +++ b/docs/examples/scaling_up_with_reducer_and_context/task_state_props/app.py @@ -0,0 +1,92 @@ +"""State and dispatch threaded down through components that only forward them.""" + +from reactpy import component, html, use_reducer + +INITIAL_TASKS = [ + {"id": 0, "text": "Packing", "done": False}, + {"id": 1, "text": "Passport", "done": True}, +] + + +def tasks_reducer(state, action): + kind = action["type"] + if kind == "added": + task = {"id": state["next_id"], "text": "New task", "done": False} + return { + **state, + "tasks": [*state["tasks"], task], + "next_id": state["next_id"] + 1, + } + if kind == "toggled": + return { + **state, + "tasks": [ + ( + {**task, "done": not task["done"]} + if task["id"] == action["id"] + else task + ) + for task in state["tasks"] + ], + } + if kind == "deleted": + return { + **state, + "tasks": [task for task in state["tasks"] if task["id"] != action["id"]], + } + raise ValueError(f"unknown action type {kind!r}") + + +# start +@component +def task_form(state, dispatch, task): + # The only component that actually wants dispatch... + async def handle_toggle(event): + dispatch({"type": "toggled", "id": task["id"]}) + + return html.div( + {"style": {"textDecoration": "line-through" if task["done"] else "none"}}, + task["text"], + html.button({"onClick": handle_toggle}, "Toggle"), + ) + + +@component +def task(state, dispatch, task): + # ...but must receive it and hand it on, unchanged. + async def handle_delete(event): + dispatch({"type": "deleted", "id": task["id"]}) + + return html.li( + {"key": task["id"]}, + task_form(state, dispatch, task), + html.button({"onClick": handle_delete}, "Delete"), + ) + + +@component +def task_list(state, dispatch): + # ...and this one forwards both, without reading either. + return html.ul( + [task(state, dispatch, item, key=item["id"]) for item in state["tasks"]] + ) + + +@component +def task_app(): + state, dispatch = use_reducer(tasks_reducer, {"tasks": INITIAL_TASKS, "next_id": 2}) + + async def handle_add(event): + dispatch({"type": "added"}) + + return html.div( + html.h3("Prague itinerary"), + html.button({"onClick": handle_add}, "Add task"), + task_list(state, dispatch), + html.p({"id": "summary"}, f"{len(state['tasks'])} tasks"), + ) + + +@component +def app(): + return task_app() diff --git a/docs/examples/scaling_up_with_reducer_and_context/task_wiring/app.py b/docs/examples/scaling_up_with_reducer_and_context/task_wiring/app.py new file mode 100644 index 000000000..eeacea9c9 --- /dev/null +++ b/docs/examples/scaling_up_with_reducer_and_context/task_wiring/app.py @@ -0,0 +1,62 @@ +"""The UI: no reducer, no context object, no wiring — just use_tasks().""" + +from tasks import task_store, use_tasks + +from reactpy import component, html + + +# start +@component +def task_form(task): + # No props. The dispatch comes from the wiring file. + _, dispatch = use_tasks() + + async def handle_toggle(event): + dispatch({"type": "toggled", "id": task["id"]}) + + return html.div( + {"style": {"textDecoration": "line-through" if task["done"] else "none"}}, + task["text"], + html.button({"onClick": handle_toggle}, "Toggle"), + ) + + +@component +def task(task): + _, dispatch = use_tasks() + + async def handle_delete(event): + dispatch({"type": "deleted", "id": task["id"]}) + + return html.li( + {"key": task["id"]}, + task_form(task), + html.button({"onClick": handle_delete}, "Delete"), + ) + + +@component +def task_list(): + state, _ = use_tasks() + return html.ul([task(item, key=item["id"]) for item in state["tasks"]]) + + +@component +def task_app(): + state, dispatch = use_tasks() + + async def handle_add(event): + dispatch({"type": "added"}) + + return html.div( + html.h3("Prague itinerary"), + html.button({"onClick": handle_add}, "Add task"), + task_list(), + html.p({"id": "summary"}, f"{len(state['tasks'])} tasks"), + ) + + +@component +def app(): + # One name from the wiring file: the provider component. + return task_store(task_app()) diff --git a/docs/examples/scaling_up_with_reducer_and_context/task_wiring/tasks.py b/docs/examples/scaling_up_with_reducer_and_context/task_wiring/tasks.py new file mode 100644 index 000000000..a89797d97 --- /dev/null +++ b/docs/examples/scaling_up_with_reducer_and_context/task_wiring/tasks.py @@ -0,0 +1,62 @@ +"""All the reducer, context, and wiring lives here; the UI imports two names.""" + +from collections.abc import Callable +from typing import Any + +from reactpy import component, create_context, use_context, use_reducer +from reactpy.types import Context + +TaskState = dict[str, Any] +TaskValue = tuple[TaskState, Callable[[dict[str, Any]], None]] + +# The reducer never mutates what it is given, so one shared constant is +# safe as the initial state for every instance. +INITIAL_TASKS = [ + {"id": 0, "text": "Packing", "done": False}, + {"id": 1, "text": "Passport", "done": True}, +] + +INITIAL_STATE: TaskState = {"tasks": INITIAL_TASKS, "next_id": 2} + +TaskContext: Context[TaskValue] = create_context((INITIAL_STATE, lambda action: None)) + + +def tasks_reducer(state: TaskState, action: dict[str, Any]) -> TaskState: + kind = action["type"] + if kind == "added": + task = {"id": state["next_id"], "text": "New task", "done": False} + return { + **state, + "tasks": [*state["tasks"], task], + "next_id": state["next_id"] + 1, + } + if kind == "toggled": + return { + **state, + "tasks": [ + ( + {**task, "done": not task["done"]} + if task["id"] == action["id"] + else task + ) + for task in state["tasks"] + ], + } + if kind == "deleted": + return { + **state, + "tasks": [task for task in state["tasks"] if task["id"] != action["id"]], + } + raise ValueError(f"unknown action type {kind!r}") + + +@component +def task_store(*children: Any): + """Owns the reducer and provides it. The UI never sees the reducer.""" + state, dispatch = use_reducer(tasks_reducer, INITIAL_STATE) + return TaskContext(*children, value=(state, dispatch)) + + +def use_tasks() -> TaskValue: + """The only accessor the UI needs: current state plus dispatch.""" + return use_context(TaskContext) diff --git a/docs/examples/separating_events_from_effects/callback_frozen_dep/app.py b/docs/examples/separating_events_from_effects/callback_frozen_dep/app.py new file mode 100644 index 000000000..0a68d0880 --- /dev/null +++ b/docs/examples/separating_events_from_effects/callback_frozen_dep/app.py @@ -0,0 +1,38 @@ +"""A locally defined function as a dependency never counts as changed, so the +callback is frozen on its first closure and stops seeing new values. +""" + +from reactpy import component, html, use_callback, use_state + + +# start +@component +def search(): + query, set_query = use_state("") + runs, set_runs = use_state(0) + + def describe(): + return f"query={query!r}" + + async def run(event): + # `describe` is re-created every render but compares code-equal, so this + # callback is still the one from the very first render. The functional + # updater keeps the counter honest; only `describe()` is frozen. + print(f"handler sees {describe()}") + set_runs(lambda current: current + 1) + + on_click = use_callback(run, dependencies=[describe]) + + async def handle_query_change(e): + set_query(await e.target.value) + + return html.div( + html.input({"value": query, "onChange": handle_query_change}), + html.button({"onClick": on_click}, f"Run ({runs})"), + html.p(f"page shows query={query!r} — compare with the console"), + ) + + +@component +def app(): + return search() diff --git a/docs/examples/separating_events_from_effects/callback_identity/app.py b/docs/examples/separating_events_from_effects/callback_identity/app.py new file mode 100644 index 000000000..8a16e006a --- /dev/null +++ b/docs/examples/separating_events_from_effects/callback_identity/app.py @@ -0,0 +1,39 @@ +"""use_callback keeps a handler's identity stable while its dependencies are +unchanged. A theme change reuses it; a count change does not. +""" + +from reactpy import component, html, use_callback, use_ref, use_state + + +# start +@component +def toolbar(): + count, set_count = use_state(0) + theme, set_theme = use_state("light") + previous = use_ref(object()) + + async def stable_handler(event): + set_count(count + 1) + + # inferred dependencies: `count` is the only free variable + on_click = use_callback(stable_handler) + + reused = previous.current is on_click + previous.current = on_click + + async def handle_theme_click(e): + set_theme("dark" if theme == "light" else "light") + + return html.div( + html.button({"onClick": on_click}, f"Count {count}"), + html.button( + {"onClick": handle_theme_click}, + f"Theme {theme}", + ), + html.p("handler reused on this render" if reused else "handler recreated"), + ) + + +@component +def app(): + return toolbar() diff --git a/docs/examples/separating_events_from_effects/handler_freshness/app.py b/docs/examples/separating_events_from_effects/handler_freshness/app.py new file mode 100644 index 000000000..4dea9f014 --- /dev/null +++ b/docs/examples/separating_events_from_effects/handler_freshness/app.py @@ -0,0 +1,41 @@ +"""A handler reads the values of the render that produced it. Click faster than +the server can re-render and every click sees the same snapshot — the safe +updater form is the fix. +""" + +from reactpy import component, html, use_state + + +# start +@component +def basket(): + items, set_items = use_state([]) + discount, set_discount = use_state(0) + + async def add_naive(event): + # reads `items` and `discount` from whichever render created this handler + print(f"naive: {len(items)} items, discount={discount}") + set_items([*items, f"item-{len(items) + 1}@{discount}"]) + + async def add_safe(event): + # the updater receives the *current* state at the moment it is applied + def append(current): + print(f"safe: {len(current)} items, discount={discount}") + return [*current, f"item-{len(current) + 1}@{discount}"] + + set_items(append) + + async def handle_discount_click(e): + set_discount(discount + 10) + + return html.div( + html.button({"onClick": add_naive}, "Add (naive)"), + html.button({"onClick": add_safe}, "Add (safe)"), + html.button({"onClick": handle_discount_click}, f"Coupon {discount}%"), + html.p(", ".join(items) or "empty basket"), + ) + + +@component +def app(): + return basket() diff --git a/docs/examples/separating_events_from_effects/intent_in_effect/app.py b/docs/examples/separating_events_from_effects/intent_in_effect/app.py new file mode 100644 index 000000000..35794238f --- /dev/null +++ b/docs/examples/separating_events_from_effects/intent_in_effect/app.py @@ -0,0 +1,67 @@ +"""Business logic parked in an Effect, driven by state that only exists to +trigger it — which makes a second click silently do nothing. +""" + +from reactpy import component, html, use_effect, use_state + + +# start +def calculate_shipping(city, country): + """Plain function: takes what it needs, returns the result.""" + if not city or not country: + return "" + return f"{city}, {country} — ${15 + 5}" + + +@component +def shipping_form(): + city, set_city = use_state("") + country, set_country = use_state("") + # state that exists purely to make the Effect run + quoted, set_quoted = use_state(False) + shipping_info, set_shipping_info = use_state("") + clicks, set_clicks = use_state(0) + + async def submit(event): + set_clicks(clicks + 1) + set_quoted(True) + + # the "intent" is buried in an Effect watching a flag a click merely sets + def recalculate(): + if quoted: + result = calculate_shipping(city, country) + set_shipping_info(result) + if result: + print(f"quoted {result}") + + use_effect(recalculate, dependencies=[quoted]) + + async def handle_city_change(e): + set_city(await e.target.value) + + async def handle_country_change(e): + set_country(await e.target.value) + + return html.div( + html.label( + "City: ", + html.input({"value": city, "onChange": handle_city_change}), + ), + html.label( + "Country: ", + html.input( + { + "value": country, + "onChange": handle_country_change, + } + ), + ), + html.button({"onClick": submit}, "Get quote"), + html.p(shipping_info or "—"), + f"Clicks: {clicks}", + ) + + +@component +def app(): + return shipping_form() diff --git a/docs/examples/separating_events_from_effects/intent_in_handler/app.py b/docs/examples/separating_events_from_effects/intent_in_handler/app.py new file mode 100644 index 000000000..93294d188 --- /dev/null +++ b/docs/examples/separating_events_from_effects/intent_in_handler/app.py @@ -0,0 +1,58 @@ +"""The same quote logic as an ordinary function called by the event handler: +no trigger state, no Effect, one render per click. +""" + +from reactpy import component, html, use_state + + +# start +def calculate_shipping(city, country): + """Plain function: takes what it needs, returns the result.""" + if not city or not country: + return "" + return f"{city}, {country} — ${15 + 5}" + + +@component +def shipping_form(): + city, set_city = use_state("") + country, set_country = use_state("") + shipping_info, set_shipping_info = use_state("") + clicks, set_clicks = use_state(0) + + async def submit(event): + set_clicks(clicks + 1) + result = calculate_shipping(city, country) + set_shipping_info(result) + if result: + print(f"quoted {result}") + + async def handle_city_change(e): + set_city(await e.target.value) + + async def handle_country_change(e): + set_country(await e.target.value) + + return html.div( + html.label( + "City: ", + html.input({"value": city, "onChange": handle_city_change}), + ), + html.label( + "Country: ", + html.input( + { + "value": country, + "onChange": handle_country_change, + } + ), + ), + html.button({"onClick": submit}, "Get quote"), + html.p(shipping_info or "—"), + f"Clicks: {clicks}", + ) + + +@component +def app(): + return shipping_form() diff --git a/docs/examples/separating_events_from_effects/latest_value_ref/app.py b/docs/examples/separating_events_from_effects/latest_value_ref/app.py new file mode 100644 index 000000000..bc9b49a52 --- /dev/null +++ b/docs/examples/separating_events_from_effects/latest_value_ref/app.py @@ -0,0 +1,47 @@ +"""When a callback really must stay stable *and* read the newest value, keep +that value in a ref and read `.current` at call time. +""" + +from reactpy import component, html, use_callback, use_ref, use_state + + +# start +@component +def checkout(): + coupon, set_coupon = use_state("") + total, set_total = use_state(0) + coupon_ref = use_ref("") + # updated during render, so any callback reads the value as of the click + coupon_ref.current = coupon + + # pinned to nothing: the identity never changes, yet it reads `coupon_ref` + async def apply_coupon_handler(event): + print(f"applying coupon={coupon_ref.current!r}") + + apply_coupon = use_callback(apply_coupon_handler, dependencies=[]) + + async def handle_coupon_change(e): + # The await comes first: `.upper()` runs on the string which comes back, + # not on the proxy. Without the parens you would call `.upper()` on the + # proxy itself and silently get the un-uppercased value. + set_coupon((await e.target.value).upper()) + + async def handle_total_click(e): + set_total(total + 10) + + return html.div( + html.input( + { + "value": coupon, + "onChange": handle_coupon_change, + } + ), + html.button({"onClick": apply_coupon}, "Apply coupon"), + html.button({"onClick": handle_total_click}, f"Total {total}"), + html.p(f"page shows coupon={coupon!r} — compare with the console"), + ) + + +@component +def app(): + return checkout() diff --git a/docs/examples/sharing_state_between_components/hardcoded/app.py b/docs/examples/sharing_state_between_components/hardcoded/app.py new file mode 100644 index 000000000..7885fee22 --- /dev/null +++ b/docs/examples/sharing_state_between_components/hardcoded/app.py @@ -0,0 +1,20 @@ +from reactpy import component, html + + +# start +@component +def panel(title, is_active, *children): + return html.section( + {"className": "panel"}, + html.h3(title), + html.p(*children) if is_active else html.button("Show"), + ) + + +@component +def accordion(): + return html( + html.h2("Almaty, Kazakhstan"), + panel("About", True, "With a population of about 2 million..."), + panel("Etymology", True, "The name comes from алма..."), + ) diff --git a/docs/examples/sharing_state_between_components/hardcoded/styles.css b/docs/examples/sharing_state_between_components/hardcoded/styles.css new file mode 100644 index 000000000..7f3515da7 --- /dev/null +++ b/docs/examples/sharing_state_between_components/hardcoded/styles.css @@ -0,0 +1,9 @@ +h3, +p { + margin: 5px 0; +} + +.panel { + padding: 10px; + border: 1px solid #aaa; +} diff --git a/docs/examples/sharing_state_between_components/independent/app.py b/docs/examples/sharing_state_between_components/independent/app.py new file mode 100644 index 000000000..0a8b9ba81 --- /dev/null +++ b/docs/examples/sharing_state_between_components/independent/app.py @@ -0,0 +1,37 @@ +from reactpy import component, html, use_state + + +# start +@component +def panel(title, *children): + is_active, set_is_active = use_state(False) + + async def handle_is_active_click(event): + set_is_active(True) + + return html.section( + {"className": "panel"}, + html.h3(title), + ( + html.p(*children) + if is_active + else html.button({"onClick": handle_is_active_click}, "Show") + ), + ) + + +@component +def accordion(): + return html( + html.h2("Almaty, Kazakhstan"), + panel( + "About", + "With a population of about 2 million, Almaty is Kazakhstan's " + "largest city. From 1929 to 1997, it was its capital city.", + ), + panel( + "Etymology", + "The name comes from алма, the Kazakh word for “apple” and is " + "often translated as “full of apples”.", + ), + ) diff --git a/docs/examples/sharing_state_between_components/independent/styles.css b/docs/examples/sharing_state_between_components/independent/styles.css new file mode 100644 index 000000000..7f3515da7 --- /dev/null +++ b/docs/examples/sharing_state_between_components/independent/styles.css @@ -0,0 +1,9 @@ +h3, +p { + margin: 5px 0; +} + +.panel { + padding: 10px; + border: 1px solid #aaa; +} diff --git a/docs/examples/sharing_state_between_components/lifted/app.py b/docs/examples/sharing_state_between_components/lifted/app.py new file mode 100644 index 000000000..fe7bbcebf --- /dev/null +++ b/docs/examples/sharing_state_between_components/lifted/app.py @@ -0,0 +1,40 @@ +from reactpy import component, html, use_state + + +# start +@component +def panel(title, is_active, on_show, *children): + return html.section( + {"className": "panel"}, + html.h3(title), + (html.p(*children) if is_active else html.button({"onClick": on_show}, "Show")), + ) + + +@component +def accordion(): + active_index, set_active_index = use_state(0) + + async def show_about(event): + set_active_index(0) + + async def show_etymology(event): + set_active_index(1) + + return html( + html.h2("Almaty, Kazakhstan"), + panel( + "About", + active_index == 0, + show_about, + "With a population of about 2 million, Almaty is Kazakhstan's " + "largest city. From 1929 to 1997, it was its capital city.", + ), + panel( + "Etymology", + active_index == 1, + show_etymology, + "The name comes from алма, the Kazakh word for “apple” and is " + "often translated as “full of apples”.", + ), + ) diff --git a/docs/examples/sharing_state_between_components/lifted/styles.css b/docs/examples/sharing_state_between_components/lifted/styles.css new file mode 100644 index 000000000..7f3515da7 --- /dev/null +++ b/docs/examples/sharing_state_between_components/lifted/styles.css @@ -0,0 +1,9 @@ +h3, +p { + margin: 5px 0; +} + +.panel { + padding: 10px; + border: 1px solid #aaa; +} diff --git a/docs/examples/state_a_components_memory/isolated_galleries/app.py b/docs/examples/state_a_components_memory/isolated_galleries/app.py new file mode 100644 index 000000000..2a7939608 --- /dev/null +++ b/docs/examples/state_a_components_memory/isolated_galleries/app.py @@ -0,0 +1,29 @@ +from data import sculptures + +from reactpy import component, html, use_state + + +# start +@component +def gallery(): + index, set_index = use_state(0) + sculpture = sculptures[index] + + async def handle_index_click(event): + set_index(index + 1) + + return html.section( + html.button( + {"onClick": handle_index_click}, + "Next", + ), + html.h3(f"({index + 1} of {len(sculptures)})"), + html.img({"src": sculpture["url"], "alt": sculpture["alt"]}), + ) + + +@component +def page(): + # Two separate mounts of the same component. Each call site is a different + # position in the tree, so each gallery keeps its own index. + return html.div(gallery(), gallery()) diff --git a/docs/examples/state_a_components_memory/isolated_galleries/data.py b/docs/examples/state_a_components_memory/isolated_galleries/data.py new file mode 100644 index 000000000..2e7354265 --- /dev/null +++ b/docs/examples/state_a_components_memory/isolated_galleries/data.py @@ -0,0 +1,86 @@ +sculptures = [ + { + "name": "Homenaje a la Neurocirugía", + "artist": "Marta Colvin Andrade", + "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.", + "url": "../../assets/images/demos/Mx7dA2Y.jpg", + "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips.", + }, + { + "name": "Floralis Genérica", + "artist": "Eduardo Catalano", + "description": "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.", + "url": "../../assets/images/demos/ZF6s192m.jpg", + "alt": "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.", + }, + { + "name": "Eternal Presence", + "artist": "John Woodrow Wilson", + "description": 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."', + "url": "../../assets/images/demos/aTtVpES.jpg", + "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.", + }, + { + "name": "Moai", + "artist": "Unknown Artist", + "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.", + "url": "../../assets/images/demos/RCwLEoQm.jpg", + "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces.", + }, + { + "name": "Blue Nana", + "artist": "Niki de Saint Phalle", + "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.", + "url": "../../assets/images/demos/Sd1AgUOm.jpg", + "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.", + }, + { + "name": "Ultimate Form", + "artist": "Barbara Hepworth", + "description": "This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.", + "url": "../../assets/images/demos/2heNQDcm.jpg", + "alt": "A tall sculpture made of three elements stacked on each other reminding of a human figure.", + }, + { + "name": "Cavaliere", + "artist": "Lamidi Olonade Fakeye", + "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.", + "url": "../../assets/images/demos/wIdGuZwm.png", + "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.", + }, + { + "name": "Big Bellies", + "artist": "Alina Szapocznikow", + "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.", + "url": "../../assets/images/demos/AlHTAdDm.jpg", + "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.", + }, + { + "name": "Terracotta Army", + "artist": "Unknown Artist", + "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.", + "url": "../../assets/images/demos/HMFmH6m.jpg", + "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.", + }, + { + "name": "Lunar Landscape", + "artist": "Louise Nevelson", + "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism's geometric abstraction of space and form.", + "url": "../../assets/images/demos/rN7hY6om.jpg", + "alt": "A black matte sculpture where the individual elements are initially indistinguishable.", + }, + { + "name": "Aureole", + "artist": "Ranjani Shettar", + "description": 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."', + "url": "../../assets/images/demos/okTpbHhm.jpg", + "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.", + }, + { + "name": "Hippos", + "artist": "Taipei Zoo", + "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.", + "url": "../../assets/images/demos/6o5Vuyu.jpg", + "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.", + }, +] diff --git a/docs/examples/state_a_components_memory/isolated_galleries/styles.css b/docs/examples/state_a_components_memory/isolated_galleries/styles.css new file mode 100644 index 000000000..a6a2bcba3 --- /dev/null +++ b/docs/examples/state_a_components_memory/isolated_galleries/styles.css @@ -0,0 +1,21 @@ +h2 { + margin-top: 10px; + margin-bottom: 0; +} + +h3 { + margin-top: 5px; + font-weight: normal; + font-size: 100%; +} + +img { + width: 120px; + height: 120px; +} + +button { + display: block; + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/docs/examples/state_a_components_memory/multiple_state/app.py b/docs/examples/state_a_components_memory/multiple_state/app.py new file mode 100644 index 000000000..1ee27018d --- /dev/null +++ b/docs/examples/state_a_components_memory/multiple_state/app.py @@ -0,0 +1,28 @@ +from data import sculptures + +from reactpy import component, html, use_state + + +# start +@component +def gallery(): + index, set_index = use_state(0) + show_more, set_show_more = use_state(False) + + async def handle_next(event): + set_index(index + 1) + + async def handle_more(event): + set_show_more(not show_more) + + sculpture = sculptures[index] + return html.div( + html.button({"onClick": handle_next}, "Next"), + html.h2(html.i(sculpture["name"]), " by ", sculpture["artist"]), + html.h3(f"({index + 1} of {len(sculptures)})"), + html.button( + {"onClick": handle_more}, "Hide details" if show_more else "Show details" + ), + html.p(sculpture["description"]) if show_more else None, + html.img({"src": sculpture["url"], "alt": sculpture["alt"]}), + ) diff --git a/docs/examples/state_a_components_memory/multiple_state/data.py b/docs/examples/state_a_components_memory/multiple_state/data.py new file mode 100644 index 000000000..2e7354265 --- /dev/null +++ b/docs/examples/state_a_components_memory/multiple_state/data.py @@ -0,0 +1,86 @@ +sculptures = [ + { + "name": "Homenaje a la Neurocirugía", + "artist": "Marta Colvin Andrade", + "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.", + "url": "../../assets/images/demos/Mx7dA2Y.jpg", + "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips.", + }, + { + "name": "Floralis Genérica", + "artist": "Eduardo Catalano", + "description": "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.", + "url": "../../assets/images/demos/ZF6s192m.jpg", + "alt": "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.", + }, + { + "name": "Eternal Presence", + "artist": "John Woodrow Wilson", + "description": 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."', + "url": "../../assets/images/demos/aTtVpES.jpg", + "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.", + }, + { + "name": "Moai", + "artist": "Unknown Artist", + "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.", + "url": "../../assets/images/demos/RCwLEoQm.jpg", + "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces.", + }, + { + "name": "Blue Nana", + "artist": "Niki de Saint Phalle", + "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.", + "url": "../../assets/images/demos/Sd1AgUOm.jpg", + "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.", + }, + { + "name": "Ultimate Form", + "artist": "Barbara Hepworth", + "description": "This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.", + "url": "../../assets/images/demos/2heNQDcm.jpg", + "alt": "A tall sculpture made of three elements stacked on each other reminding of a human figure.", + }, + { + "name": "Cavaliere", + "artist": "Lamidi Olonade Fakeye", + "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.", + "url": "../../assets/images/demos/wIdGuZwm.png", + "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.", + }, + { + "name": "Big Bellies", + "artist": "Alina Szapocznikow", + "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.", + "url": "../../assets/images/demos/AlHTAdDm.jpg", + "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.", + }, + { + "name": "Terracotta Army", + "artist": "Unknown Artist", + "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.", + "url": "../../assets/images/demos/HMFmH6m.jpg", + "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.", + }, + { + "name": "Lunar Landscape", + "artist": "Louise Nevelson", + "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism's geometric abstraction of space and form.", + "url": "../../assets/images/demos/rN7hY6om.jpg", + "alt": "A black matte sculpture where the individual elements are initially indistinguishable.", + }, + { + "name": "Aureole", + "artist": "Ranjani Shettar", + "description": 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."', + "url": "../../assets/images/demos/okTpbHhm.jpg", + "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.", + }, + { + "name": "Hippos", + "artist": "Taipei Zoo", + "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.", + "url": "../../assets/images/demos/6o5Vuyu.jpg", + "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.", + }, +] diff --git a/docs/examples/state_a_components_memory/multiple_state/styles.css b/docs/examples/state_a_components_memory/multiple_state/styles.css new file mode 100644 index 000000000..a6a2bcba3 --- /dev/null +++ b/docs/examples/state_a_components_memory/multiple_state/styles.css @@ -0,0 +1,21 @@ +h2 { + margin-top: 10px; + margin-bottom: 0; +} + +h3 { + margin-top: 5px; + font-weight: normal; + font-size: 100%; +} + +img { + width: 120px; + height: 120px; +} + +button { + display: block; + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/docs/examples/state_a_components_memory/regular_variable/app.py b/docs/examples/state_a_components_memory/regular_variable/app.py new file mode 100644 index 000000000..4608e8942 --- /dev/null +++ b/docs/examples/state_a_components_memory/regular_variable/app.py @@ -0,0 +1,25 @@ +from data import sculptures + +from reactpy import component, html + + +# start +@component +def gallery(): + index = 0 + + async def handle_next(event): + # `nonlocal` is what makes this assignment legal Python at all; without + # it the nested function gets its own `index` and the read on the right + # hand side raises UnboundLocalError. JS closures need no such keyword. + nonlocal index + index = index + 1 + + sculpture = sculptures[index] + return html.div( + html.button({"onClick": handle_next}, "Next"), + html.h2(html.i(sculpture["name"]), " by ", sculpture["artist"]), + html.h3(f"({index + 1} of {len(sculptures)})"), + html.img({"src": sculpture["url"], "alt": sculpture["alt"]}), + html.p(sculpture["description"]), + ) diff --git a/docs/examples/state_a_components_memory/regular_variable/data.py b/docs/examples/state_a_components_memory/regular_variable/data.py new file mode 100644 index 000000000..2e7354265 --- /dev/null +++ b/docs/examples/state_a_components_memory/regular_variable/data.py @@ -0,0 +1,86 @@ +sculptures = [ + { + "name": "Homenaje a la Neurocirugía", + "artist": "Marta Colvin Andrade", + "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.", + "url": "../../assets/images/demos/Mx7dA2Y.jpg", + "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips.", + }, + { + "name": "Floralis Genérica", + "artist": "Eduardo Catalano", + "description": "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.", + "url": "../../assets/images/demos/ZF6s192m.jpg", + "alt": "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.", + }, + { + "name": "Eternal Presence", + "artist": "John Woodrow Wilson", + "description": 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."', + "url": "../../assets/images/demos/aTtVpES.jpg", + "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.", + }, + { + "name": "Moai", + "artist": "Unknown Artist", + "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.", + "url": "../../assets/images/demos/RCwLEoQm.jpg", + "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces.", + }, + { + "name": "Blue Nana", + "artist": "Niki de Saint Phalle", + "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.", + "url": "../../assets/images/demos/Sd1AgUOm.jpg", + "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.", + }, + { + "name": "Ultimate Form", + "artist": "Barbara Hepworth", + "description": "This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.", + "url": "../../assets/images/demos/2heNQDcm.jpg", + "alt": "A tall sculpture made of three elements stacked on each other reminding of a human figure.", + }, + { + "name": "Cavaliere", + "artist": "Lamidi Olonade Fakeye", + "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.", + "url": "../../assets/images/demos/wIdGuZwm.png", + "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.", + }, + { + "name": "Big Bellies", + "artist": "Alina Szapocznikow", + "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.", + "url": "../../assets/images/demos/AlHTAdDm.jpg", + "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.", + }, + { + "name": "Terracotta Army", + "artist": "Unknown Artist", + "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.", + "url": "../../assets/images/demos/HMFmH6m.jpg", + "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.", + }, + { + "name": "Lunar Landscape", + "artist": "Louise Nevelson", + "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism's geometric abstraction of space and form.", + "url": "../../assets/images/demos/rN7hY6om.jpg", + "alt": "A black matte sculpture where the individual elements are initially indistinguishable.", + }, + { + "name": "Aureole", + "artist": "Ranjani Shettar", + "description": 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."', + "url": "../../assets/images/demos/okTpbHhm.jpg", + "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.", + }, + { + "name": "Hippos", + "artist": "Taipei Zoo", + "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.", + "url": "../../assets/images/demos/6o5Vuyu.jpg", + "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.", + }, +] diff --git a/docs/examples/state_a_components_memory/regular_variable/styles.css b/docs/examples/state_a_components_memory/regular_variable/styles.css new file mode 100644 index 000000000..a6a2bcba3 --- /dev/null +++ b/docs/examples/state_a_components_memory/regular_variable/styles.css @@ -0,0 +1,21 @@ +h2 { + margin-top: 10px; + margin-bottom: 0; +} + +h3 { + margin-top: 5px; + font-weight: normal; + font-size: 100%; +} + +img { + width: 120px; + height: 120px; +} + +button { + display: block; + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/docs/examples/state_a_components_memory/state_variable/app.py b/docs/examples/state_a_components_memory/state_variable/app.py new file mode 100644 index 000000000..336a7fedf --- /dev/null +++ b/docs/examples/state_a_components_memory/state_variable/app.py @@ -0,0 +1,21 @@ +from data import sculptures + +from reactpy import component, html, use_state + + +# start +@component +def gallery(): + index, set_index = use_state(0) + + async def handle_next(event): + set_index(index + 1) + + sculpture = sculptures[index] + return html.div( + html.button({"onClick": handle_next}, "Next"), + html.h2(html.i(sculpture["name"]), " by ", sculpture["artist"]), + html.h3(f"({index + 1} of {len(sculptures)})"), + html.img({"src": sculpture["url"], "alt": sculpture["alt"]}), + html.p(sculpture["description"]), + ) diff --git a/docs/examples/state_a_components_memory/state_variable/data.py b/docs/examples/state_a_components_memory/state_variable/data.py new file mode 100644 index 000000000..2e7354265 --- /dev/null +++ b/docs/examples/state_a_components_memory/state_variable/data.py @@ -0,0 +1,86 @@ +sculptures = [ + { + "name": "Homenaje a la Neurocirugía", + "artist": "Marta Colvin Andrade", + "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.", + "url": "../../assets/images/demos/Mx7dA2Y.jpg", + "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips.", + }, + { + "name": "Floralis Genérica", + "artist": "Eduardo Catalano", + "description": "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.", + "url": "../../assets/images/demos/ZF6s192m.jpg", + "alt": "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.", + }, + { + "name": "Eternal Presence", + "artist": "John Woodrow Wilson", + "description": 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."', + "url": "../../assets/images/demos/aTtVpES.jpg", + "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.", + }, + { + "name": "Moai", + "artist": "Unknown Artist", + "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.", + "url": "../../assets/images/demos/RCwLEoQm.jpg", + "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces.", + }, + { + "name": "Blue Nana", + "artist": "Niki de Saint Phalle", + "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.", + "url": "../../assets/images/demos/Sd1AgUOm.jpg", + "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.", + }, + { + "name": "Ultimate Form", + "artist": "Barbara Hepworth", + "description": "This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.", + "url": "../../assets/images/demos/2heNQDcm.jpg", + "alt": "A tall sculpture made of three elements stacked on each other reminding of a human figure.", + }, + { + "name": "Cavaliere", + "artist": "Lamidi Olonade Fakeye", + "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.", + "url": "../../assets/images/demos/wIdGuZwm.png", + "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.", + }, + { + "name": "Big Bellies", + "artist": "Alina Szapocznikow", + "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.", + "url": "../../assets/images/demos/AlHTAdDm.jpg", + "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.", + }, + { + "name": "Terracotta Army", + "artist": "Unknown Artist", + "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.", + "url": "../../assets/images/demos/HMFmH6m.jpg", + "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.", + }, + { + "name": "Lunar Landscape", + "artist": "Louise Nevelson", + "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism's geometric abstraction of space and form.", + "url": "../../assets/images/demos/rN7hY6om.jpg", + "alt": "A black matte sculpture where the individual elements are initially indistinguishable.", + }, + { + "name": "Aureole", + "artist": "Ranjani Shettar", + "description": 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."', + "url": "../../assets/images/demos/okTpbHhm.jpg", + "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.", + }, + { + "name": "Hippos", + "artist": "Taipei Zoo", + "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.", + "url": "../../assets/images/demos/6o5Vuyu.jpg", + "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.", + }, +] diff --git a/docs/examples/state_a_components_memory/state_variable/styles.css b/docs/examples/state_a_components_memory/state_variable/styles.css new file mode 100644 index 000000000..a6a2bcba3 --- /dev/null +++ b/docs/examples/state_a_components_memory/state_variable/styles.css @@ -0,0 +1,21 @@ +h2 { + margin-top: 10px; + margin-bottom: 0; +} + +h3 { + margin-top: 5px; + font-weight: normal; + font-size: 100%; +} + +img { + width: 120px; + height: 120px; +} + +button { + display: block; + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/docs/examples/state_as_a_snapshot/delayed_handler/app.py b/docs/examples/state_as_a_snapshot/delayed_handler/app.py new file mode 100644 index 000000000..276d21ad7 --- /dev/null +++ b/docs/examples/state_as_a_snapshot/delayed_handler/app.py @@ -0,0 +1,25 @@ +import asyncio + +from reactpy import component, html, use_state + + +# start +@component +def counter(): + number, set_number = use_state(0) + seen, set_seen = use_state([]) + + async def handle_click(event): + set_number(number + 5) + + def delayed(): + # Runs long after this render, but still sees this render's value. + set_seen([*seen, number]) + + asyncio.get_running_loop().call_later(3, delayed) + + return html.div( + html.h1(number), + html.button({"onClick": handle_click}, "+5"), + html.p("Handler saw (3s later): ", ", ".join(map(str, seen)) or "-"), + ) diff --git a/docs/examples/state_as_a_snapshot/delayed_handler/styles.css b/docs/examples/state_as_a_snapshot/delayed_handler/styles.css new file mode 100644 index 000000000..772e90119 --- /dev/null +++ b/docs/examples/state_as_a_snapshot/delayed_handler/styles.css @@ -0,0 +1,18 @@ +button { + display: inline-block; + margin: 10px; + font-size: 20px; +} + +h1 { + display: inline-block; + margin: 10px; + width: 30px; + text-align: center; +} + +label, +textarea { + display: block; + margin-bottom: 10px; +} diff --git a/docs/examples/state_as_a_snapshot/delayed_message/app.py b/docs/examples/state_as_a_snapshot/delayed_message/app.py new file mode 100644 index 000000000..d28c92295 --- /dev/null +++ b/docs/examples/state_as_a_snapshot/delayed_message/app.py @@ -0,0 +1,46 @@ +import asyncio + +from reactpy import component, html, use_state + + +# start +@component +def form(): + to, set_to = use_state("Alice") + message, set_message = use_state("Hello") + status, set_status = use_state("Not sent yet") + + async def handle_submit(event): + # This handler closes over `to` and `message` from ITS OWN render. + set_status("Sending...") + await asyncio.sleep(5) + set_status(f"You said {message} to {to}") + + async def handle_to_change(event): + set_to(await event.target.value) + + async def handle_message_change(event): + set_message(await event.target.value) + + return html.div( + html.label( + "To: ", + html.input( + { + "value": to, + "onChange": handle_to_change, + } + ), + ), + html.label( + "Message: ", + html.input( + { + "value": message, + "onChange": handle_message_change, + } + ), + ), + html.button({"onClick": handle_submit}, "Send"), + html.p(status), + ) diff --git a/docs/examples/state_as_a_snapshot/delayed_message/styles.css b/docs/examples/state_as_a_snapshot/delayed_message/styles.css new file mode 100644 index 000000000..772e90119 --- /dev/null +++ b/docs/examples/state_as_a_snapshot/delayed_message/styles.css @@ -0,0 +1,18 @@ +button { + display: inline-block; + margin: 10px; + font-size: 20px; +} + +h1 { + display: inline-block; + margin: 10px; + width: 30px; + text-align: center; +} + +label, +textarea { + display: block; + margin-bottom: 10px; +} diff --git a/docs/examples/state_as_a_snapshot/handler_sees_snapshot/app.py b/docs/examples/state_as_a_snapshot/handler_sees_snapshot/app.py new file mode 100644 index 000000000..abfe47fed --- /dev/null +++ b/docs/examples/state_as_a_snapshot/handler_sees_snapshot/app.py @@ -0,0 +1,19 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + number, set_number = use_state(0) + seen, set_seen = use_state([]) + + async def handle_click(event): + set_number(number + 5) + # `number` here is still the value from THIS render. + set_seen([*seen, number]) + + return html.div( + html.h1(number), + html.button({"onClick": handle_click}, "+5"), + html.p("Handler saw: ", ", ".join(map(str, seen)) or "-"), + ) diff --git a/docs/examples/state_as_a_snapshot/handler_sees_snapshot/styles.css b/docs/examples/state_as_a_snapshot/handler_sees_snapshot/styles.css new file mode 100644 index 000000000..772e90119 --- /dev/null +++ b/docs/examples/state_as_a_snapshot/handler_sees_snapshot/styles.css @@ -0,0 +1,18 @@ +button { + display: inline-block; + margin: 10px; + font-size: 20px; +} + +h1 { + display: inline-block; + margin: 10px; + width: 30px; + text-align: center; +} + +label, +textarea { + display: block; + margin-bottom: 10px; +} diff --git a/docs/examples/state_as_a_snapshot/snapshot_in_time/app.py b/docs/examples/state_as_a_snapshot/snapshot_in_time/app.py new file mode 100644 index 000000000..a1c8fb153 --- /dev/null +++ b/docs/examples/state_as_a_snapshot/snapshot_in_time/app.py @@ -0,0 +1,17 @@ +from reactpy import component, html, use_state + + +# start +@component +def counter(): + number, set_number = use_state(0) + + async def handle_click(event): + set_number(number + 1) + set_number(number + 1) + set_number(number + 1) + + return html.div( + html.h1(number), + html.button({"onClick": handle_click}, "+3"), + ) diff --git a/docs/examples/state_as_a_snapshot/snapshot_in_time/styles.css b/docs/examples/state_as_a_snapshot/snapshot_in_time/styles.css new file mode 100644 index 000000000..772e90119 --- /dev/null +++ b/docs/examples/state_as_a_snapshot/snapshot_in_time/styles.css @@ -0,0 +1,18 @@ +button { + display: inline-block; + margin: 10px; + font-size: 20px; +} + +h1 { + display: inline-block; + margin: 10px; + width: 30px; + text-align: center; +} + +label, +textarea { + display: block; + margin-bottom: 10px; +} diff --git a/docs/examples/state_as_a_snapshot/triggers_renders/app.py b/docs/examples/state_as_a_snapshot/triggers_renders/app.py new file mode 100644 index 000000000..6b6c80191 --- /dev/null +++ b/docs/examples/state_as_a_snapshot/triggers_renders/app.py @@ -0,0 +1,35 @@ +from reactpy import component, html, use_state + + +# start +@component +def form(): + is_sent, set_is_sent = use_state(False) + message, set_message = use_state("Hi!") + + if is_sent: + return html.h1("Your message is on its way!") + + async def handle_submit(event): + event.preventDefault() + set_is_sent(True) + send_message(message) + + async def handle_message_change(event): + set_message(await event.target.value) + + return html.form( + {"onSubmit": handle_submit}, + html.textarea( + { + "placeholder": "Message", + "value": message, + "onChange": handle_message_change, + } + ), + html.button({"type": "submit"}, "Send"), + ) + + +def send_message(message): + print(f"Sending: {message}") diff --git a/docs/examples/state_as_a_snapshot/triggers_renders/styles.css b/docs/examples/state_as_a_snapshot/triggers_renders/styles.css new file mode 100644 index 000000000..66f0974af --- /dev/null +++ b/docs/examples/state_as_a_snapshot/triggers_renders/styles.css @@ -0,0 +1,17 @@ +button { + display: inline-block; + margin: 10px; + font-size: 20px; +} + +h1 { + display: inline-block; + margin: 10px; + text-align: center; +} + +label, +textarea { + display: block; + margin-bottom: 10px; +} diff --git a/docs/examples/synchronizing_with_effects/analytics/app.py b/docs/examples/synchronizing_with_effects/analytics/app.py new file mode 100644 index 000000000..4ea31d496 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/analytics/app.py @@ -0,0 +1,48 @@ +"""Report a page view once per page, not once per render.""" + +from data import analytics + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def page_view(path): + draft, set_draft = use_state("") + + def effect(): + # Depends on `path` only: editing the field re-renders the component + # but does not report another visit. + analytics.log_visit(path) + + use_effect(effect, dependencies=[path]) + + async def handle_draft_change(event): + set_draft(await event.target.value) + + return html.div( + html.h2(path), + html.p(f"total reports sent: {len(analytics.sent)}"), + html.input( + { + "placeholder": "notes", + "onChange": handle_draft_change, + } + ), + ) + + +@component +def app(): + path, set_path = use_state("/home") + + async def handle_path_click(event): + set_path("/about" if path == "/home" else "/home") + + return html.div( + html.button( + {"onClick": handle_path_click}, + "Navigate", + ), + page_view(path), + ) diff --git a/docs/examples/synchronizing_with_effects/analytics/data.py b/docs/examples/synchronizing_with_effects/analytics/data.py new file mode 100644 index 000000000..5808907d9 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/analytics/data.py @@ -0,0 +1,12 @@ +"""Collects the analytics events a component reports.""" + + +class Analytics: + def __init__(self): + self.sent = [] + + def log_visit(self, page): + self.sent.append(page) + + +analytics = Analytics() diff --git a/docs/examples/synchronizing_with_effects/cleanup/app.py b/docs/examples/synchronizing_with_effects/cleanup/app.py new file mode 100644 index 000000000..f7c4cb132 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/cleanup/app.py @@ -0,0 +1,52 @@ +"""Connect on mount, disconnect on unmount: the setup/cleanup pair.""" + +from data import chat_server + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def chat_room(room): + status, set_status = use_state("offline") + + def effect(): + # ReactPy runs the returned function before the *next* run of this + # same Effect, and again when the component unmounts. A connection + # without a clean-up is a permanent leak in a long-lived server. + close = chat_server.connect(room) + set_status(f"connected to {room}") + + def cleanup(): + close() + set_status("offline") + + return cleanup + + use_effect(effect, dependencies=[room]) + + return html.p(status) + + +@component +def app(): + room, set_room = use_state("lobby") + connected, set_connected = use_state(True) + + async def handle_room_click(event): + set_room("dev" if room == "lobby" else "lobby") + + async def handle_connected_click(event): + set_connected(not connected) + + return html.div( + html.button( + {"onClick": handle_room_click}, + f"Switch room (now {room})", + ), + html.button( + {"onClick": handle_connected_click}, + "Leave" if connected else "Join", + ), + *([] if not connected else [chat_room(room)]), + ) diff --git a/docs/examples/synchronizing_with_effects/cleanup/data.py b/docs/examples/synchronizing_with_effects/cleanup/data.py new file mode 100644 index 000000000..da927765e --- /dev/null +++ b/docs/examples/synchronizing_with_effects/cleanup/data.py @@ -0,0 +1,21 @@ +"""A stand-in for the server connection an Effect opens and closes.""" + + +class ChatServer: + def __init__(self): + self.log = [] + self.open_count = 0 + + def connect(self, room): + """Return the function that closes this particular connection.""" + self.open_count += 1 + self.log.append(f"connect {room}") + + def close(): + self.open_count -= 1 + self.log.append(f"close {room}") + + return close + + +chat_server = ChatServer() diff --git a/docs/examples/synchronizing_with_effects/declare_effect/app.py b/docs/examples/synchronizing_with_effects/declare_effect/app.py new file mode 100644 index 000000000..d5bcf3401 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/declare_effect/app.py @@ -0,0 +1,28 @@ +"""The smallest possible Effect: a mount log, and nothing else.""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def status_page(): + visit_count, set_visit_count = use_state(0) + + def effect(): + # Runs after this component is on screen. ReactPy runs it in the + # connection's own task, after the render that scheduled it. + print("status_page is on screen") + + use_effect(effect, dependencies=[]) + + async def handle_visit_count_click(event): + set_visit_count(visit_count + 1) + + return html.div( + html.h1("Service status"), + html.p(f"This page has been opened {visit_count} times."), + html.button( + {"onClick": handle_visit_count_click}, + "Re-check", + ), + ) diff --git a/docs/examples/synchronizing_with_effects/fetching/app.py b/docs/examples/synchronizing_with_effects/fetching/app.py new file mode 100644 index 000000000..98fb8b203 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/fetching/app.py @@ -0,0 +1,39 @@ +"""Fetch in an Effect; cancellation is what discards a stale answer.""" + +from data import catalog + +from reactpy import component, html, use_async_effect, use_state + + +# start +@component +def report_view(name): + rows, set_rows = use_state("nothing loaded yet") + + async def fetch(): + set_rows(f"loading {name} ...") + result = await catalog.fetch(name) + # This line is reached only if nobody cancelled the task. When `name` + # changes, ReactPy cancels the running task before this line runs, so + # an answer for the OLD report can never overwrite the new one. + set_rows(result) + + use_async_effect(fetch, dependencies=[name]) + + return html.div(html.h2(name), html.p(rows)) + + +@component +def app(): + name, set_name = use_state("orders") + + async def handle_name_click(event): + set_name("users" if name == "orders" else "orders") + + return html.div( + html.button( + {"onClick": handle_name_click}, + "Toggle report", + ), + report_view(name), + ) diff --git a/docs/examples/synchronizing_with_effects/fetching/data.py b/docs/examples/synchronizing_with_effects/fetching/data.py new file mode 100644 index 000000000..043ee1348 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/fetching/data.py @@ -0,0 +1,22 @@ +"""A slow data source, slow on purpose so two requests can overlap.""" + +import asyncio + +# Deliberately asymmetric: `orders` is slow and `users` is fast, so a +# switch made while `orders` is in flight proves that the stale answer +# never lands. +LATENCIES = {"orders": 0.8, "users": 0.05} + + +class Catalog: + def __init__(self): + self.answers = [] + + async def fetch(self, name): + await asyncio.sleep(LATENCIES.get(name, 0.2)) + answer = f"{name}: {len(name) * 100} rows" + self.answers.append(answer) + return answer + + +catalog = Catalog() diff --git a/docs/examples/synchronizing_with_effects/inferred/app.py b/docs/examples/synchronizing_with_effects/inferred/app.py new file mode 100644 index 000000000..f69b01733 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/inferred/app.py @@ -0,0 +1,32 @@ +"""An Effect with no dependencies argument infers them from the closure.""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def counter(): + count, set_count = use_state(0) + label, set_label = use_state("clicks") + runs, set_runs = use_state([]) + + def effect(): + # This closure reads `count` and nothing else, so `count` is the + # Effect's only dependency. It never reads `label`, so renaming + # re-renders the component WITHOUT re-running this Effect. + set_runs(lambda previous: [*previous, f"effect ran with count={count}"]) + + use_effect(effect) + + async def handle_count_click(event): + set_count(count + 1) + + async def handle_label_click(event): + set_label(label + "!") + + return html.div( + html.h1(f"{label}: {count}"), + html.button({"onClick": handle_count_click}, "Add 1"), + html.button({"onClick": handle_label_click}, "Rename"), + html.ul([html.li({"key": index}, line) for index, line in enumerate(runs)]), + ) diff --git a/docs/examples/synchronizing_with_effects/latest_state/app.py b/docs/examples/synchronizing_with_effects/latest_state/app.py new file mode 100644 index 000000000..f89feac18 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/latest_state/app.py @@ -0,0 +1,48 @@ +"""Read the newest value from a long-lived Effect using a ref.""" + +import asyncio + +from reactpy import component, html, use_async_effect, use_ref, use_state + +SLOW_LATENCY = 30 +HIGH_THRESHOLD = 50 +LOW_THRESHOLD = 10 + + +def append_alert(previous): + """A module-level updater, so the Effect's loop builds no closures.""" + return [*previous, f"slow: {SLOW_LATENCY}ms"] + + +# start +@component +def connection_monitor(): + threshold, set_threshold = use_state(HIGH_THRESHOLD) + alerts, set_alerts = use_state([]) + # Write the newest value during render so a reader that wakes up later + # gets this render's number rather than the one captured at mount. + latest_threshold = use_ref(threshold) + latest_threshold.current = threshold + + async def watch(): + while True: + await asyncio.sleep(0.2) + # SLOW_LATENCY stands in for a real measurement. + if SLOW_LATENCY > latest_threshold.current: + set_alerts(append_alert) + + # `dependencies=[]` keeps ONE task alive for the component's lifetime. + # Reading `threshold` directly here would freeze it at its first value. + use_async_effect(watch, dependencies=[]) + + async def handle_threshold_click(event): + set_threshold(LOW_THRESHOLD if threshold == HIGH_THRESHOLD else HIGH_THRESHOLD) + + return html.div( + html.p(f"threshold: {threshold}"), + html.button( + {"onClick": handle_threshold_click}, + "Toggle threshold", + ), + html.p(f"alerts: {len(alerts)}"), + ) diff --git a/docs/examples/synchronizing_with_effects/non_react_widget/app.py b/docs/examples/synchronizing_with_effects/non_react_widget/app.py new file mode 100644 index 000000000..9344454a0 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/non_react_widget/app.py @@ -0,0 +1,44 @@ +"""Keep a non-ReactPy object in sync with state, and destroy it on unmount.""" + +from data import ReportEngine + +from reactpy import Ref, component, html, use_effect, use_ref, use_state + + +# start +@component +def report(name): + page_size, set_page_size = use_state("letter") + # The engine lives in a ref: it has to survive renders, and changing it + # must not itself trigger a render. + engine_box: Ref[ReportEngine | None] = use_ref(None) + + def create_engine(): + engine = ReportEngine(name) + engine_box.current = engine + + def destroy(): + engine.destroy() + engine_box.current = None + + return destroy + + use_effect(create_engine, dependencies=[name]) + + def apply_page_size(): + if engine_box.current is not None: + engine_box.current.set_page_size(page_size) + + use_effect(apply_page_size, dependencies=[page_size]) + + async def handle_page_size_click(event): + set_page_size("a4" if page_size == "letter" else "letter") + + return html.div( + html.h2(name), + html.p(f"page size: {page_size}"), + html.button( + {"onClick": handle_page_size_click}, + "Toggle page size", + ), + ) diff --git a/docs/examples/synchronizing_with_effects/non_react_widget/data.py b/docs/examples/synchronizing_with_effects/non_react_widget/data.py new file mode 100644 index 000000000..231ec9025 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/non_react_widget/data.py @@ -0,0 +1,20 @@ +"""A stand-in for a third-party object with imperative setters and a teardown.""" + +engines = [] + + +class ReportEngine: + def __init__(self, name): + self.name = name + self.page_size = "letter" + self.alive = True + self.log = [f"created {name}"] + engines.append(self) + + def set_page_size(self, size): + self.page_size = size + self.log.append(f"set_page_size {size}") + + def destroy(self): + self.alive = False + self.log.append(f"destroyed {self.name}") diff --git a/docs/examples/synchronizing_with_effects/per_render/app.py b/docs/examples/synchronizing_with_effects/per_render/app.py new file mode 100644 index 000000000..8f71ce7a3 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/per_render/app.py @@ -0,0 +1,43 @@ +"""Each render gets its own Effect, closing over that render's values.""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def audit_log(room, message): + entries, set_entries = use_state([]) + + def effect(): + # This closure sees the `message` from the render that CREATED this + # Effect -- not the newest one. The dependency list says when to make + # a new Effect; the values it reads are frozen from that render. + set_entries(lambda previous: [*previous, f"{room} <- {message}"]) + + use_effect(effect, dependencies=[room]) + + return html.ul( + [html.li({"key": index}, line) for index, line in enumerate(entries)] + ) + + +@component +def app(): + room, set_room = use_state("lobby") + message, set_message = use_state("hello") + + async def handle_message_click(event): + set_message(message + "!") + + async def handle_room_click(event): + set_room("dev" if room == "lobby" else "lobby") + + return html.div( + html.p(f"room={room} message={message!r}"), + html.button({"onClick": handle_message_click}, "Type !"), + html.button( + {"onClick": handle_room_click}, + "Switch room", + ), + audit_log(room, message), + ) diff --git a/docs/examples/synchronizing_with_effects/subscribe/app.py b/docs/examples/synchronizing_with_effects/subscribe/app.py new file mode 100644 index 000000000..c95669bda --- /dev/null +++ b/docs/examples/synchronizing_with_effects/subscribe/app.py @@ -0,0 +1,55 @@ +"""Subscribe on mount, unsubscribe on unmount, and never leak a subscriber.""" + +from data import ticker + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def scroll_log(topic): + lines, set_lines = use_state([]) + + def effect(): + # The clean-up is what keeps the subscriber count at 1 across + # re-subscriptions. Forget it and every render adds another callback. + def on_message(message): + set_lines(lambda previous: [*previous, message]) + + unsubscribe = ticker.subscribe(topic, on_message) + return unsubscribe + + use_effect(effect, dependencies=[topic]) + + return html.div( + html.h2(topic), + html.ul([html.li({"key": index}, line) for index, line in enumerate(lines)]), + ) + + +@component +def app(): + topic, set_topic = use_state("news") + watching, set_watching = use_state(True) + + async def handle_topic_click(event): + set_topic("sports" if topic == "news" else "news") + + async def handle_watching_click(event): + set_watching(not watching) + + async def handle_click(event): + ticker.publish(topic, "ping") + + return html.div( + html.button( + {"onClick": handle_topic_click}, + "Switch topic", + ), + html.button( + {"onClick": handle_watching_click}, + "Stop watching" if watching else "Watch", + ), + html.button({"onClick": handle_click}, "Ping"), + *([] if not watching else [scroll_log(topic)]), + ) diff --git a/docs/examples/synchronizing_with_effects/subscribe/data.py b/docs/examples/synchronizing_with_effects/subscribe/data.py new file mode 100644 index 000000000..87e1f9418 --- /dev/null +++ b/docs/examples/synchronizing_with_effects/subscribe/data.py @@ -0,0 +1,27 @@ +"""A stand-in for any pub/sub system with subscribe/unsubscribe semantics.""" + + +class Ticker: + def __init__(self): + self.topics = {} + self.log = [] + + def subscribe(self, topic, callback): + self.log.append(f"subscribe {topic}") + self.topics.setdefault(topic, []).append(callback) + + def unsubscribe(): + self.log.append(f"unsubscribe {topic}") + self.topics[topic].remove(callback) + + return unsubscribe + + def publish(self, topic, message): + for callback in list(self.topics.get(topic, [])): + callback(message) + + def subscriber_count(self, topic): + return len(self.topics.get(topic, [])) + + +ticker = Ticker() diff --git a/docs/examples/thinking_in_react/add_inverse_data_flow.css b/docs/examples/thinking_in_react/add_inverse_data_flow.css new file mode 100644 index 000000000..4be625123 --- /dev/null +++ b/docs/examples/thinking_in_react/add_inverse_data_flow.css @@ -0,0 +1,14 @@ +body { + padding: 5px; +} +label { + display: block; + margin-top: 5px; + margin-bottom: 5px; +} +th { + padding: 4px; +} +td { + padding: 2px; +} diff --git a/docs/examples/thinking_in_react/add_inverse_data_flow.py b/docs/examples/thinking_in_react/add_inverse_data_flow.py new file mode 100644 index 000000000..b562eb63a --- /dev/null +++ b/docs/examples/thinking_in_react/add_inverse_data_flow.py @@ -0,0 +1,112 @@ +from reactpy import component, html, use_state + + +@component +def filterable_product_table(products): + filter_text, set_filter_text = use_state("") + in_stock_only, set_in_stock_only = use_state(False) + + return html.div( + search_bar( + filter_text=filter_text, + in_stock_only=in_stock_only, + set_filter_text=set_filter_text, + set_in_stock_only=set_in_stock_only, + ), + product_table( + products=products, filter_text=filter_text, in_stock_only=in_stock_only + ), + ) + + +@component +def product_category_row(category): + return html.tr( + html.th({"colspan": 2}, category), + ) + + +@component +def product_row(product): + if product["stocked"]: + name = product["name"] + else: + name = html.span({"style": {"color": "red"}}, product["name"]) + + return html.tr( + html.td(name), + html.td(product["price"]), + ) + + +@component +def product_table(products, filter_text, in_stock_only): + rows = [] + last_category = None + + for product in products: + if filter_text.lower() not in product["name"].lower(): + continue + if in_stock_only and not product["stocked"]: + continue + if product["category"] != last_category: + rows.append( + product_category_row(product["category"], key=product["category"]) + ) + rows.append(product_row(product, key=product["name"])) + last_category = product["category"] + + return html.table( + html.thead( + html.tr( + html.th("Name"), + html.th("Price"), + ), + ), + html.tbody(rows), + ) + + +@component +def search_bar(filter_text, in_stock_only, set_filter_text, set_in_stock_only): + async def handle_filter_text_change(event): + set_filter_text(await event.target.value) + + async def handle_in_stock_only_change(event): + set_in_stock_only(await event.target.checked) + + return html.form( + html.input( + { + "type": "text", + "value": filter_text, + "placeholder": "Search...", + "onChange": handle_filter_text_change, + } + ), + html.label( + html.input( + { + "type": "checkbox", + "checked": in_stock_only, + "onChange": handle_in_stock_only_change, + } + ), + "Only show products in stock", + ), + ) + + +PRODUCTS = [ + {"category": "Fruits", "price": "$1", "stocked": True, "name": "Apple"}, + {"category": "Fruits", "price": "$1", "stocked": True, "name": "Dragonfruit"}, + {"category": "Fruits", "price": "$2", "stocked": False, "name": "Passionfruit"}, + {"category": "Vegetables", "price": "$2", "stocked": True, "name": "Spinach"}, + {"category": "Vegetables", "price": "$4", "stocked": False, "name": "Pumpkin"}, + {"category": "Vegetables", "price": "$1", "stocked": True, "name": "Peas"}, +] + + +@component +def app(): + return filterable_product_table(PRODUCTS) diff --git a/docs/examples/thinking_in_react/build_a_static_version_in_react.css b/docs/examples/thinking_in_react/build_a_static_version_in_react.css new file mode 100644 index 000000000..5b8624389 --- /dev/null +++ b/docs/examples/thinking_in_react/build_a_static_version_in_react.css @@ -0,0 +1,15 @@ +body { + padding: 5px; +} +label { + display: block; + margin-top: 5px; + margin-bottom: 5px; +} +th { + padding-top: 10px; +} +td { + padding: 2px; + padding-right: 40px; +} diff --git a/docs/examples/thinking_in_react/build_a_static_version_in_react.py b/docs/examples/thinking_in_react/build_a_static_version_in_react.py new file mode 100644 index 000000000..3c8c46761 --- /dev/null +++ b/docs/examples/thinking_in_react/build_a_static_version_in_react.py @@ -0,0 +1,60 @@ +from reactpy import component, html + + +@component +def product_category_row(category): + return html.tr(html.th({"colSpan": "2"}, category)) + + +@component +def product_row(product): + if product["stocked"]: + name = product["name"] + else: + name = html.span({"style": {"color": "red"}}, product["name"]) + return html.tr(html.td(name), html.td(product["price"])) + + +@component +def product_table(products): + rows = [] + last_category = None + for product in products: + if product["category"] != last_category: + rows.append( + product_category_row(product["category"], key=product["category"]) + ) + rows.append(product_row(product, key=product["name"])) + last_category = product["category"] + + return html.table( + html.thead(html.tr(html.th("Name"), html.th("Price"))), html.tbody(rows) + ) + + +@component +def search_bar(): + return html.form( + html.input({"type": "text", "placeholder": "Search..."}), + html.label(html.input({"type": "checkbox"}), "Only show products in stock"), + ) + + +@component +def filterable_product_table(products): + return html.div(search_bar(), product_table(products)) + + +PRODUCTS = [ + {"category": "Fruits", "price": "$1", "stocked": True, "name": "Apple"}, + {"category": "Fruits", "price": "$1", "stocked": True, "name": "Dragonfruit"}, + {"category": "Fruits", "price": "$2", "stocked": False, "name": "Passionfruit"}, + {"category": "Vegetables", "price": "$2", "stocked": True, "name": "Spinach"}, + {"category": "Vegetables", "price": "$4", "stocked": False, "name": "Pumpkin"}, + {"category": "Vegetables", "price": "$1", "stocked": True, "name": "Peas"}, +] + + +@component +def app(): + return filterable_product_table(PRODUCTS) diff --git a/docs/examples/thinking_in_react/error_example.py b/docs/examples/thinking_in_react/error_example.py new file mode 100644 index 000000000..e1ae45c3c --- /dev/null +++ b/docs/examples/thinking_in_react/error_example.py @@ -0,0 +1,19 @@ +from reactpy import component, html + + +# start +@component +def search_bar(filter_text, in_stock_only): + return html.form( + html.input( + { + "type": "text", + "value": filter_text, + "placeholder": "Search...", + } + ), + html.p( + html.input({"type": "checkbox", "checked": in_stock_only}), + "Only show products in stock", + ), + ) diff --git a/docs/examples/thinking_in_react/event_handlers.py b/docs/examples/thinking_in_react/event_handlers.py new file mode 100644 index 000000000..1efab28df --- /dev/null +++ b/docs/examples/thinking_in_react/event_handlers.py @@ -0,0 +1,21 @@ +from reactpy import html + +filter_text = "" + + +def set_filter_text(value): ... + + +# start +async def handle_filter_text_change(event): + set_filter_text(await event.target.value) + + +html.input( + { + "type": "text", + "value": filter_text, + "placeholder": "Search...", + "onChange": handle_filter_text_change, + } +) diff --git a/docs/examples/thinking_in_react/identify_where_your_state_should_live.css b/docs/examples/thinking_in_react/identify_where_your_state_should_live.css new file mode 100644 index 000000000..b811252a1 --- /dev/null +++ b/docs/examples/thinking_in_react/identify_where_your_state_should_live.css @@ -0,0 +1,14 @@ +body { + padding: 5px; +} +label { + display: block; + margin-top: 5px; + margin-bottom: 5px; +} +th { + padding-top: 5px; +} +td { + padding: 2px; +} diff --git a/docs/examples/thinking_in_react/identify_where_your_state_should_live.py b/docs/examples/thinking_in_react/identify_where_your_state_should_live.py new file mode 100644 index 000000000..f81a2cd03 --- /dev/null +++ b/docs/examples/thinking_in_react/identify_where_your_state_should_live.py @@ -0,0 +1,88 @@ +from reactpy import component, html, use_state + + +@component +def filterable_product_table(products): + filter_text, set_filter_text = use_state("") + in_stock_only, set_in_stock_only = use_state(False) + + return html.div( + search_bar(filter_text=filter_text, in_stock_only=in_stock_only), + product_table( + products=products, filter_text=filter_text, in_stock_only=in_stock_only + ), + ) + + +@component +def product_category_row(category): + return html.tr( + html.th({"colspan": 2}, category), + ) + + +@component +def product_row(product): + if product["stocked"]: + name = product["name"] + else: + name = html.span({"style": {"color": "red"}}, product["name"]) + + return html.tr( + html.td(name), + html.td(product["price"]), + ) + + +@component +def product_table(products, filter_text, in_stock_only): + rows = [] + last_category = None + + for product in products: + if filter_text.lower() not in product["name"].lower(): + continue + if in_stock_only and not product["stocked"]: + continue + if product["category"] != last_category: + rows.append( + product_category_row(product["category"], key=product["category"]) + ) + rows.append(product_row(product, key=product["name"])) + last_category = product["category"] + + return html.table( + html.thead( + html.tr( + html.th("Name"), + html.th("Price"), + ), + ), + html.tbody(rows), + ) + + +@component +def search_bar(filter_text, in_stock_only): + return html.form( + html.input({"type": "text", "value": filter_text, "placeholder": "Search..."}), + html.label( + html.input({"type": "checkbox", "checked": in_stock_only}), + "Only show products in stock", + ), + ) + + +PRODUCTS = [ + {"category": "Fruits", "price": "$1", "stocked": True, "name": "Apple"}, + {"category": "Fruits", "price": "$1", "stocked": True, "name": "Dragonfruit"}, + {"category": "Fruits", "price": "$2", "stocked": False, "name": "Passionfruit"}, + {"category": "Vegetables", "price": "$2", "stocked": True, "name": "Spinach"}, + {"category": "Vegetables", "price": "$4", "stocked": False, "name": "Pumpkin"}, + {"category": "Vegetables", "price": "$1", "stocked": True, "name": "Peas"}, +] + + +@component +def app(): + return filterable_product_table(PRODUCTS) diff --git a/docs/examples/thinking_in_react/set_state_props.py b/docs/examples/thinking_in_react/set_state_props.py new file mode 100644 index 000000000..821249933 --- /dev/null +++ b/docs/examples/thinking_in_react/set_state_props.py @@ -0,0 +1,20 @@ +from reactpy import component, hooks, html + + +def search_bar(**_kws): ... + + +# start +@component +def filterable_product_table(products): + filter_text, set_filter_text = hooks.use_state("") + in_stock_only, set_in_stock_only = hooks.use_state(False) + + return html.div( + search_bar( + filter_text=filter_text, + in_stock_only=in_stock_only, + set_filter_text=set_filter_text, + set_in_stock_only=set_in_stock_only, + ) + ) diff --git a/docs/examples/thinking_in_react/start_with_the_mockup.json b/docs/examples/thinking_in_react/start_with_the_mockup.json new file mode 100644 index 000000000..f49a528b0 --- /dev/null +++ b/docs/examples/thinking_in_react/start_with_the_mockup.json @@ -0,0 +1,38 @@ +[ + { + "category": "Fruits", + "price": "$1", + "stocked": true, + "name": "Apple" + }, + { + "category": "Fruits", + "price": "$1", + "stocked": true, + "name": "Dragonfruit" + }, + { + "category": "Fruits", + "price": "$2", + "stocked": false, + "name": "Passionfruit" + }, + { + "category": "Vegetables", + "price": "$2", + "stocked": true, + "name": "Spinach" + }, + { + "category": "Vegetables", + "price": "$4", + "stocked": false, + "name": "Pumpkin" + }, + { + "category": "Vegetables", + "price": "$1", + "stocked": true, + "name": "Peas" + } +] diff --git a/docs/examples/thinking_in_react/use_state.py b/docs/examples/thinking_in_react/use_state.py new file mode 100644 index 000000000..1844a9d66 --- /dev/null +++ b/docs/examples/thinking_in_react/use_state.py @@ -0,0 +1,8 @@ +from reactpy import component, use_state + + +# start +@component +def filterable_product_table(products): + _filter_text, _set_filter_text = use_state("") + in_stock_only, set_in_stock_only = use_state(False) diff --git a/docs/examples/thinking_in_react/use_state_with_components.py b/docs/examples/thinking_in_react/use_state_with_components.py new file mode 100644 index 000000000..fa683cd33 --- /dev/null +++ b/docs/examples/thinking_in_react/use_state_with_components.py @@ -0,0 +1,20 @@ +from reactpy import html + +filter_text = "" +in_stock_only = False +products = () + + +def search_bar(**_kw): ... + + +def product_table(**_kw): ... + + +# start +html.div( + search_bar(filter_text=filter_text, in_stock_only=in_stock_only), + product_table( + products=products, filter_text=filter_text, in_stock_only=in_stock_only + ), +) diff --git a/docs/examples/tutorial_tic_tac_toe/board/app.py b/docs/examples/tutorial_tic_tac_toe/board/app.py new file mode 100644 index 000000000..2615a4ce7 --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/board/app.py @@ -0,0 +1,26 @@ +from reactpy import component, html + + +# start +@component +def board(): + return html.fragment( + html.div( + {"className": "board-row"}, + html.button({"className": "square"}, "1"), + html.button({"className": "square"}, "2"), + html.button({"className": "square"}, "3"), + ), + html.div( + {"className": "board-row"}, + html.button({"className": "square"}, "4"), + html.button({"className": "square"}, "5"), + html.button({"className": "square"}, "6"), + ), + html.div( + {"className": "board-row"}, + html.button({"className": "square"}, "7"), + html.button({"className": "square"}, "8"), + html.button({"className": "square"}, "9"), + ), + ) diff --git a/docs/examples/tutorial_tic_tac_toe/declaring_winner/app.py b/docs/examples/tutorial_tic_tac_toe/declaring_winner/app.py new file mode 100644 index 000000000..bc2a1b98f --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/declaring_winner/app.py @@ -0,0 +1,69 @@ +from reactpy import component, html, use_state + + +@component +def square(value, on_click): + return html.button({"className": "square", "onClick": on_click}, value) + + +# start +@component +def board(): + initial_board: list[str | None] = [None] * 9 + squares, set_squares = use_state(initial_board) + x_is_next, set_x_is_next = use_state(True) + + def handle_click(i): + async def inner(event): + if calculate_winner(squares) or squares[i]: + return + next_squares = squares.copy() + next_squares[i] = "X" if x_is_next else "O" + set_squares(next_squares) + set_x_is_next(not x_is_next) + + return inner + + winner = calculate_winner(squares) + status = ( + f"Winner: {winner}" if winner else "Next player: " + ("X" if x_is_next else "O") + ) + + return html.fragment( + html.div({"className": "status"}, status), + html.div( + {"className": "board-row"}, + square(squares[0], handle_click(0)), + square(squares[1], handle_click(1)), + square(squares[2], handle_click(2)), + ), + html.div( + {"className": "board-row"}, + square(squares[3], handle_click(3)), + square(squares[4], handle_click(4)), + square(squares[5], handle_click(5)), + ), + html.div( + {"className": "board-row"}, + square(squares[6], handle_click(6)), + square(squares[7], handle_click(7)), + square(squares[8], handle_click(8)), + ), + ) + + +def calculate_winner(squares): + lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + for a, b, c in lines: + if squares[a] and squares[a] == squares[b] and squares[a] == squares[c]: + return squares[a] + return None diff --git a/docs/examples/tutorial_tic_tac_toe/game.css b/docs/examples/tutorial_tic_tac_toe/game.css new file mode 100644 index 000000000..8c73e818c --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/game.css @@ -0,0 +1,43 @@ +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.square { + background: #fff; + color: #000; + border: 1px solid #999; + float: left; + font-size: 24px; + font-weight: bold; + line-height: 34px; + height: 34px; + margin-right: -1px; + margin-top: -1px; + padding: 0; + text-align: center; + width: 34px; +} + +.board-row:after { + clear: both; + content: ""; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} diff --git a/docs/examples/tutorial_tic_tac_toe/guarded_turns/app.py b/docs/examples/tutorial_tic_tac_toe/guarded_turns/app.py new file mode 100644 index 000000000..21747a005 --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/guarded_turns/app.py @@ -0,0 +1,49 @@ +from reactpy import component, html, use_state + + +@component +def square(value, on_click): + return html.button({"className": "square", "onClick": on_click}, value) + + +# start +@component +def board(): + initial_board: list[str | None] = [None] * 9 + squares, set_squares = use_state(initial_board) + x_is_next, set_x_is_next = use_state(True) + + def handle_click(i): + async def inner(event): + if squares[i]: + return + next_squares = squares.copy() + next_squares[i] = "X" if x_is_next else "O" + set_squares(next_squares) + set_x_is_next(not x_is_next) + + return inner + + status = "Next player: " + ("X" if x_is_next else "O") + + return html.fragment( + html.div({"className": "status"}, status), + html.div( + {"className": "board-row"}, + square(squares[0], handle_click(0)), + square(squares[1], handle_click(1)), + square(squares[2], handle_click(2)), + ), + html.div( + {"className": "board-row"}, + square(squares[3], handle_click(3)), + square(squares[4], handle_click(4)), + square(squares[5], handle_click(5)), + ), + html.div( + {"className": "board-row"}, + square(squares[6], handle_click(6)), + square(squares[7], handle_click(7)), + square(squares[8], handle_click(8)), + ), + ) diff --git a/docs/examples/tutorial_tic_tac_toe/lifting_state/app.py b/docs/examples/tutorial_tic_tac_toe/lifting_state/app.py new file mode 100644 index 000000000..abf4e89ad --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/lifting_state/app.py @@ -0,0 +1,44 @@ +from reactpy import component, html, use_state + + +# start +@component +def square(value, on_click): + return html.button({"className": "square", "onClick": on_click}, value) + + +@component +def board(): + initial_board: list[str | None] = [None] * 9 + squares, set_squares = use_state(initial_board) + + def handle_click(i): + async def inner(event): + if squares[i]: + return + next_squares = squares.copy() + next_squares[i] = "X" + set_squares(next_squares) + + return inner + + return html.fragment( + html.div( + {"className": "board-row"}, + square(squares[0], handle_click(0)), + square(squares[1], handle_click(1)), + square(squares[2], handle_click(2)), + ), + html.div( + {"className": "board-row"}, + square(squares[3], handle_click(3)), + square(squares[4], handle_click(4)), + square(squares[5], handle_click(5)), + ), + html.div( + {"className": "board-row"}, + square(squares[6], handle_click(6)), + square(squares[7], handle_click(7)), + square(squares[8], handle_click(8)), + ), + ) diff --git a/docs/examples/tutorial_tic_tac_toe/mutating/app.py b/docs/examples/tutorial_tic_tac_toe/mutating/app.py new file mode 100644 index 000000000..e10321316 --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/mutating/app.py @@ -0,0 +1,44 @@ +from reactpy import component, html, use_state + + +@component +def square(value, on_click): + return html.button({"className": "square", "onClick": on_click}, value) + + +# start +@component +def board(): + initial_board: list[str | None] = [None] * 9 + squares, set_squares = use_state(initial_board) + + def handle_click(i): + # Mutating in place and handing back the same object: ReactPy compares + # the new value with the old one, sees the same object, and schedules no + # render at all. The button looks fine and never responds. + async def inner(event): + squares[i] = "X" + set_squares(squares) + + return inner + + return html.fragment( + html.div( + {"className": "board-row"}, + square(squares[0], handle_click(0)), + square(squares[1], handle_click(1)), + square(squares[2], handle_click(2)), + ), + html.div( + {"className": "board-row"}, + square(squares[3], handle_click(3)), + square(squares[4], handle_click(4)), + square(squares[5], handle_click(5)), + ), + html.div( + {"className": "board-row"}, + square(squares[6], handle_click(6)), + square(squares[7], handle_click(7)), + square(squares[8], handle_click(8)), + ), + ) diff --git a/docs/examples/tutorial_tic_tac_toe/nine_squares/app.py b/docs/examples/tutorial_tic_tac_toe/nine_squares/app.py new file mode 100644 index 000000000..e8e120538 --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/nine_squares/app.py @@ -0,0 +1,26 @@ +from reactpy import component, html + + +# start +@component +def square(): + return html.fragment( + html.div( + {"className": "board-row"}, + html.button({"className": "square"}, "1"), + html.button({"className": "square"}, "2"), + html.button({"className": "square"}, "3"), + ), + html.div( + {"className": "board-row"}, + html.button({"className": "square"}, "4"), + html.button({"className": "square"}, "5"), + html.button({"className": "square"}, "6"), + ), + html.div( + {"className": "board-row"}, + html.button({"className": "square"}, "7"), + html.button({"className": "square"}, "8"), + html.button({"className": "square"}, "9"), + ), + ) diff --git a/docs/examples/tutorial_tic_tac_toe/passing_props/app.py b/docs/examples/tutorial_tic_tac_toe/passing_props/app.py new file mode 100644 index 000000000..2582cdc14 --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/passing_props/app.py @@ -0,0 +1,31 @@ +from reactpy import component, html + + +# start +@component +def square(value): + return html.button({"className": "square"}, value) + + +@component +def board(): + return html.fragment( + html.div( + {"className": "board-row"}, + square("A"), + square("B"), + square("C"), + ), + html.div( + {"className": "board-row"}, + square("D"), + square("E"), + square("F"), + ), + html.div( + {"className": "board-row"}, + square("G"), + square("H"), + square("I"), + ), + ) diff --git a/docs/examples/tutorial_tic_tac_toe/showing_moves/app.py b/docs/examples/tutorial_tic_tac_toe/showing_moves/app.py new file mode 100644 index 000000000..d6cf4b8ba --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/showing_moves/app.py @@ -0,0 +1,102 @@ +from copy import deepcopy + +from reactpy import component, html, use_state + + +@component +def square(value, on_click): + return html.button({"className": "square", "onClick": on_click}, value) + + +@component +def board(x_is_next, squares, on_play): + def handle_click(i): + async def inner(event): + if calculate_winner(squares) or squares[i]: + return + next_squares = squares.copy() + next_squares[i] = "X" if x_is_next else "O" + on_play(next_squares) + + return inner + + winner = calculate_winner(squares) + status = ( + f"Winner: {winner}" if winner else "Next player: " + ("X" if x_is_next else "O") + ) + + return html.fragment( + html.div({"className": "status"}, status), + html.div( + {"className": "board-row"}, + square(squares[0], handle_click(0)), + square(squares[1], handle_click(1)), + square(squares[2], handle_click(2)), + ), + html.div( + {"className": "board-row"}, + square(squares[3], handle_click(3)), + square(squares[4], handle_click(4)), + square(squares[5], handle_click(5)), + ), + html.div( + {"className": "board-row"}, + square(squares[6], handle_click(6)), + square(squares[7], handle_click(7)), + square(squares[8], handle_click(8)), + ), + ) + + +# start +@component +def game(): + history, set_history = use_state([[None] * 9]) + current_move, set_current_move = use_state(0) + + def handle_play(next_squares): + next_history = deepcopy(history[: current_move + 1]) + next_history.append(next_squares) + set_history(next_history) + set_current_move(len(next_history) - 1) + + def jump_to(move): + async def inner(event): + # TODO: make the board show move + pass + + return inner + + x_is_next = current_move % 2 == 0 + current_squares = history[current_move] + + moves = [] + for move in range(len(history)): + description = f"Go to move #{move}" if move > 0 else "Go to game start" + moves.append(html.li(html.button({"onClick": jump_to(move)}, description))) + + return html.div( + {"className": "game"}, + html.div( + {"className": "game-board"}, + board(x_is_next, current_squares, handle_play), + ), + html.div({"className": "game-info"}, html.ol(moves)), + ) + + +def calculate_winner(squares): + lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + for a, b, c in lines: + if squares[a] and squares[a] == squares[b] and squares[a] == squares[c]: + return squares[a] + return None diff --git a/docs/examples/tutorial_tic_tac_toe/square_starter/app.py b/docs/examples/tutorial_tic_tac_toe/square_starter/app.py new file mode 100644 index 000000000..e0893d6b4 --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/square_starter/app.py @@ -0,0 +1,7 @@ +from reactpy import component, html + + +# start +@component +def square(): + return html.button({"className": "square"}, "X") diff --git a/docs/examples/tutorial_tic_tac_toe/square_starter/main.py b/docs/examples/tutorial_tic_tac_toe/square_starter/main.py new file mode 100644 index 000000000..bb54b6cba --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/square_starter/main.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from app import square + +from reactpy import html +from reactpy.executors.asgi import ReactPy + +# ReactPy serves the components, not the files on your disk, so the stylesheet +# is read here and injected into the page's . +CSS = Path("styles.css").read_text() + +my_app = ReactPy(square, html_head=html.head(html.style(CSS))) diff --git a/docs/examples/tutorial_tic_tac_toe/square_state/app.py b/docs/examples/tutorial_tic_tac_toe/square_state/app.py new file mode 100644 index 000000000..43b9e7a21 --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/square_state/app.py @@ -0,0 +1,39 @@ +from reactpy import component, html, use_state + +# start +# None is what an unfilled square holds; ReactPy renders None as nothing. +EMPTY_SQUARE: str | None = None + + +@component +def square(): + value, set_value = use_state(EMPTY_SQUARE) + + async def handle_click(event): + set_value("X") + + return html.button({"className": "square", "onClick": handle_click}, value) + + +@component +def board(): + return html.fragment( + html.div( + {"className": "board-row"}, + square(), + square(), + square(), + ), + html.div( + {"className": "board-row"}, + square(), + square(), + square(), + ), + html.div( + {"className": "board-row"}, + square(), + square(), + square(), + ), + ) diff --git a/docs/examples/tutorial_tic_tac_toe/storing_history/app.py b/docs/examples/tutorial_tic_tac_toe/storing_history/app.py new file mode 100644 index 000000000..4aa45f120 --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/storing_history/app.py @@ -0,0 +1,85 @@ +from reactpy import component, html, use_state + + +@component +def square(value, on_click): + return html.button({"className": "square", "onClick": on_click}, value) + + +@component +def board(x_is_next, squares, on_play): + def handle_click(i): + async def inner(event): + if calculate_winner(squares) or squares[i]: + return + next_squares = squares.copy() + next_squares[i] = "X" if x_is_next else "O" + on_play(next_squares) + + return inner + + winner = calculate_winner(squares) + status = ( + f"Winner: {winner}" if winner else "Next player: " + ("X" if x_is_next else "O") + ) + + return html.fragment( + html.div({"className": "status"}, status), + html.div( + {"className": "board-row"}, + square(squares[0], handle_click(0)), + square(squares[1], handle_click(1)), + square(squares[2], handle_click(2)), + ), + html.div( + {"className": "board-row"}, + square(squares[3], handle_click(3)), + square(squares[4], handle_click(4)), + square(squares[5], handle_click(5)), + ), + html.div( + {"className": "board-row"}, + square(squares[6], handle_click(6)), + square(squares[7], handle_click(7)), + square(squares[8], handle_click(8)), + ), + ) + + +# start +@component +def game(): + history, set_history = use_state([[None] * 9]) + + def handle_play(next_squares): + set_history([*history, next_squares]) + + current_move = len(history) - 1 + x_is_next = current_move % 2 == 0 + current_squares = history[current_move] + + return html.div( + {"className": "game"}, + html.div( + {"className": "game-board"}, + board(x_is_next, current_squares, handle_play), + ), + html.div({"className": "game-info"}, html.ol()), + ) + + +def calculate_winner(squares): + lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + for a, b, c in lines: + if squares[a] and squares[a] == squares[b] and squares[a] == squares[c]: + return squares[a] + return None diff --git a/docs/examples/tutorial_tic_tac_toe/taking_turns/app.py b/docs/examples/tutorial_tic_tac_toe/taking_turns/app.py new file mode 100644 index 000000000..084e0331b --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/taking_turns/app.py @@ -0,0 +1,47 @@ +from reactpy import component, html, use_state + + +@component +def square(value, on_click): + return html.button({"className": "square", "onClick": on_click}, value) + + +# start +@component +def board(): + initial_board: list[str | None] = [None] * 9 + squares, set_squares = use_state(initial_board) + x_is_next, set_x_is_next = use_state(True) + + def handle_click(i): + async def inner(event): + next_squares = squares.copy() + next_squares[i] = "X" if x_is_next else "O" + set_squares(next_squares) + set_x_is_next(not x_is_next) + + return inner + + status = "Next player: " + ("X" if x_is_next else "O") + + return html.fragment( + html.div({"className": "status"}, status), + html.div( + {"className": "board-row"}, + square(squares[0], handle_click(0)), + square(squares[1], handle_click(1)), + square(squares[2], handle_click(2)), + ), + html.div( + {"className": "board-row"}, + square(squares[3], handle_click(3)), + square(squares[4], handle_click(4)), + square(squares[5], handle_click(5)), + ), + html.div( + {"className": "board-row"}, + square(squares[6], handle_click(6)), + square(squares[7], handle_click(7)), + square(squares[8], handle_click(8)), + ), + ) diff --git a/docs/examples/tutorial_tic_tac_toe/time_travel/app.py b/docs/examples/tutorial_tic_tac_toe/time_travel/app.py new file mode 100644 index 000000000..64d9e1d89 --- /dev/null +++ b/docs/examples/tutorial_tic_tac_toe/time_travel/app.py @@ -0,0 +1,116 @@ +from reactpy import component, html, use_state + + +@component +def square(value, on_click): + return html.button( + {"className": "square", "onClick": on_click}, + value, + ) + + +@component +def board(x_is_next, squares, on_play): + def handle_click(i): + async def inner(event): + """ + Due to a quirk of Python, if your event handler needs args other than + `event`, you will need to create a wrapper function as seen above. + Ref: https://pylint.readthedocs.io/en/stable/user_guide/messages/warning/cell-var-from-loop.html + """ + if calculate_winner(squares) or squares[i]: + return + + next_squares = squares.copy() + next_squares[i] = "X" if x_is_next else "O" + on_play(next_squares) + + return inner + + winner = calculate_winner(squares) + status = ( + f"Winner: {winner}" if winner else "Next player: " + ("X" if x_is_next else "O") + ) + + return html.fragment( + html.div({"className": "status"}, status), + html.div( + {"className": "board-row"}, + square(squares[0], handle_click(0)), + square(squares[1], handle_click(1)), + square(squares[2], handle_click(2)), + ), + html.div( + {"className": "board-row"}, + square(squares[3], handle_click(3)), + square(squares[4], handle_click(4)), + square(squares[5], handle_click(5)), + ), + html.div( + {"className": "board-row"}, + square(squares[6], handle_click(6)), + square(squares[7], handle_click(7)), + square(squares[8], handle_click(8)), + ), + ) + + +@component +def game(): + history, set_history = use_state([[None] * 9]) + current_move, set_current_move = use_state(0) + x_is_next = current_move % 2 == 0 + current_squares = history[current_move] + + def handle_play(next_squares): + next_history = history[: current_move + 1] + next_history.append(next_squares) + set_history(next_history) + set_current_move(len(next_history) - 1) + + def jump_to(next_move): + async def go_to_move(event): + set_current_move(next_move) + + return go_to_move + + moves = [] + for move, _squares in enumerate(history): + description = f"Go to move #{move}" if move > 0 else "Go to game start" + + moves.append( + html.li( + {"key": move}, + html.button({"onClick": jump_to(move)}, description), + ) + ) + + return html.div( + {"className": "game"}, + html.div( + {"className": "game-board"}, + board(x_is_next, current_squares, handle_play), + ), + html.div({"className": "game-info"}, html.ol(moves)), + ) + + +def calculate_winner(squares): + lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + for line in lines: + a, b, c = line + if not squares: + continue + if squares[a] and squares[a] == squares[b] and squares[a] == squares[c]: + return squares[a] + + return None diff --git a/docs/examples/updating_arrays_in_state/add_to_array/app.py b/docs/examples/updating_arrays_in_state/add_to_array/app.py new file mode 100644 index 000000000..936468fcd --- /dev/null +++ b/docs/examples/updating_arrays_in_state/add_to_array/app.py @@ -0,0 +1,35 @@ +from reactpy import component, html, use_state + + +# start +@component +def todo_list(): + items, set_items = use_state([{"id": 0, "text": "Walk the dog"}]) + text, set_text = use_state("") + + async def handle_add(event): + event.preventDefault() + if not text: + return + # A new list; `items.append(...)` would be silently ignored. + next_id = max((item["id"] for item in items), default=-1) + 1 + set_items([*items, {"id": next_id, "text": text}]) + set_text("") + + async def handle_text_change(event): + set_text(await event.target.value) + + return html.div( + html.form( + {"onSubmit": handle_add}, + html.input( + { + "value": text, + "placeholder": "To do...", + "onChange": handle_text_change, + } + ), + html.button({"type": "submit"}, "Add"), + ), + html.ul([html.li({"key": item["id"]}, item["text"]) for item in items]), + ) diff --git a/docs/examples/updating_arrays_in_state/insert_at_index/app.py b/docs/examples/updating_arrays_in_state/insert_at_index/app.py new file mode 100644 index 000000000..ce4e728f1 --- /dev/null +++ b/docs/examples/updating_arrays_in_state/insert_at_index/app.py @@ -0,0 +1,39 @@ +from reactpy import component, html, use_state + + +# start +@component +def todo_list(): + items, set_items = use_state( + [ + {"id": 0, "text": "Walk the dog"}, + {"id": 1, "text": "Brush the cat"}, + ] + ) + text, set_text = use_state("") + + async def handle_insert(event): + event.preventDefault() + if not text: + return + # Slicing builds the new list; list.insert() would mutate in place. + set_items([*items[:1], {"id": 3, "text": text}, *items[1:]]) + set_text("") + + async def handle_text_change(event): + set_text(await event.target.value) + + return html.div( + html.form( + {"onSubmit": handle_insert}, + html.input( + { + "value": text, + "placeholder": "Insert at index 1", + "onChange": handle_text_change, + } + ), + html.button({"type": "submit"}, "Insert"), + ), + html.ul([html.li({"key": item["id"]}, item["text"]) for item in items]), + ) diff --git a/docs/examples/updating_arrays_in_state/objects_in_arrays/app.py b/docs/examples/updating_arrays_in_state/objects_in_arrays/app.py new file mode 100644 index 000000000..e7329e7b3 --- /dev/null +++ b/docs/examples/updating_arrays_in_state/objects_in_arrays/app.py @@ -0,0 +1,60 @@ +from reactpy import component, html, use_state + + +def initial_artworks(): + return [ + { + "id": 0, + "title": "The Day of the Ribbon", + "artist": "Eva Gonzalès", + "seen": False, + }, + { + "id": 1, + "title": "A loge at the Folies-Bergère", + "artist": "Édouard Manet", + "seen": False, + }, + ] + + +# start +@component +def form(): + artworks, set_artworks = use_state(initial_artworks()) + + def handle_toggle(id_to_toggle): + async def handler(event): + # Copy the LIST, and copy the one dict inside it that changed. + # A shallow copy would leave the dict shared with the old state. + set_artworks( + [ + ( + {**artwork, "seen": not artwork["seen"]} + if artwork["id"] == id_to_toggle + else artwork + ) + for artwork in artworks + ] + ) + + return handler + + return html.ul( + [ + html.li( + {"key": artwork["id"]}, + html.label( + html.input( + { + "type": "checkbox", + "checked": artwork["seen"], + "onChange": handle_toggle(artwork["id"]), + } + ), + f"{artwork['title']} - {artwork['artist']}", + ), + ) + for artwork in artworks + ] + ) diff --git a/docs/examples/updating_arrays_in_state/other_changes/app.py b/docs/examples/updating_arrays_in_state/other_changes/app.py new file mode 100644 index 000000000..ea69f7984 --- /dev/null +++ b/docs/examples/updating_arrays_in_state/other_changes/app.py @@ -0,0 +1,46 @@ +from reactpy import component, html, use_state + + +# start +@component +def todo_list(): + items, set_items = use_state( + [ + {"id": 0, "text": "Walk the dog"}, + {"id": 1, "text": "Brush the cat"}, + {"id": 2, "text": "Feed the fish"}, + ] + ) + log, set_log = use_state([]) + + def record(label, next_items): + set_items(next_items) + set_log([*log, f"{label}: {', '.join(i['text'] for i in next_items)}"]) + + async def handle_reversed_click(event): + record("reversed", list(reversed(items))) + + async def handle_sorted_click(event): + record("sorted", sorted(items, key=lambda i: i["text"])) + + async def handle_filtered_click(event): + record("filtered", items[:2]) + + return html.div( + html.div( + html.button( + {"onClick": handle_reversed_click}, + "Reverse", + ), + html.button( + {"onClick": handle_sorted_click}, + "Sort", + ), + html.button( + {"onClick": handle_filtered_click}, + "Keep first two", + ), + ), + html.ul([html.li({"key": item["id"]}, item["text"]) for item in items]), + html.ul([html.li({"key": i}, line) for i, line in enumerate(log)]), + ) diff --git a/docs/examples/updating_arrays_in_state/remove_from_array/app.py b/docs/examples/updating_arrays_in_state/remove_from_array/app.py new file mode 100644 index 000000000..58b39ffae --- /dev/null +++ b/docs/examples/updating_arrays_in_state/remove_from_array/app.py @@ -0,0 +1,31 @@ +from reactpy import component, html, use_state + + +# start +@component +def todo_list(): + items, set_items = use_state( + [ + {"id": 0, "text": "Walk the dog"}, + {"id": 1, "text": "Brush the cat"}, + {"id": 2, "text": "Teach the fish to juggle"}, + ] + ) + + def handle_delete(id_to_keep): + async def handler(event): + # filter() gives a new list; items.remove(...) would mutate it. + set_items([item for item in items if item["id"] != id_to_keep]) + + return handler + + return html.ul( + [ + html.li( + {"key": item["id"]}, + item["text"], + html.button({"onClick": handle_delete(item["id"])}, "Delete"), + ) + for item in items + ] + ) diff --git a/docs/examples/updating_arrays_in_state/replace_item/app.py b/docs/examples/updating_arrays_in_state/replace_item/app.py new file mode 100644 index 000000000..64dc26f91 --- /dev/null +++ b/docs/examples/updating_arrays_in_state/replace_item/app.py @@ -0,0 +1,40 @@ +from reactpy import component, html, use_state + + +# start +@component +def todo_list(): + items, set_items = use_state( + [ + {"id": 0, "text": "Walk the dog"}, + {"id": 1, "text": "Brush the cat"}, + ] + ) + text, set_text = use_state("") + + async def handle_replace(event): + event.preventDefault() + if not text: + return + # Replace one item by building a whole new list. Mutating + # items[0]["text"] would be invisible to ReactPy. + set_items([{"id": 0, "text": text}, *items[1:]]) + set_text("") + + async def handle_text_change(event): + set_text(await event.target.value) + + return html.div( + html.form( + {"onSubmit": handle_replace}, + html.input( + { + "value": text, + "placeholder": "Replace the first item", + "onChange": handle_text_change, + } + ), + html.button({"type": "submit"}, "Replace"), + ), + html.ul([html.li({"key": item["id"]}, item["text"]) for item in items]), + ) diff --git a/docs/examples/updating_arrays_in_state/transform_array/app.py b/docs/examples/updating_arrays_in_state/transform_array/app.py new file mode 100644 index 000000000..361f605a5 --- /dev/null +++ b/docs/examples/updating_arrays_in_state/transform_array/app.py @@ -0,0 +1,21 @@ +from reactpy import component, html, use_state + + +# start +@component +def todo_list(): + items, set_items = use_state( + [ + {"id": 0, "text": "Walk the dog"}, + {"id": 1, "text": "Brush the cat"}, + ] + ) + + async def handle_click(event): + # map() becomes a comprehension; the old items stay untouched. + set_items([{"id": item["id"], "text": item["text"] + "!"} for item in items]) + + return html.div( + html.button({"onClick": handle_click}, "Add !"), + html.ul([html.li({"key": item["id"]}, item["text"]) for item in items]), + ) diff --git a/docs/examples/updating_objects_in_state/form_single_handler/app.py b/docs/examples/updating_objects_in_state/form_single_handler/app.py new file mode 100644 index 000000000..a2fee0bad --- /dev/null +++ b/docs/examples/updating_objects_in_state/form_single_handler/app.py @@ -0,0 +1,47 @@ +from reactpy import component, html, use_state + + +# start +@component +def form(): + person, set_person = use_state( + {"firstName": "Jane", "lastName": "Jacobs", "email": "jane@example.com"} + ) + + # One handler for every field. ReactPy always calls a handler with the + # event as its single positional argument, so the field name is bound by + # the thin handlers below rather than delivered by the client. + async def handle_change(field, event): + set_person({**person, field: await event.target.value}) + + async def handle_first_name_change(event): + await handle_change("firstName", event) + + async def handle_last_name_change(event): + await handle_change("lastName", event) + + async def handle_email_change(event): + await handle_change("email", event) + + return html.div( + html.input( + { + "value": person["firstName"], + "onChange": handle_first_name_change, + } + ), + html.input( + { + "value": person["lastName"], + "onChange": handle_last_name_change, + } + ), + html.input( + { + "value": person["email"], + "onChange": handle_email_change, + } + ), + html.hr(), + html.h1(f"Hello, {person['firstName']} {person['lastName']}!"), + ) diff --git a/docs/examples/updating_objects_in_state/form_spread/app.py b/docs/examples/updating_objects_in_state/form_spread/app.py new file mode 100644 index 000000000..5b669a471 --- /dev/null +++ b/docs/examples/updating_objects_in_state/form_spread/app.py @@ -0,0 +1,32 @@ +from reactpy import component, html, use_state + + +# start +@component +def form(): + person, set_person = use_state( + { + "firstName": "Jane", + "lastName": "Jacobs", + "email": "jane@example.com", + } + ) + + async def handle_first_name(event): + # Copy everything, then override just this one field. + set_person({**person, "firstName": await event.target.value}) + + async def handle_last_name(event): + set_person({**person, "lastName": await event.target.value}) + + async def handle_email(event): + set_person({**person, "email": await event.target.value}) + + return html.div( + html.input({"value": person["firstName"], "onChange": handle_first_name}), + html.input({"value": person["lastName"], "onChange": handle_last_name}), + html.input({"value": person["email"], "onChange": handle_email}), + html.hr(), + html.h1(f"Hello, {person['firstName']} {person['lastName']}!"), + html.a({"href": f"mailto:{person['email']}"}, person["email"]), + ) diff --git a/docs/examples/updating_objects_in_state/moving_dot/app.py b/docs/examples/updating_objects_in_state/moving_dot/app.py new file mode 100644 index 000000000..7b6b9b54b --- /dev/null +++ b/docs/examples/updating_objects_in_state/moving_dot/app.py @@ -0,0 +1,37 @@ +from reactpy import component, html, use_state + + +# start +@component +def moving_dot(): + position, set_position = use_state({"x": 0, "y": 0}) + + async def handle_move(event): + # Replace the whole object: ReactPy never merges a partial update. + set_position({"x": await event.clientX, "y": await event.clientY}) + + return html.div( + { + "onPointerMove": handle_move, + "style": { + "position": "relative", + "width": "100%", + "height": "180px", + "background": "#f0f0f0", + }, + }, + html.div( + { + "style": { + "position": "absolute", + "background": "black", + "borderRadius": "50%", + "width": "20px", + "height": "20px", + "left": "-10px", + "top": "-10px", + "transform": f"translate({position['x']}px, {position['y']}px)", + } + } + ), + ) diff --git a/docs/examples/updating_objects_in_state/nested_object/app.py b/docs/examples/updating_objects_in_state/nested_object/app.py new file mode 100644 index 000000000..c1bafbf5a --- /dev/null +++ b/docs/examples/updating_objects_in_state/nested_object/app.py @@ -0,0 +1,36 @@ +from reactpy import component, html, use_state + + +# start +@component +def form(): + person, set_person = use_state( + { + "firstName": "Jane", + "lastName": "Jacobs", + "email": "jane@example.com", + "shippingAddress": { + "street": "123 Maple Street", + "city": "Pretendville", + }, + } + ) + + async def handle_city(event): + address = {**person["shippingAddress"], "city": await event.target.value} + set_person({**person, "shippingAddress": address}) + + return html.div( + html.label( + "City: ", + html.input( + { + "value": person["shippingAddress"]["city"], + "onChange": handle_city, + } + ), + ), + html.h1(person["firstName"]), + html.p(person["shippingAddress"]["street"]), + html.p(person["shippingAddress"]["city"]), + ) diff --git a/docs/examples/vdom_mutations/build_fresh.py b/docs/examples/vdom_mutations/build_fresh.py new file mode 100644 index 000000000..5ad8bfa8e --- /dev/null +++ b/docs/examples/vdom_mutations/build_fresh.py @@ -0,0 +1,28 @@ +from reactpy import component, html, use_state + + +# start +# A function that builds a fresh element on every call: the safe shape. +def status_banner(text, tone): + return html.p( + {"id": "status", "className": f"status-{tone}"}, + text, + ) + + +@component +def panel(): + n, set_n = use_state(0) + tone = "ok" if n == 0 else "warn" + + async def handle_n_click(event): + set_n(n + 1) + + return html.div( + html.h1(f"n={n}"), + html.button({"onClick": handle_n_click}, "bump"), + status_banner(f"{n} items", tone), # a new dict every render + ) + + +# end diff --git a/docs/examples/vdom_mutations/shallow_copy.py b/docs/examples/vdom_mutations/shallow_copy.py new file mode 100644 index 000000000..7909d1d33 --- /dev/null +++ b/docs/examples/vdom_mutations/shallow_copy.py @@ -0,0 +1,20 @@ +from reactpy import html + +# start +# The tag constructors copy the top level, so the element's own attributes and +# children containers are separate objects from the ones you passed in. +style = {"color": "red"} +attributes = {"id": "card", "style": style} +children = ["hello"] + +element = html.div(attributes, *children) + +print(element["attributes"] is attributes) # False -- a copy +print(element["children"] is children) # False -- a fresh list + +# But the copy is shallow: nested values are still the objects you handed over. +print(element["attributes"].get("style") is style) # True + +style["color"] = "blue" +print(element["attributes"].get("style")) # {'color': 'blue'} -- changed after the fact +# end diff --git a/docs/examples/vdom_mutations/shared_element_leak.py b/docs/examples/vdom_mutations/shared_element_leak.py new file mode 100644 index 000000000..d34909402 --- /dev/null +++ b/docs/examples/vdom_mutations/shared_element_leak.py @@ -0,0 +1,30 @@ +from reactpy import component, html, use_state + +# start +# Built once, at module scope. Every visitor's Layout renders this same dict. +STATUS = html.p({"id": "status"}, "idle") + + +@component +def panel(): + n, set_n = use_state(0) + + async def mark_dirty(event): + # Mutates the shared element and asks for no render: nothing changes + # on screen, and the server's model now disagrees with every client. + STATUS["children"] = [f"mutated by visitor {n}"] + + async def bump(event): + # The next render of ANY state in this component re-sends STATUS -- + # including the mutation, whoever made it. + set_n(n + 1) + + return html.div( + html.h1({"id": "counter"}, f"n={n}"), + html.button({"id": "dirty", "onClick": mark_dirty}, "mutate"), + html.button({"id": "bump", "onClick": bump}, "bump"), + STATUS, + ) + + +# end diff --git a/docs/examples/you_might_not_need_an_effect/adjust_on_prop_change/app.py b/docs/examples/you_might_not_need_an_effect/adjust_on_prop_change/app.py new file mode 100644 index 000000000..da875f180 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/adjust_on_prop_change/app.py @@ -0,0 +1,48 @@ +"""Adjust selection when the items change — by deriving, not by an Effect.""" + +from reactpy import component, html, use_state + + +# start +@component +def list_box(items): + is_reverse, set_is_reverse = use_state(False) + # One state value that remembers which list the choice belonged to. The + # selection is *derived*: if the items changed, the old choice is not + # valid for the new list, so it reads back as None. No Effect, no + # intermediate render with a stale selection. + chosen, set_chosen = use_state({"items": None, "value": None}) + selection = chosen["value"] if chosen["items"] == items else None + + shown = items[::-1] if is_reverse else items + + async def handle_is_reverse_click(event): + set_is_reverse(not is_reverse) + + async def handle_chosen_click(event): + set_chosen({"items": items, "value": shown[0]}) + + return html.div( + html.p(f"items: {shown} selected: {selection}"), + html.button({"onClick": handle_is_reverse_click}, "Reverse"), + html.button( + {"onClick": handle_chosen_click}, + "Select", + ), + ) + + +@component +def app(): + items, set_items = use_state(["a", "b", "c"]) + + async def handle_items_click(event): + set_items([*items, f"item {len(items) + 1}"]) + + return html.div( + html.button( + {"onClick": handle_items_click}, + "Add item", + ), + list_box(items), + ) diff --git a/docs/examples/you_might_not_need_an_effect/expensive_filter/app.py b/docs/examples/you_might_not_need_an_effect/expensive_filter/app.py new file mode 100644 index 000000000..799fdc135 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/expensive_filter/app.py @@ -0,0 +1,48 @@ +"""The filter runs on every render; use_memo runs it only when inputs change.""" + +from data import filter_runs, todos + +from reactpy import component, html, use_memo, use_state + + +# start +@component +def todo_list(): + filter_name, set_filter_name = use_state("all") + new_todo, set_new_todo = use_state("") + + def visible_todos(): + # A real filter over a real dataset. It records every run so the + # effect of use_memo is countable. + filter_runs.append(filter_name) + if filter_name == "all": + return todos + if filter_name == "active": + return [todo for todo in todos if not todo["completed"]] + return [todo for todo in todos if todo["completed"]] + + shown = use_memo(visible_todos) + + async def handle_filter_name_click(event): + set_filter_name( + {"all": "active", "active": "completed", "completed": "all"}[filter_name] + ) + + async def handle_new_todo_change(event): + set_new_todo(await event.target.value) + + return html.div( + html.p(f"{len(shown)} of {len(todos)} shown (filter: {filter_name})"), + html.ul([html.li({"key": todo["id"]}, todo["text"]) for todo in shown]), + html.button( + {"onClick": handle_filter_name_click}, + "Change filter", + ), + html.input( + { + "placeholder": "new todo", + "value": new_todo, + "onChange": handle_new_todo_change, + } + ), + ) diff --git a/docs/examples/you_might_not_need_an_effect/expensive_filter/data.py b/docs/examples/you_might_not_need_an_effect/expensive_filter/data.py new file mode 100644 index 000000000..3930f7b39 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/expensive_filter/data.py @@ -0,0 +1,24 @@ +"""The dataset the filter example runs over, plus a call counter.""" + +# Every todo the list could show. Large enough that filtering is worth +# measuring, small enough to keep the rendered page readable. +todos = [ + {"id": 1, "text": "Write the effect-free draft", "completed": True}, + {"id": 2, "text": "Port the remaining hooks", "completed": False}, + {"id": 3, "text": "Measure render counts", "completed": False}, + {"id": 4, "text": "Delete the redundant state", "completed": False}, + {"id": 5, "text": "Write clean-ups for every subscription", "completed": False}, + {"id": 6, "text": "Replace the derived Effects", "completed": False}, + {"id": 7, "text": "Move handlers out of Effects", "completed": True}, + {"id": 8, "text": "Review the state shape", "completed": True}, + { + "id": 9, + "text": "Add a regression test for the stale closure", + "completed": False, + }, + {"id": 10, "text": "Ship it", "completed": False}, +] + +# The filter function appends to this on every run, so the docs can state how +# often it actually executes. +filter_runs = [] diff --git a/docs/examples/you_might_not_need_an_effect/external_store/app.py b/docs/examples/you_might_not_need_an_effect/external_store/app.py new file mode 100644 index 000000000..436fd3c61 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/external_store/app.py @@ -0,0 +1,45 @@ +"""The legitimate use of an Effect: mirror a value that lives outside ReactPy.""" + +from data import clock + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def clock_view(): + # The value is owned by `clock`, not by this component, so it cannot be + # derived during render -- ReactPy has to be told when it changes. + ticks, set_ticks = use_state(clock.get()) + + def effect(): + # Subscribe, and hand back the unsubscribe. This is what an Effect + # is for: the source of truth lives outside ReactPy. + def on_change(): + set_ticks(clock.get()) + + return clock.subscribe(on_change) + + use_effect(effect, dependencies=[]) + + return html.div(html.h1(f"{ticks} ticks")) + + +@component +def app(): + watching, set_watching = use_state(True) + + async def handle_click(event): + clock.tick() + + async def handle_watching_click(event): + set_watching(not watching) + + return html.div( + html.button({"onClick": handle_click}, "Tick"), + html.button( + {"onClick": handle_watching_click}, + "Stop watching" if watching else "Watch", + ), + *([] if not watching else [clock_view()]), + ) diff --git a/docs/examples/you_might_not_need_an_effect/external_store/data.py b/docs/examples/you_might_not_need_an_effect/external_store/data.py new file mode 100644 index 000000000..a1974709f --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/external_store/data.py @@ -0,0 +1,32 @@ +"""A plain publish/subscribe object: the external thing an Effect mirrors.""" + + +class ServerClock: + """Stands in for any external store: a value, plus subscribers.""" + + def __init__(self): + self._listeners = [] + self.ticks = 0 + + def get(self): + return self.ticks + + def subscribe(self, listener): + self._listeners.append(listener) + + def unsubscribe(): + self._listeners.remove(listener) + + return unsubscribe + + def tick(self): + """Called by whatever actually drives time: a timer, a socket, a queue.""" + self.ticks += 1 + for listener in list(self._listeners): + listener() + + def listener_count(self): + return len(self._listeners) + + +clock = ServerClock() diff --git a/docs/examples/you_might_not_need_an_effect/form_submit/app.py b/docs/examples/you_might_not_need_an_effect/form_submit/app.py new file mode 100644 index 000000000..ed29c96b6 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/form_submit/app.py @@ -0,0 +1,41 @@ +"""Submit on the click, not from an Effect watching a piece of state.""" + +from data import send_email + +from reactpy import component, html, use_state + + +# start +@component +def signup(): + email, set_email = use_state("") + status, set_status = use_state("idle") + + async def handle_submit(event): + event.preventDefault() + # Wrong: set a "pending" flag and let an Effect notice it. That costs + # a state variable, a render, and a way to submit the same value twice. + # set_pending(True) + # + # Right: do the work from the handler. It runs once per submission. + set_status("sending...") + await send_email(email) + set_status(f"sent to {email}") + + async def handle_email_change(event): + set_email(await event.target.value) + + return html.div( + html.p(status), + html.form( + {"onSubmit": handle_submit}, + html.input( + { + "value": email, + "placeholder": "you@example.com", + "onChange": handle_email_change, + } + ), + html.button({"type": "submit"}, "Sign up"), + ), + ) diff --git a/docs/examples/you_might_not_need_an_effect/form_submit/data.py b/docs/examples/you_might_not_need_an_effect/form_submit/data.py new file mode 100644 index 000000000..ed09637fe --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/form_submit/data.py @@ -0,0 +1,12 @@ +"""Records what the form actually sent, so the page can count submissions.""" + +import asyncio + +sent = [] + + +async def send_email(address): + """Stands in for an HTTP call. Appends to `sent` when it actually runs.""" + await asyncio.sleep(0) + sent.append(address) + return True diff --git a/docs/examples/you_might_not_need_an_effect/full_name_derived/app.py b/docs/examples/you_might_not_need_an_effect/full_name_derived/app.py new file mode 100644 index 000000000..aeaa50020 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/full_name_derived/app.py @@ -0,0 +1,34 @@ +"""Derived during render: one state variable per real input, nothing else.""" + +from reactpy import component, html, use_state + + +# start +@component +def form(): + first_name, set_first_name = use_state("Ada") + last_name, set_last_name = use_state("Lovelace") + # Right: calculated during rendering, so it is always in sync. + full_name = f"{first_name} {last_name}" + + async def handle_first_name_change(event): + set_first_name(await event.target.value) + + async def handle_last_name_change(event): + set_last_name(await event.target.value) + + return html.div( + html.h1(full_name), + html.input( + { + "value": first_name, + "onChange": handle_first_name_change, + } + ), + html.input( + { + "value": last_name, + "onChange": handle_last_name_change, + } + ), + ) diff --git a/docs/examples/you_might_not_need_an_effect/full_name_effect/app.py b/docs/examples/you_might_not_need_an_effect/full_name_effect/app.py new file mode 100644 index 000000000..88ff90214 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/full_name_effect/app.py @@ -0,0 +1,39 @@ +"""Redundant state: `full_name` is stored and re-synced by an Effect.""" + +from reactpy import component, html, use_effect, use_state + + +# start +@component +def form(): + first_name, set_first_name = use_state("Ada") + last_name, set_last_name = use_state("Lovelace") + # Wrong: this value is not state, it is a calculation of other state. + full_name, set_full_name = use_state("") + + def effect(): + set_full_name(f"{first_name} {last_name}") + + use_effect(effect, dependencies=[first_name, last_name]) + + async def handle_first_name_change(event): + set_first_name(await event.target.value) + + async def handle_last_name_change(event): + set_last_name(await event.target.value) + + return html.div( + html.h1(full_name or "(nothing yet)"), + html.input( + { + "value": first_name, + "onChange": handle_first_name_change, + } + ), + html.input( + { + "value": last_name, + "onChange": handle_last_name_change, + } + ), + ) diff --git a/docs/examples/you_might_not_need_an_effect/parent_notify/app.py b/docs/examples/you_might_not_need_an_effect/parent_notify/app.py new file mode 100644 index 000000000..86b8f196e --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/parent_notify/app.py @@ -0,0 +1,39 @@ +"""Pass a plain function down; the child calls it on the user's action.""" + +from reactpy import component, html, use_state + + +# start +@component +def search_box(on_search): + query, set_query = use_state("") + + async def handle_submit(event): + event.preventDefault() + # Not an Effect: the search happens because the user submitted. + on_search(query) + + async def handle_query_change(event): + set_query(await event.target.value) + + return html.form( + {"onSubmit": handle_submit}, + html.input( + { + "value": query, + "placeholder": "search", + "onChange": handle_query_change, + } + ), + html.button({"type": "submit"}, "Search"), + ) + + +@component +def app(): + results, set_results = use_state("Type something and submit.") + + def handle_search(query): + set_results(f"searching for {query!r}..." if query else "empty query") + + return html.div(html.p(results), search_box(handle_search)) diff --git a/docs/examples/you_might_not_need_an_effect/reset_with_key/app.py b/docs/examples/you_might_not_need_an_effect/reset_with_key/app.py new file mode 100644 index 000000000..404cec869 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/reset_with_key/app.py @@ -0,0 +1,43 @@ +"""Reset a form when the profile changes: render a key, not an Effect.""" + +from reactpy import component, html, use_state + + +# start +@component +def profile(user_id): + # Any state declared here is thrown away whenever the key changes, along + # with the state of every component below it. No Effect needed. + comment, set_comment = use_state("") + + async def handle_comment_change(event): + set_comment(await event.target.value) + + return html.div( + html.h2(f"profile {user_id}"), + html.textarea( + { + "value": comment, + "placeholder": "leave a comment", + "onChange": handle_comment_change, + } + ), + html.p(f"draft: {comment or '(empty)'}"), + ) + + +@component +def profile_page(): + user_id, set_user_id = use_state("ada") + + async def handle_user_id_click(event): + set_user_id("grace" if user_id == "ada" else "ada") + + return html.div( + html.button( + {"onClick": handle_user_id_click}, + "Next profile", + ), + # The key is what makes ReactPy treat this as a different profile. + profile(user_id, key=user_id), + ) diff --git a/docs/examples/you_might_not_need_an_effect/shared_handler_logic/app.py b/docs/examples/you_might_not_need_an_effect/shared_handler_logic/app.py new file mode 100644 index 000000000..1b17369c6 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/shared_handler_logic/app.py @@ -0,0 +1,34 @@ +"""Two event handlers share logic; there is no Effect between them.""" + +from reactpy import component, html, use_state + + +# start +@component +def shipping_form(): + shipping_city, set_shipping_city = use_state("") + billing_city, set_billing_city = use_state("") + + async def handle_shipping_change(event): + city = await event.target.value + set_shipping_city(city) + # Shared logic: just call the other setter. Handlers are ordinary + # functions; there is nothing to schedule and nothing to re-sync. + set_billing_city(city) + + async def handle_billing_city_change(event): + set_billing_city(await event.target.value) + + return html.div( + html.p(f"shipping: {shipping_city or '(empty)'}"), + html.p(f"billing: {billing_city or '(empty)'}"), + html.input( + {"value": shipping_city, "onChange": handle_shipping_change}, + ), + html.input( + { + "value": billing_city, + "onChange": handle_billing_city_change, + } + ), + ) diff --git a/docs/examples/you_might_not_need_an_effect/transform_data/app.py b/docs/examples/you_might_not_need_an_effect/transform_data/app.py new file mode 100644 index 000000000..eeac35fe6 --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/transform_data/app.py @@ -0,0 +1,58 @@ +"""Transform the response during render, not in an Effect after it lands.""" + +from data import catalog + +from reactpy import component, html, use_async_effect, use_state + + +# start +@component +def report_view(name): + # Store the response *with the name it belongs to*, so a render for a new + # report cannot display the old one's rows. The empty read is the + # "loading" state -- derived, not a second state variable. + loaded, set_loaded = use_state({"name": None, "data": {}}) + + async def load(): + data = await catalog.fetch(name) + set_loaded({"name": name, "data": data}) + + use_async_effect(load, dependencies=[name]) + + raw = loaded["data"] if loaded["name"] == name else {} + + if not raw: + return html.p("loading...") + + # Right: the shape the page needs is computed here, from the data that + # arrived. There is no second state variable to keep in step, so there is + # nothing for an Effect to synchronise. + rows = [{"label": key.title(), "value": value} for key, value in raw.items()] + total = sum(row["value"] for row in rows) + lines = [ + html.li({"key": row["label"]}, f"{row['label']}: {row['value']}") + for row in rows + ] + + return html.div( + html.h2(f"{name}: {total} total"), + html.ul(lines), + ) + + +@component +def app(): + name, set_name = use_state("orders") + + async def handle_name_click(event): + set_name("users" if name == "orders" else "orders") + + return html.div( + html.button( + {"onClick": handle_name_click}, + "Toggle report", + ), + # No `key` here on purpose: the derived read in report_view is what + # keeps the old report off the screen, so nothing has to be remounted. + report_view(name), + ) diff --git a/docs/examples/you_might_not_need_an_effect/transform_data/data.py b/docs/examples/you_might_not_need_an_effect/transform_data/data.py new file mode 100644 index 000000000..7d9abdbfd --- /dev/null +++ b/docs/examples/you_might_not_need_an_effect/transform_data/data.py @@ -0,0 +1,21 @@ +"""A report source that returns rows, so the shape can be built in render.""" + +import asyncio + +REPORTS = { + "orders": {"pending": 12, "shipped": 40, "returned": 3}, + "users": {"active": 87, "invited": 9, "blocked": 1}, +} + + +class Catalog: + def __init__(self): + self.requests = [] + + async def fetch(self, name): + self.requests.append(name) + await asyncio.sleep(0.05) + return REPORTS[name] + + +catalog = Catalog() diff --git a/docs/examples/your_first_component/browser_output.html b/docs/examples/your_first_component/browser_output.html new file mode 100644 index 000000000..4a912dada --- /dev/null +++ b/docs/examples/your_first_component/browser_output.html @@ -0,0 +1,6 @@ +
+

Amazing scientists

+ Katherine Johnson + Katherine Johnson + Katherine Johnson +
diff --git a/docs/examples/your_first_component/gallery.py b/docs/examples/your_first_component/gallery.py new file mode 100644 index 000000000..af759b6ce --- /dev/null +++ b/docs/examples/your_first_component/gallery.py @@ -0,0 +1,20 @@ +from reactpy import component, html + +# start + + +@component +def profile(): + return html.img( + {"src": "../../assets/images/demos/MK3eW3As.jpg", "alt": "Katherine Johnson"} + ) + + +@component +def gallery(): + return html.section( + html.h1("Amazing scientists"), + profile(), + profile(), + profile(), + ) diff --git a/docs/examples/your_first_component/nested_component_definition.py b/docs/examples/your_first_component/nested_component_definition.py new file mode 100644 index 000000000..c297f0fb9 --- /dev/null +++ b/docs/examples/your_first_component/nested_component_definition.py @@ -0,0 +1,17 @@ +from reactpy import component, html + +# Do not do this: a component defined inside another component. + + +@component +def gallery(): + @component + def profile(): + return html.img({"src": "../../assets/images/demos/MK3eW3As.jpg"}) + + return html.section( + html.h1("Amazing scientists"), + profile(), + profile(), + profile(), + ) diff --git a/docs/examples/your_first_component/profile.py b/docs/examples/your_first_component/profile.py new file mode 100644 index 000000000..c219b46bf --- /dev/null +++ b/docs/examples/your_first_component/profile.py @@ -0,0 +1,10 @@ +from reactpy import component, html + +# start + + +@component +def profile(): + return html.img( + {"src": "../../assets/images/demos/MK3eW3Am.jpg", "alt": "Katherine Johnson"} + ) diff --git a/docs/main.py b/docs/main.py deleted file mode 100644 index e3181f393..000000000 --- a/docs/main.py +++ /dev/null @@ -1,9 +0,0 @@ -import sys - -from docs_app import dev, prod - -if __name__ == "__main__": - if len(sys.argv) == 1: - prod.main() - else: - dev.main() diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 000000000..2d1c9d8db --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,205 @@ +--- +nav: + - Home: index.md + - Get Started: + - Overview: + - Quick Start: learn/quick-start.md + - "Tutorial: Tic-Tac-Toe": learn/tutorial-tic-tac-toe.md + - Thinking in React: learn/thinking-in-react.md + - Installation: + - Creating a Standalone React App: learn/creating-a-react-app.md + - Add React to an Existing Project: learn/add-react-to-an-existing-project.md + - Setup: + - Editor Setup: learn/editor-setup.md + - Tools, Libraries, and Packages: learn/tools-and-packages.md + # - More Tutorials: + # - "Tutorial: React Bootstrap 🚫": learn/tutorial-react-bootstrap.md + # - "Tutorial: Material UI 🚫": learn/tutorial-material-ui.md + - Learn: + - Describing the UI: + - Your First Component: learn/your-first-component.md + - Importing and Exporting Components: learn/importing-and-exporting-components.md + # - Writing Markup with PSX 🚫: learn/writing-markup-with-psx.md + # - Python in PSX with Curly Braces 🚫: learn/python-in-psx-with-curly-braces.md + - Passing Props to a Component: learn/passing-props-to-a-component.md + - Conditional Rendering: learn/conditional-rendering.md + - Rendering Lists: learn/rendering-lists.md + - Keeping Components Pure: learn/keeping-components-pure.md + - Adding Interactivity: + - Responding to Events: learn/responding-to-events.md + - "State: A Component's Memory": learn/state-a-components-memory.md + - Render and Commit: learn/render-and-commit.md + - State as a Snapshot: learn/state-as-a-snapshot.md + - Queueing a Series of State Updates: learn/queueing-a-series-of-state-updates.md + - Updating Objects in State: learn/updating-objects-in-state.md + - Updating Arrays in State: learn/updating-arrays-in-state.md + - Managing State: + - Reacting to Input with State: learn/reacting-to-input-with-state.md + - Choosing the State Structure: learn/choosing-the-state-structure.md + - Sharing State Between Components: learn/sharing-state-between-components.md + - Preserving and Resetting State: learn/preserving-and-resetting-state.md + - Extracting State Logic into a Reducer: learn/extracting-state-logic-into-a-reducer.md + - Passing Data Deeply with Context: learn/passing-data-deeply-with-context.md + - Scaling Up with Reducer and Context: learn/scaling-up-with-reducer-and-context.md + - Escape Hatches: + - Referencing Values with Refs: learn/referencing-values-with-refs.md + # - Manipulating the DOM with Refs 🚫: learn/manipulating-the-dom-with-refs.md + - Synchronizing with Effects: learn/synchronizing-with-effects.md + - You Might Not Need an Effect: learn/you-might-not-need-an-effect.md + - Lifecycle of Reactive Effects: learn/lifecycle-of-reactive-effects.md + - Separating Events from Effects: learn/separating-events-from-effects.md + - Removing Effect Dependencies: learn/removing-effect-dependencies.md + - Reusing Logic with Custom Hooks: learn/reusing-logic-with-custom-hooks.md + - Communicating Data Between Server and Client: learn/communicate-data-between-server-and-client.md + - Convert Between VDOM and HTML: learn/convert-between-vdom-and-html.md + - VDOM Mutations: learn/vdom-mutations.md + - Creating VDOM Event Handlers: learn/creating-vdom-event-handlers.md + - Creating HTML Tags: learn/creating-html-tags.md + - Creating Backends: learn/creating-backends.md + - Manually Register a Client: learn/manually-register-a-client.md + - Reference: + - Overview: reference/overview.md + - Hooks: + - Use State: reference/use-state.md + - Use Effect: reference/use-effect.md + - Use Async Effect: reference/use-async-effect.md + - Use Context: reference/use-context.md + - Use Reducer: reference/use-reducer.md + - Use Callback: reference/use-callback.md + - Use Memo: reference/use-memo.md + - Use Ref: reference/use-ref.md + - Use Debug Value: reference/use-debug-value.md + - Use Connection: reference/use-connection.md + - Use Scope: reference/use-scope.md + - Use Location: reference/use-location.md + - HTML Tags: + - Common Props: reference/common-props.md + - Usage: reference/usage.md + - Executors: + - ReactPy: reference/reactpy.md + - ReactPyCsr: reference/reactpy-csr.md + - ReactPyMiddleware: reference/reactpy-middleware.md + - Rules of React: + - Overview: reference/rules-of-react.md + - Components and Hooks must be pure: reference/components-and-hooks-must-be-pure.md + - React calls Components and Hooks: reference/react-calls-components-and-hooks.md + - Rules of Hooks: reference/rules-of-hooks.md + - Template Tags: + - Jinja: reference/jinja.md + - Protocol Structure: reference/protocol-structure.md + - Client API: reference/client-api.md + - About: + - Changelog: about/changelog.md + - Community: about/community.md + - Running Tests: about/running-tests.md + - Contributing Code: about/code.md + - Contributing Documentation: about/docs.md + - Licenses: about/licenses.md + +theme: + name: material + custom_dir: overrides + palette: + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/white-balance-sunny + name: Switch to light mode + primary: red # We use red to indicate that something is unthemed + accent: red + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-night + name: Switch to dark mode + primary: white + accent: red + features: + # - navigation.instant + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - content.code.copy + - search.highlight + icon: + repo: fontawesome/brands/github + admonition: + note: fontawesome/solid/note-sticky + logo: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg + favicon: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg + +markdown_extensions: + - toc: + permalink: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + linenums: true + - pymdownx.superfences + - pymdownx.details + - pymdownx.inlinehilite + - admonition + - attr_list + - md_in_html + - pymdownx.keys + +hooks: + - plugins/reactpy_run.py + +plugins: + - search + - include-markdown + - git-authors + - minify: + minify_html: true + minify_js: true + minify_css: true + cache_safe: true + - git-revision-date-localized: + fallback_to_build_date: true + # - spellcheck: + # known_words: dictionary.txt + # allow_unicode: no + +extra: + generator: false + version: + provider: mike + # analytics: + # provider: google + # property: G-xxx + +extra_javascript: + - assets/js/main.js + +extra_css: + - assets/css/main.css + - assets/css/button.css + - assets/css/admonition.css + - assets/css/banner.css + - assets/css/sidebar.css + - assets/css/navbar.css + - assets/css/table-of-contents.css + - assets/css/code.css + - assets/css/footer.css + - assets/css/home.css + +watch: + - "../docs" + - ../README.md + - ../CHANGELOG.md + - ../LICENSE.md + - "../src" + +site_name: ReactPy +site_author: Archmonger +site_description: It's React, but in Python. +copyright: '©
Reactive Python and affiliates.' +repo_url: https://github.com/reactive-python/reactpy +site_url: https://reactive-python.github.io/reactpy +repo_name: ReactPy +edit_uri: edit/main/docs/src +docs_dir: src diff --git a/docs/overrides/home.html b/docs/overrides/home.html new file mode 100644 index 000000000..4673061f8 --- /dev/null +++ b/docs/overrides/home.html @@ -0,0 +1,135 @@ + +{% extends "main.html" %} + + +{% block content %}{% endblock %} + + +{% block tabs %} + +
+
+ +

{{ config.site_name }}

+

{{ config.site_description }}

+ +
+ +
+

Create user interfaces from components

+

+ ReactPy lets you build user interfaces out of individual pieces called components. Create your own ReactPy + components like thumbnail, like_button, and video. Then combine + them into entire screens, pages, and apps. +

+
+ {% with image="create-user-interfaces.png", class="pop-left" %} + {% include "homepage_examples/code_block.html" %} + {% endwith %} + {% include "homepage_examples/create_user_interfaces_demo.html" %} +
+

+ Whether you work on your own or with thousands of other developers, using React feels the same. It is + designed to let you seamlessly combine components written by independent people, teams, and + organizations. +

+
+ +
+

Write components with pure Python code

+

+ ReactPy components are Python functions. Want to show some content conditionally? Use an + if statement. Displaying a list? Try using + list comprehension. + Learning ReactPy is learning programming. +

+
+ {% with image="write-components-with-python.png", class="pop-left" %} + {% include "homepage_examples/code_block.html" %} + {% endwith %} + {% include "homepage_examples/write_components_with_python_demo.html" %} + +
+
+ +
+

Add interactivity wherever you need it

+

+ ReactPy components receive data and return what should appear on the screen. You can pass them new data in + response to an interaction, like when the user types into an input. ReactPy will then update the screen to + match the new data. +

+
+ {% with image="add-interactivity.png" %} + {% include "homepage_examples/code_block.html" %} + {% endwith %} + {% include "homepage_examples/add_interactivity_demo.html" %} +
+

+ You don't have to build your whole page in ReactPy. Add React to your existing HTML page, and render + interactive ReactPy components anywhere on it. +

+
+ +
+

Go full-stack with a framework

+

+ ReactPy is a library. It lets you put components together, but it doesn't prescribe how to do routing and + data fetching. To build an entire app with ReactPy, you can leverage a backend framework like + Django or Starlette. +

+ + Get Started + +
+
+{% endblock %} diff --git a/docs/overrides/homepage_examples/add_interactivity.py b/docs/overrides/homepage_examples/add_interactivity.py new file mode 100644 index 000000000..14335668d --- /dev/null +++ b/docs/overrides/homepage_examples/add_interactivity.py @@ -0,0 +1,31 @@ +from reactpy import component, html, use_state + + +def filter_videos(*_, **__): + return [] + + +def search_input(*_, **__): ... + + +def video_list(*_, **__): ... + + +@component +def searchable_video_list(videos): + search_text, set_search_text = use_state("") + found_videos = filter_videos(videos, search_text) + + async def handle_search_change(event): + set_search_text(await event.target.value) + + return html.fragment( + search_input( + {"onChange": handle_search_change}, + value=search_text, + ), + video_list( + videos=found_videos, + empty_heading=f"No matches for “{search_text}”", + ), + ) diff --git a/docs/overrides/homepage_examples/add_interactivity_demo.html b/docs/overrides/homepage_examples/add_interactivity_demo.html new file mode 100644 index 000000000..ec8c1e4db --- /dev/null +++ b/docs/overrides/homepage_examples/add_interactivity_demo.html @@ -0,0 +1,172 @@ +
+
+ +
+
+ + + + example.com/videos.html +
+
+ +
+
+

Searchable Videos

+

Type a search query below.

+ +
+ +

5 Videos

+ +
+
+ + + +
+
+

ReactPy: The Documentary

+

From web library to taco delivery service

+
+ +
+ +
+
+ + + +
+
+

Code using Worst Practices

+

Harriet Potter (2013)

+
+ +
+ +
+
+ + + +
+
+

Introducing ReactPy Foriegn

+

Tim Cooker (2015)

+
+ +
+ +
+
+ + + +
+
+

Introducing ReactPy Cooks

+

Soap Boat and Dinosaur Dan (2018)

+
+ +
+ +
+
+ + + +
+
+

Introducing Quantum Components

+

Isaac Asimov and Lauren-kun (2020)

+
+ +
+

+
+ + +
+
diff --git a/docs/overrides/homepage_examples/code_block.html b/docs/overrides/homepage_examples/code_block.html new file mode 100644 index 000000000..27b362046 --- /dev/null +++ b/docs/overrides/homepage_examples/code_block.html @@ -0,0 +1,7 @@ +
+ +
+
+ +
+
diff --git a/docs/overrides/homepage_examples/create_user_interfaces.py b/docs/overrides/homepage_examples/create_user_interfaces.py new file mode 100644 index 000000000..873b9d884 --- /dev/null +++ b/docs/overrides/homepage_examples/create_user_interfaces.py @@ -0,0 +1,20 @@ +from reactpy import component, html + + +def thumbnail(*_, **__): ... + + +def like_button(*_, **__): ... + + +@component +def video(data): + return html.div( + thumbnail(data), + html.a( + {"href": data.url}, + html.h3(data.title), + html.p(data.description), + ), + like_button(data), + ) diff --git a/docs/overrides/homepage_examples/create_user_interfaces_demo.html b/docs/overrides/homepage_examples/create_user_interfaces_demo.html new file mode 100644 index 000000000..9ec25437e --- /dev/null +++ b/docs/overrides/homepage_examples/create_user_interfaces_demo.html @@ -0,0 +1,24 @@ +
+
+
+
+ + + +
+
+

My video

+

Video description

+
+ +
+
+
diff --git a/docs/overrides/homepage_examples/write_components_with_python.py b/docs/overrides/homepage_examples/write_components_with_python.py new file mode 100644 index 000000000..47e28b68c --- /dev/null +++ b/docs/overrides/homepage_examples/write_components_with_python.py @@ -0,0 +1,18 @@ +from reactpy import component, html + + +def video(*_, **__): ... + + +@component +def video_list(videos, empty_heading): + count = len(videos) + heading = empty_heading + if count > 0: + noun = "Videos" if count > 1 else "Video" + heading = f"{count} {noun}" + + return html.section( + html.h2(heading), + [video(x, key=x.id) for x in videos], + ) diff --git a/docs/overrides/homepage_examples/write_components_with_python_demo.html b/docs/overrides/homepage_examples/write_components_with_python_demo.html new file mode 100644 index 000000000..779f7abbe --- /dev/null +++ b/docs/overrides/homepage_examples/write_components_with_python_demo.html @@ -0,0 +1,65 @@ +
+
+

3 Videos

+
+
+ + + +
+
+

First video

+

Video description

+
+ +
+
+
+ + + +
+
+

Second video

+

Video description

+
+ +
+
+
+ + + +
+
+

Third video

+

Video description

+
+ +
+
+
diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 000000000..c63ca9e71 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +{{ super() }} + +{% if git_page_authors %} +
+ + Authors: {{ git_page_authors | default('enable mkdocs-git-authors-plugin') }} + +
+{% endif %} +{% endblock %} + +{% block outdated %} +You're not viewing the latest release. + + Click here to go to latest. + +{% endblock %} diff --git a/docs/plugins/reactpy_run.py b/docs/plugins/reactpy_run.py new file mode 100644 index 000000000..4622e5b89 --- /dev/null +++ b/docs/plugins/reactpy_run.py @@ -0,0 +1,837 @@ +"""MkDocs hook: make the ``:material-play: Run`` tab placeholders execute for real. + +Loaded by MkDocs' builtin ``hooks:`` config, so there is no entry point to +install, no ``PYTHONPATH`` to set, and no new dependency: + + # docs/mkdocs.yml + hooks: + - plugins/reactpy_run.py + +Which files a Run tab executes is **inferred from the sibling tabs** of the +same tab set, because their ``{% include "../../examples//…" %}`` +directives already name them. A Run tab therefore cannot drift from the code +it runs, and authoring a new one costs nothing. + +Authoring (unchanged from today):: + + === "app.py" + + ```python + {% include "../../examples/rendering_lists/from_array/app.py" start="# start" %} + ``` + + === ":material-play: Run" + + ```python + # TODO + ``` + +Rare cases get an explicit fence inside the Run tab instead:: + + === ":material-play: Run" + + ```reactpy-run + dir: reference_reactpy_csr + files: csr/main.py csr_root/root.py + root: my_app + ``` + +Rendering reuses ReactPyCsr's own primitives: ``pyscript_setup_html`` / +``pyscript_executor_html``. One Pyodide interpreter is shared per page and is +**not loaded until the first Run click**, so a docs page with 8 demos costs +exactly what a docs page with 0 demos costs. +""" + +from __future__ import annotations + +import hashlib +import json +import posixpath +import re +import shutil +import textwrap +from collections.abc import Iterator +from pathlib import Path +from typing import Any, TypedDict + +from mkdocs.exceptions import BuildError +from mkdocs.plugins import event_priority + +# ------------------------------------------------------------------ tuning -- + +TAB_TITLE = ":material-play: Run" +EXAMPLES_DIR_NAME = "examples" +RUNTIME_DIR = "assets/reactpy-runtime" +STATIC_IGNORES = ("*.map", "index*.js", "preact*") + +# ReactPy hands PyScript root-absolute asset URLs (``/reactpy/static/…``), +# which only work when ReactPy itself serves the page. The generated config +# replaces that prefix with this marker and the client expands it. +STATIC_MARKER = "__REACTPY_STATIC__" +_ROOT_NAME_PREFERENCE = ("my_app", "app", "main", "root") +_MODULE_GLOBALS_RE = re.compile( + r"^(?P[A-Za-z_][A-Za-z0-9_]*) *(?:\+|-|\*|/|%|\*\*)?=(?!=)", re.M +) + +# Run *before* mkdocs-include-markdown-plugin (priority 100) so that the +# ``{% include %}`` directives we infer from are still in the markdown. +_INFERENCE_PRIORITY = 200 + +_INCLUDE_RE = re.compile(r'\{%\s*include\s+"(?P[^"]+?)"[^%]*%\}') +_IMPORT_RE = re.compile(r"^(?:from|import)\s+([A-Za-z_][\w.]*)", re.MULTILINE) +_TAB_RE = re.compile(r'^[ \t]*=== "(?P[^"]*)"[ \t]*$', re.MULTILINE) +_FENCE_RE = re.compile(r"^[ \t]*(?:```|~~~)", re.MULTILINE) +_COMPONENT_RE = re.compile(r"@component\s*\n(?:@[^\n]*\n)*[ \t]*def (?P<name>\w+)\(") +_ROOT_MARKER_RE = re.compile(r"^[ \t]*#[ \t]*root:[ \t]*(?P<name>\w+)", re.MULTILINE) +_OVERRIDES_RE = re.compile( + r"^([ \t]*)```reactpy-run[ \t]*\n(.*?)\1```[ \t]*$", re.MULTILINE | re.DOTALL +) + + +# ------------------------------------------------------------------- state -- + + +class _Resolved(TypedDict): + """The files a Run tab executes, and the directory label to show.""" + + main: Path + extra: list[Path] + css: list[Path] + parts: list[str] + + +class _State: + def __init__(self) -> None: + self.blocks: dict[str, list[dict[str, Any]]] = {} + self.examples_dir: Path = Path("docs/examples") + self.site_dir: Path | None = None + + +_state = _State() + + +# ----------------------------------------------------------------- events -- + + +def on_config(config): + docs_dir = Path(config["docs_dir"]) + _state.examples_dir = (docs_dir.parent / EXAMPLES_DIR_NAME).resolve() + return config + + +def on_post_build(config): + """Copy ReactPy's vendored PyScript runtime into the site, once per build.""" + import reactpy + + site_dir = Path(config["site_dir"]) + static_src = Path(reactpy.__file__).parent / "static" + static_dst = site_dir / RUNTIME_DIR / "static" + if static_dst.exists(): + shutil.rmtree(static_dst) + shutil.copytree( + static_src, static_dst, ignore=shutil.ignore_patterns(*STATIC_IGNORES) + ) + _write_runtime_config(site_dir, config) + + +@event_priority(_INFERENCE_PRIORITY) +def on_page_markdown(markdown, page, config, files): + blocks: list[dict[str, Any]] = [] + edits: list[tuple[int, int, str]] = [] + + for index, (tab_set, tab) in enumerate(_run_tabs(markdown)): + block = _build_block(markdown, tab, tab_set, page, index) + blocks.append(block) + indent = " " * (len(tab["indent"]) + 4) + body = f"{indent}<!--REACTPY-RUN:{block['id']}-->\n\n" + edits.append((tab["body_start"], tab["body_end"], body)) + + _state.blocks[page.file.src_path] = blocks + # Apply from the back so earlier offsets stay valid. + for start, end, body in sorted(edits, key=lambda e: e[0], reverse=True): + markdown = markdown[:start] + body + markdown[end:] + return markdown + + +def on_page_content(html, page, config, files): + _check_asset_paths(page, html) + + blocks = _state.blocks.get(page.file.src_path) or [] + if not blocks: + return html + + base = _relative_to_page(page) + templates: list[str] = [] + for block in blocks: + marker = f"<!--REACTPY-RUN:{block['id']}-->" + if block.get("error"): + html = html.replace( + marker, + '<div class="reactpy-run reactpy-run--error">' + f'<span class="reactpy-run-msg">{block["label"]}: {block["error"]}</span></div>', + ) + continue + html = html.replace( + marker, + # No title, no button: opening the Run tab is itself the signal to + # execute, and the panel is styled to read as one of the page's + # code blocks. + f'<div class="reactpy-run" data-reactpy-run="{block["id"]}">' + # The id/class/``data-uuid`` trio is ReactPyCsr's contract: the + # layout handler renders into ``#pyscript-<uuid>`` and reclaims + # workspaces by reading ``.pyscript`` elements' ``data-uuid``. + f'<div class="reactpy-run-out pyscript" id="pyscript-{block["uuid"]}" ' + f'data-uuid="{block["uuid"]}"></div>' + '<div class="reactpy-run-status" hidden></div></div>', + ) + templates.append( + f'<template id="reactpy-src-{block["id"]}">' + f"{block['css_html']}" + f'<script type="py">{block["executor"]}</script></template>' + ) + + return ( + html + + f'<link rel="stylesheet" href="{base}assets/css/reactpy-run.css" />' + + f'<script type="module" src="{base}assets/js/reactpy-run.js"></script>' + + "".join(templates) + ) + + +_RELATIVE_ASSET_RE = re.compile(r"\.\./\.\./assets/[A-Za-z0-9_./-]*") + + +def _check_asset_paths(page, html: str) -> None: + """Fail the build when a page-relative asset path is off by a level. + + Examples write ``../../assets/images/scientists/`` because every page that + shows a demo sits two levels below the site root. Move one to a deeper or + shallower page and the same literal points somewhere else — the image + breaks on that page alone, which no check that only asks "did the demo + render" will ever notice. Resolving it here turns that silent 404 into a + build failure that names the page. + """ + directory = posixpath.dirname(page.file.dest_uri) + if outside := sorted( + { + match.group(0) + for match in _RELATIVE_ASSET_RE.finditer(html) + if not posixpath.normpath( + posixpath.join(directory, match.group(0)) + ).startswith("assets/") + } + ): + depth = len(directory.split("/")) if directory else 0 + raise BuildError( + f"'{page.file.src_path}': '{outside[0]}' resolves outside the site from " + f"{depth} level(s) deep. These paths assume two — add or drop a '../' to " + "match this page's depth." + ) + + +def _relative_to_page(page) -> str: + """MkDocs rewrites links in markdown, but not the raw HTML we inject. + + ``page.file.dest_uri`` is the page's path under ``site/``, so its depth is + exactly how many ``../`` the injected URLs need. This keeps the runtime + working under mike's versioned URLs (``/reactpy/dev/learn/…``) and with + ``use_directory_urls: false``. + """ + from posixpath import dirname + + directory = dirname(page.file.dest_uri) + return "" if directory in ("", ".") else "../" * (directory.count("/") + 1) + + +# -------------------------------------------------------------- generation -- + + +def _run_tabs(markdown: str) -> Iterator[tuple[dict, dict]]: + """Every ``:material-play: Run`` tab, with the tab set that contains it.""" + for tab_set in _tab_sets(markdown): + for tab in tab_set["tabs"]: + if tab["title"].strip() != TAB_TITLE: + continue + if _inside_code_fence(markdown, tab["start"]): + # A page documenting the Run convention is not a demo. + continue + yield tab_set, tab + + +def _inside_code_fence(markdown: str, pos: int) -> bool: + """Whether *pos* sits inside a fenced code block.""" + return len(_FENCE_RE.findall(markdown[:pos])) % 2 == 1 + + +def _build_block( + markdown: str, + tab: dict[str, Any], + tab_set: dict[str, Any], + page: Any, + index: int, +) -> dict[str, Any]: + from uuid import uuid4 + + from reactpy.executors.pyscript.utils import pyscript_executor_html + + body = markdown[tab["body_start"] : tab["body_end"]] + overrides = _parse_overrides(body) + resolved, error = _resolve_files(markdown, tab, tab_set, page, overrides) + label = overrides.get("dir") or ("/".join(resolved["parts"]) if resolved else "?") + # Content alone is not unique: two demos on one page can share a + # directory, and they must not collide with each other's DOM ids. + block_id = _make_id(page.file.src_path, str(index), label or "") + + if resolved is None or error: + return { + "id": block_id, + "label": label, + "error": error or "Nothing runnable was found in the sibling tabs", + } + + main_file = resolved["main"] + extra_files = resolved["extra"] + merged = _merged_source([main_file, *extra_files], block_id) + cache = {str(main_file): merged} + uuid = uuid4().hex + + root = overrides.get("root") or _guess_root(main_file) + if root is None: + # Script-style examples (they ``print()`` VDOM rather than mount a + # component) still get a Run tab: their stdout becomes a <pre>. + root, cache[str(main_file)] = "_console_output", _as_console_script(merged) + else: + if f"def {root}():" not in merged: + # The example's outermost component takes props, and ReactPy's + # executor only mounts zero-argument ones. Mount it from a wrapper, + # passing whatever the page declared (``props: symbol="BTC"``). + args = overrides.get("props", "") + if not args and _requires_props(merged, root): + return { + "id": block_id, + "label": label, + "error": f"{root}() needs props; add a props: line to the fence", + } + merged += _mount_wrapper(root, args) + root = "_reactpy_run_root" + # Examples that mount a component have no <pre> to catch their stdout, + # so give ``print()`` somewhere to say itself. + cache[str(main_file)] = ( + f"{_print_shim(block_id)}\n{merged}" if "print(" in merged else merged + ) + + if syntax_error := _nested_syntax_error(cache[str(main_file)]): + return {"id": block_id, "label": label, "error": syntax_error} + + try: + executor = pyscript_executor_html( + file_paths=[str(main_file)], + uuid=uuid, + root=root, + cache_handler=lambda path, **_: cache[path], + ) + except Exception as exc: + return {"id": block_id, "label": label, "error": str(exc)} + + css_html = "" + for css_file in resolved["css"]: + scoped = _scope_css(css_file.read_text(encoding="utf-8"), f"#pyscript-{uuid}") + css_html += f"<style>{scoped}</style>" + + return { + "id": block_id, + "label": label, + "uuid": uuid, + "executor": executor, + "css_html": css_html, + } + + +def _parse_overrides(body: str) -> dict[str, str]: + """Read the ``key: value`` lines of a ``reactpy-run`` fence, if present. + + Recognised keys are ``dir:`` (which example directory), ``files:`` (which + files within it), ``root:`` (which component to mount), and ``props:`` + (arguments for a component whose parameters have no defaults). + """ + match = _OVERRIDES_RE.search(body) + if not match: + return {} + values: dict[str, str] = {} + for line in match.group(2).splitlines(): + if ":" in line: + key, _, value = line.partition(":") + values[key.strip()] = value.strip() + return values + + +def _resolve_files( + markdown: str, + tab: dict[str, Any], + tab_set: dict[str, Any], + page: Any, + overrides: dict[str, str], +) -> tuple[_Resolved | None, str | None]: + """Work out which example files a Run tab should execute. + + Either an author tells us (``dir:``/``files:`` in a ``reactpy-run`` fence) + or we read it off the sibling tabs' ``{% include %}`` directives, which + already name these exact files. + """ + examples = _state.examples_dir + page_dir = Path(page.file.abs_src_path).parent + + if overrides.get("dir"): + demo_dir = examples / overrides["dir"] + if not demo_dir.is_dir(): + return None, f"No such example directory: {overrides['dir']}" + if overrides.get("files"): + found = [demo_dir / name.strip() for name in overrides["files"].split(",")] + else: + found = sorted( + p for p in demo_dir.rglob("*.py") if "__pycache__" not in p.parts + ) + found += sorted(demo_dir.glob("*.css")) + label = [overrides["dir"]] + elif overrides.get("files"): + found, label = [], None + for name in (n.strip() for n in overrides["files"].split(",")): + matches = sorted( + p + for p in examples.rglob(name) + if p.is_file() and "__pycache__" not in p.parts + ) + if len(matches) > 1: + names = ", ".join(str(m.relative_to(examples)) for m in matches) + return None, f"'{name}' is ambiguous ({names}); add a dir: line" + if not matches: + return None, f"No example file named '{name}'" + found.append(matches[0]) + label = str(_demo_dir_of(matches[0], examples).relative_to(examples)) + if label is None: + return None, "files: listed nothing" + label = [label] + else: + found = [] + for sibling in tab_set["tabs"]: + if sibling is tab: + continue + for match in _INCLUDE_RE.finditer( + markdown[sibling["body_start"] : sibling["body_end"]] + ): + candidate = (page_dir / match.group("path")).resolve() + if candidate.is_file() and _is_under(candidate, examples): + found.append(candidate) + label = None + + if not found: + return None, "Nothing runnable was found in the sibling tabs" + if missing := [str(p.relative_to(examples)) for p in found if not p.is_file()]: + return None, f"No such example file(s): {', '.join(missing)}" + + # A shared file that lives *above* the demo (a page-level ``game.css``) is + # part of that demo, not a second directory competing to be its root. + groups: dict[Path, list[Path]] = {} + for path in found: + groups.setdefault(_demo_dir_of(path, examples), []).append(path) + primary = max( + groups, + key=lambda d: ( + sum(p.suffix == ".py" for p in groups[d]), + len(d.relative_to(examples).parts), + ), + ) + files = list(groups[primary]) + for directory, members in groups.items(): + if directory == primary: + continue + if directory in primary.parents: + files.extend(members) + elif label is None: + names = ", ".join(sorted(str(g.relative_to(examples)) for g in groups)) + return ( + None, + f"Multiple example directories ({names}); add a reactpy-run fence", + ) + + # A demo may import a sibling the page never shows (``data.py``). Add what + # the code actually imports, transitively — not every file in the + # directory, which would drag in the alternative versions of it. + if label is None: + files = _with_imported_siblings(files, primary) + + py = [p for p in _order(files) if p.suffix == ".py"] + css = [p for p in sorted(files) if p.suffix == ".css"] + if not py: + return None, f"No Python files for {primary.relative_to(examples)}" + return { + "main": py[0], + "extra": py[1:], + "css": css, + "parts": label or list(primary.relative_to(examples).parts), + }, None + + +def _demo_dir_of(path: Path, examples: Path) -> Path: + """``examples/<page_slug>/<demo>/app.py`` -> ``examples/<page_slug>/<demo>``.""" + rel = path.relative_to(examples) + return examples / rel.parent + + +def _with_imported_siblings(files: list[Path], directory: Path) -> list[Path]: + """Add sibling modules that *files* import, directly or transitively. + + A demo's helper module (``data.py``, ``tasks.py``) is part of the demo even + when the page only shows ``app.py``. The reverse would be wrong: a + directory often holds *alternative* versions of a demo, and merging those + in produces a module that imports a web server. + """ + included = list(files) + queue = list(files) + while queue: + source = queue.pop().read_text(encoding="utf-8") + for name in {m.split(".")[0] for m in _IMPORT_RE.findall(source)}: + for module in sorted(directory.glob(f"{name}.py")): + if module not in included: + included.append(module) + queue.append(module) + return included + + +def _order(files: list[Path]) -> list[Path]: + def rank(path: Path) -> tuple[int, str]: + # app.py is the component; main.py is an ASGI server (never the root). + return ( + 0 if path.name == "app.py" else 1 if path.name != "main.py" else 2, + path.name, + ) + + return sorted(files, key=rank) + + +def _merged_source(files: list[Path], block_id: str = "") -> str: + """Concatenate a demo's files, shimming cross-file imports. + + PyScript gets one flat script, so ``from data import people`` would raise + ``ModuleNotFoundError``. Registering the siblings in ``sys.modules`` from + embedded strings keeps the on-disk files importable as normal modules. + """ + # Everything below lands inside one function, so a module-level + # assignment becomes a local and any ``global`` in the example then reads + # a name that was never bound. Declare those names global up front. + declared = _MODULE_GLOBALS_RE.findall(files[0].read_text(encoding="utf-8")) + head: list[str] = ( + [f"global {', '.join(sorted(set(declared)))}", ""] if declared else [] + ) + ["import sys, types", ""] + for path in files[1:]: + name, source = path.stem, path.read_text(encoding="utf-8") + if block_id and "print(" in source: + # A sibling that prints needs the same console as the component. + source = _print_shim(block_id) + source + head += [ + f"_{name}_src = {json.dumps(source)}", + f"_{name}_mod = types.ModuleType({json.dumps(name)})", + f"exec(compile(_{name}_src, {json.dumps(name + '.py')}, 'exec'), _{name}_mod.__dict__)", + f"sys.modules[{json.dumps(name)}] = _{name}_mod", + f"del _{name}_src, _{name}_mod", + "", + ] + return "\n".join(head) + files[0].read_text(encoding="utf-8") + + +def _guess_root(main_file: Path) -> str | None: + """The component nothing else renders — that is what the page mounts. + + Declaring the outermost component last is the common convention, but not a + rule, so prefer the component no other one calls, and fall back to the + last declaration when that is ambiguous. + """ + text = main_file.read_text(encoding="utf-8") + if marked := _ROOT_MARKER_RE.search(text): + return marked.group("name") + + names = _COMPONENT_RE.findall(text) + if not names: + return None + + roots = [ + name + for name in names + if not re.search(rf"\b{name}\(", re.sub(rf"def {name}\(", "(", text)) + ] + for preferred in _ROOT_NAME_PREFERENCE: + if preferred in roots: + return preferred + return roots[-1] if roots else names[-1] + + +def _requires_props(source: str, root: str) -> bool: + """Whether ``root`` has parameters without defaults, so it needs props.""" + match = re.search(rf"def {re.escape(root)}\(([^)]*)\)", source) + if not match: + return False + params = [p.strip() for p in match.group(1).split(",") if p.strip()] + return any("=" not in p for p in params) + + +def _nested_syntax_error(script: str) -> str | None: + """Compile the script the way PyScript runs it — inside a function. + + Top-level Python accepts what a nested scope refuses: a name both annotated + and declared ``global`` is one such case. In the browser a syntax error is + a button that does nothing, so catch it where it can still be reported. + """ + nested = "def _reactpy_run_probe():\n" + textwrap.indent(script, " ") + try: + compile(nested, "<reactpy-run>", "exec") + except SyntaxError as error: + return f"{error.msg} (line {error.lineno})" + return None + + +def _mount_wrapper(root: str, args: str = "") -> str: + """Mount an example's component from a zero-argument root. + + ``pyscript_executor_html`` refuses to mount anything but ``def name():``, + and plenty of teaching examples are components that take props. + """ + return ( + "\n\nimport reactpy as _reactpy_run_rt\n\n\n" + "@_reactpy_run_rt.component\n" + "def _reactpy_run_root():\n" + f" return {root}({args})\n" + ) + + +def _print_shim(block_id: str) -> str: + """Route an example's ``print()`` into a console region of its own. + + Shadowing the builtin inside the example's module — rather than swapping + out ``sys.stdout`` — is what makes this work at all: + + * One interpreter runs every demo on the page, so a global redirect would + mix their output together with no way to sort it back out. + * The prints that teach the most are inside event handlers and effects. + They fire long after the example finished executing, which is exactly + when a ``redirect_stdout`` block has already been torn down. + * Writes to ``sys.stdout`` have no demo to belong to. A module-level + ``print`` always resolves to the module that called it, so output from + this example can only ever land under this example's panel. + + The example's source lands inside a function (ReactPy's executor wraps it + in a ``user_workspace`` callable), so this assignment makes ``print`` a + closure cell over the example's own code — which is why the state below + lives in a mutable container instead of being rebound with ``global``. + Every file of the demo gets its own copy, and they find each other through + an attribute on ``builtins`` keyed by the panel. + """ + shim = f''' +import builtins as _reactpy_builtins +import sys as _reactpy_sys + +try: # absent outside the browser, e.g. when tests import an example + from js import document as _reactpy_document +except ImportError as _reactpy_exc: # pragma: no cover + _reactpy_document = None + _reactpy_sys.stderr.write(f"reactpy-run: no console region ({{_reactpy_exc}})\\n") + +_reactpy_console_key = "reactpy_console_{block_id}" +# Every file of one demo shares a single region, so the state lives somewhere +# all of the demo's modules can reach. They are exec'd into separate module +# dicts, which share nothing but the interpreter's builtins. +_reactpy_console_state = getattr(_reactpy_builtins, _reactpy_console_key, None) +if _reactpy_console_state is None: + _reactpy_console_state = {{"lines": [], "chars": 0, "box": None}} + setattr(_reactpy_builtins, _reactpy_console_key, _reactpy_console_state) + + +def _reactpy_console(chunk): + """Append to the console region under this example's panel. + + The element is made once and kept, because there is no reliable way to test + for its absence: Pyodide hands back a ``JsNull`` proxy for a missing + element, which is not ``None`` and quietly accepts attribute assignment. So + the panel is looked up only while building the element, and a panel that + has gone away just means nowhere to write. + """ + state = _reactpy_console_state + box = state["box"] + if box is None: + try: + host = _reactpy_document.querySelector('[data-reactpy-run="{block_id}"]') + box = _reactpy_document.createElement("div") + box.id = "reactpy-console-{block_id}" + box.className = "reactpy-run-console" + host.appendChild(box) + except Exception: + return + state["box"] = box + state["lines"].append(chunk) + state["chars"] += len(chunk) + if state["chars"] > 20_000: # a runaway loop must not freeze the page + del state["lines"][:-1] + state["lines"][0] = "... (output truncated)\\n" + state["lines"][0][-2_000:] + state["chars"] = len(state["lines"][0]) + box.textContent = "".join(state["lines"]) + box.scrollTop = box.scrollHeight + + +def print(*args, sep=" ", end="\\n", file=None, flush=False): + \"\"\"``print()``, but into this example's console region.\"\"\" + if file is not None: + return _reactpy_builtins.print(*args, sep=sep, end=end, file=file) + text = sep.join(_reactpy_builtins.str(a) for a in args) + end + try: + _reactpy_console(text) + except Exception as _reactpy_exc: # a console is not a reason to break a demo + _reactpy_builtins.print(text, end="") + _reactpy_sys.stderr.write(f"reactpy-run: console failed ({{_reactpy_exc!r}})\\n") +''' + compile(shim, "<reactpy-run console>", "exec") + return shim + + +def _as_console_script(source: str) -> str: + """Turn a print-style example into a component that shows its stdout. + + Roughly a fifth of the examples teach VDOM by ``print()``-ing a result + rather than mounting a component, so there is no root component to run. + Capturing stdout and rendering it keeps those examples runnable instead of + leaving them as dead placeholders. + """ + import textwrap + + return ( + "import contextlib\nimport io\nfrom reactpy import component, html\n\n" + "_stdout = io.StringIO()\n" + "with contextlib.redirect_stdout(_stdout):\n" + f"{textwrap.indent(source, ' ')}\n\n" + "@component\n" + "def _console_output():\n" + ' return html.pre(_stdout.getvalue() or "(no output)")\n' + ) + + +# ------------------------------------------------------------------ utils -- + + +def _tab_sets(markdown: str) -> list[dict]: + """Group consecutive ``=== "Title"`` tabs that form one tab set.""" + tabs = [ + { + "indent": m.group(0)[: len(m.group(0)) - len(m.group(0).lstrip())], + "title": m.group("title"), + "start": m.start(), + "line_end": m.end() + 1, + } + for m in _TAB_RE.finditer(markdown) + ] + sets: list[dict] = [] + for tab in tabs: + if ( + sets + and sets[-1]["indent"] == tab["indent"] + and _still_in_set(markdown, sets[-1]["end"], tab["start"], tab["indent"]) + ): + sets[-1]["tabs"].append(tab) + else: + sets.append( + {"indent": tab["indent"], "tabs": [tab], "end": tab["line_end"]} + ) + # A set runs to the end of its last tab's indented body block. + sets[-1]["end"] = _consume_body(markdown, tab["line_end"], tab["indent"]) + + for group in sets: + for i, tab in enumerate(group["tabs"]): + tab["body_start"] = tab["line_end"] + tab["body_end"] = ( + group["tabs"][i + 1]["start"] + if i + 1 < len(group["tabs"]) + else group["end"] + ) + return sets + + +def _still_in_set(markdown: str, from_pos: int, to_pos: int, indent: str) -> bool: + """True if only blank or deeper-indented lines sit between two tabs.""" + for line in markdown[from_pos:to_pos].splitlines(): + if ( + line.strip() + and not line.startswith(" " * (len(indent) + 1)) + and not line.startswith("\t") + ): + return False + return True + + +def _consume_body(markdown: str, pos: int, indent: str) -> int: + """Offset just past the indented block that follows a ``=== "Title"`` line.""" + deeper = " " * (len(indent) + 1) + end = pos + for line in markdown[pos:].split("\n"): + if not line.strip() or line.startswith(deeper) or line.startswith("\t"): + end += len(line) + 1 + else: + break + # Trailing blank lines belong to whatever comes next, not to this body. + return pos + len(markdown[pos:end].rstrip("\n")) + + +def _scope_css(css: str, container_selector: str) -> str: + """Attach a demo's CSS to its own container so docs styles do not leak.""" + out: list[str] = [] + for rule in re.findall(r"([^{}]+)\{([^{}]*)\}", css): + selectors, declarations = rule + if selectors.strip().startswith("@"): + continue + scoped = [] + for selector in selectors.split(","): + sel = selector.strip() + if not sel: + continue + if sel in {"body", "html", "*"}: + scoped.append(container_selector) + else: + scoped.append(f"{container_selector} {sel}") + out.append(f"{', '.join(scoped)}{{{declarations}}}") + return "".join(out) + + +def _is_under(path: Path, root: Path) -> bool: + return root in path.parents + + +def _make_id(*parts: str) -> str: + return hashlib.md5("\0".join(parts).encode(), usedforsecurity=False).hexdigest()[ + :12 + ] + + +def _write_runtime_config(site_dir: Path, config) -> None: + """Emit the PyScript bootstrap constants straight from ReactPy itself.""" + from reactpy.executors.pyscript.utils import ( + PYSCRIPT_LAYOUT_HANDLER, + extend_pyscript_config, + ) + + # PyScript resolves every URL in this JSON against the *document*, so a + # relative path would break on a page at /learn/some/page/. Hand it a + # marker instead and let the client expand it relative to this file. The + # wheel may be cached because ReactPy names it after its own bytes, so the + # URL changes whenever the wheel does. + pyscript_config = extend_pyscript_config( + [], {}, {}, asset_prefix=STATIC_MARKER, packages_cache="normal" + ) + payload = { + "staticRoot": "static", + "staticMarker": STATIC_MARKER, + "pyscriptConfig": pyscript_config, + "layoutHandler": PYSCRIPT_LAYOUT_HANDLER, + } + out = site_dir / RUNTIME_DIR + out.mkdir(parents=True, exist_ok=True) + (out / "reactpy-run-config.js").write_text( + f"export const RUNTIME = {json.dumps(payload)};\n" + "export const CONFIG_URL = import.meta.url;\n", + encoding="utf-8", + ) diff --git a/docs/poetry.lock b/docs/poetry.lock deleted file mode 100644 index 4b1c41d1d..000000000 --- a/docs/poetry.lock +++ /dev/null @@ -1,2671 +0,0 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. - -[[package]] -name = "aiofiles" -version = "24.1.0" -description = "File support for asyncio." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, - {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, -] - -[[package]] -name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.8.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, - {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] -trio = ["trio (>=0.26.1)"] - -[[package]] -name = "asgiref" -version = "3.8.1" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, - {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - -[package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] - -[[package]] -name = "babel" -version = "2.12.1" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, - {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, -] - -[[package]] -name = "beautifulsoup4" -version = "4.12.2" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -groups = ["main"] -files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "certifi" -version = "2023.5.7" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, - {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.1.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -groups = ["main"] -files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, -] - -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "colorlog" -version = "6.9.0" -description = "Add colours to the output of Python's logging module." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff"}, - {file = "colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -development = ["black", "flake8", "mypy", "pytest", "types-colorama"] - -[[package]] -name = "contourpy" -version = "1.0.7" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d"}, - {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab"}, - {file = "contourpy-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69"}, - {file = "contourpy-1.0.7-cp310-cp310-win32.whl", hash = "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3"}, - {file = "contourpy-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80"}, - {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71"}, - {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5"}, - {file = "contourpy-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414"}, - {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2"}, - {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02"}, - {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae"}, - {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc"}, - {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac"}, - {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566"}, - {file = "contourpy-1.0.7-cp311-cp311-win32.whl", hash = "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0"}, - {file = "contourpy-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc"}, - {file = "contourpy-1.0.7-cp38-cp38-win32.whl", hash = "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66"}, - {file = "contourpy-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967"}, - {file = "contourpy-1.0.7-cp39-cp39-win32.whl", hash = "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693"}, - {file = "contourpy-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd"}, - {file = "contourpy-1.0.7.tar.gz", hash = "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e"}, -] - -[package.dependencies] -numpy = ">=1.16" - -[package.extras] -bokeh = ["bokeh", "chromedriver", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh]", "docutils-stubs", "mypy (==0.991)", "types-Pillow"] -test = ["Pillow", "matplotlib", "pytest"] -test-no-images = ["pytest"] - -[[package]] -name = "cycler" -version = "0.11.0" -description = "Composable style cycles" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, -] - -[[package]] -name = "docutils" -version = "0.17.1" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] -files = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "fastapi" -version = "0.115.6" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, - {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.42.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "fastjsonschema" -version = "2.21.1" -description = "Fastest Python implementation of JSON schema" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, - {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, -] - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - -[[package]] -name = "flask" -version = "2.1.3" -description = "A simple framework for building complex web applications." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "Flask-2.1.3-py3-none-any.whl", hash = "sha256:9013281a7402ad527f8fd56375164f3aa021ecfaff89bfe3825346c24f87e04c"}, - {file = "Flask-2.1.3.tar.gz", hash = "sha256:15972e5017df0575c3d6c090ba168b6db90259e620ac8d7ea813a396bad5b6cb"}, -] - -[package.dependencies] -click = ">=8.0" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.0" -Jinja2 = ">=3.0" -Werkzeug = ">=2.0" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] - -[[package]] -name = "flask-cors" -version = "5.0.0" -description = "A Flask extension adding a decorator for CORS support" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc"}, - {file = "flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef"}, -] - -[package.dependencies] -Flask = ">=0.9" - -[[package]] -name = "flask-sock" -version = "0.7.0" -description = "WebSocket support for Flask" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "flask-sock-0.7.0.tar.gz", hash = "sha256:e023b578284195a443b8d8bdb4469e6a6acf694b89aeb51315b1a34fcf427b7d"}, - {file = "flask_sock-0.7.0-py3-none-any.whl", hash = "sha256:caac4d679392aaf010d02fabcf73d52019f5bdaf1c9c131ec5a428cb3491204a"}, -] - -[package.dependencies] -flask = ">=2" -simple-websocket = ">=0.5.1" - -[package.extras] -docs = ["sphinx"] - -[[package]] -name = "fonttools" -version = "4.39.4" -description = "Tools to manipulate font files" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "fonttools-4.39.4-py3-none-any.whl", hash = "sha256:106caf6167c4597556b31a8d9175a3fdc0356fdcd70ab19973c3b0d4c893c461"}, - {file = "fonttools-4.39.4.zip", hash = "sha256:dba8d7cdb8e2bac1b3da28c5ed5960de09e59a2fe7e63bb73f5a59e57b0430d2"}, -] - -[package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "scipy"] -lxml = ["lxml (>=4.0,<5)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.0.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] - -[[package]] -name = "furo" -version = "2022.4.7" -description = "A clean customisable Sphinx documentation theme." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "furo-2022.4.7-py3-none-any.whl", hash = "sha256:7f3e3d2fb977483590f8ecb2c2cd511bd82661b79c18efb24de9558bc9cdf2d7"}, - {file = "furo-2022.4.7.tar.gz", hash = "sha256:96204ab7cd047e4b6c523996e0279c4c629a8fc31f4f109b2efd470c17f49c80"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -pygments = ">=2.7,<3.0" -sphinx = ">=4.0,<5.0" - -[[package]] -name = "greenlet" -version = "3.1.1" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, - {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, - {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, - {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, - {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, - {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, - {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, - {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, - {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, - {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, - {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, - {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, - {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, - {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, - {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, - {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, - {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, - {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, - {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, - {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, - {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, - {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, - {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, - {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, - {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, - {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, - {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, - {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, - {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, - {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, - {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, - {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, - {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "html5tagger" -version = "1.3.0" -description = "Pythonic HTML generation/templating (no template files)" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "html5tagger-1.3.0-py3-none-any.whl", hash = "sha256:ce14313515edffec8ed8a36c5890d023922641171b4e6e5774ad1a74998f5351"}, - {file = "html5tagger-1.3.0.tar.gz", hash = "sha256:84fa3dfb49e5c83b79bbd856ab7b1de8e2311c3bb46a8be925f119e3880a8da9"}, -] - -[[package]] -name = "httptools" -version = "0.6.4" -description = "A collection of framework independent HTTP protocol utils." -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -files = [ - {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, - {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, - {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, - {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, - {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, - {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, - {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, - {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, - {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, - {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, - {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, - {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, - {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, - {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, - {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, - {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, - {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, - {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, - {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, - {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, - {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, - {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, - {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, - {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, - {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, - {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, - {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, - {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, - {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, - {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, - {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, - {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, - {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, - {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, - {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, - {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, - {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, - {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, - {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, - {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, - {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, - {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, - {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, -] - -[package.extras] -test = ["Cython (>=0.29.24)"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "6.6.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.7" -groups = ["main"] -markers = "python_version < \"3.10\"" -files = [ - {file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"}, - {file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "importlib-resources" -version = "5.12.0" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.7" -groups = ["main"] -markers = "python_version < \"3.10\"" -files = [ - {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, - {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -description = "Safely pass data to untrusted environments and back." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, - {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, -] - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jsonpatch" -version = "1.33" -description = "Apply JSON-Patches (RFC 6902)" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -groups = ["main"] -files = [ - {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, - {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, -] - -[package.dependencies] -jsonpointer = ">=1.9" - -[[package]] -name = "jsonpointer" -version = "3.0.0" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, - {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, -] - -[[package]] -name = "kiwisolver" -version = "1.4.4" -description = "A fast implementation of the Cassowary constraint solver" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, - {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, -] - -[[package]] -name = "livereload" -version = "2.6.3" -description = "Python LiveReload is an awesome tool for web developers" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "livereload-2.6.3-py2.py3-none-any.whl", hash = "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4"}, - {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, -] - -[package.dependencies] -six = "*" -tornado = {version = "*", markers = "python_version > \"2.7\""} - -[[package]] -name = "lxml" -version = "5.3.0" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, - {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, - {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, - {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, - {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, - {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, - {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, - {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, - {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, - {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, - {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, - {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, - {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, - {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, - {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, - {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, - {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, - {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, - {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, - {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, - {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, - {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml-html-clean"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.11)"] - -[[package]] -name = "markupsafe" -version = "2.0.1" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, -] - -[[package]] -name = "matplotlib" -version = "3.7.1" -description = "Python plotting package" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1"}, - {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353"}, - {file = "matplotlib-3.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556"}, - {file = "matplotlib-3.7.1-cp310-cp310-win32.whl", hash = "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24"}, - {file = "matplotlib-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba"}, - {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61"}, - {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476"}, - {file = "matplotlib-3.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba"}, - {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332"}, - {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6"}, - {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0"}, - {file = "matplotlib-3.7.1-cp311-cp311-win32.whl", hash = "sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304"}, - {file = "matplotlib-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa"}, - {file = "matplotlib-3.7.1-cp38-cp38-win32.whl", hash = "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0"}, - {file = "matplotlib-3.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb"}, - {file = "matplotlib-3.7.1-cp39-cp39-win32.whl", hash = "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b"}, - {file = "matplotlib-3.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529"}, - {file = "matplotlib-3.7.1.tar.gz", hash = "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.0.1" -numpy = ">=1.20" -packaging = ">=20.0" -pillow = ">=6.2.0" -pyparsing = ">=2.3.1" -python-dateutil = ">=2.7" - -[[package]] -name = "multidict" -version = "6.1.0" -description = "multidict implementation" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, - {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, - {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, - {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, - {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, - {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, - {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, - {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, - {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, - {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, - {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, - {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, - {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, - {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, - {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "numpy" -version = "1.24.3" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "numpy-1.24.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570"}, - {file = "numpy-1.24.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7"}, - {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463"}, - {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6"}, - {file = "numpy-1.24.3-cp310-cp310-win32.whl", hash = "sha256:f21c442fdd2805e91799fbe044a7b999b8571bb0ab0f7850d0cb9641a687092b"}, - {file = "numpy-1.24.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f23af8c16022663a652d3b25dcdc272ac3f83c3af4c02eb8b824e6b3ab9d7"}, - {file = "numpy-1.24.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a7721ec204d3a237225db3e194c25268faf92e19338a35f3a224469cb6039a3"}, - {file = "numpy-1.24.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6cc757de514c00b24ae8cf5c876af2a7c3df189028d68c0cb4eaa9cd5afc2bf"}, - {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76e3f4e85fc5d4fd311f6e9b794d0c00e7002ec122be271f2019d63376f1d385"}, - {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1d3c026f57ceaad42f8231305d4653d5f05dc6332a730ae5c0bea3513de0950"}, - {file = "numpy-1.24.3-cp311-cp311-win32.whl", hash = "sha256:c91c4afd8abc3908e00a44b2672718905b8611503f7ff87390cc0ac3423fb096"}, - {file = "numpy-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:5342cf6aad47943286afa6f1609cad9b4266a05e7f2ec408e2cf7aea7ff69d80"}, - {file = "numpy-1.24.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7776ea65423ca6a15255ba1872d82d207bd1e09f6d0894ee4a64678dd2204078"}, - {file = "numpy-1.24.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ae8d0be48d1b6ed82588934aaaa179875e7dc4f3d84da18d7eae6eb3f06c242c"}, - {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecde0f8adef7dfdec993fd54b0f78183051b6580f606111a6d789cd14c61ea0c"}, - {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4749e053a29364d3452c034827102ee100986903263e89884922ef01a0a6fd2f"}, - {file = "numpy-1.24.3-cp38-cp38-win32.whl", hash = "sha256:d933fabd8f6a319e8530d0de4fcc2e6a61917e0b0c271fded460032db42a0fe4"}, - {file = "numpy-1.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:56e48aec79ae238f6e4395886b5eaed058abb7231fb3361ddd7bfdf4eed54289"}, - {file = "numpy-1.24.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4719d5aefb5189f50887773699eaf94e7d1e02bf36c1a9d353d9f46703758ca4"}, - {file = "numpy-1.24.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ec87a7084caa559c36e0a2309e4ecb1baa03b687201d0a847c8b0ed476a7187"}, - {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea8282b9bcfe2b5e7d491d0bf7f3e2da29700cec05b49e64d6246923329f2b02"}, - {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210461d87fb02a84ef243cac5e814aad2b7f4be953b32cb53327bb49fd77fbb4"}, - {file = "numpy-1.24.3-cp39-cp39-win32.whl", hash = "sha256:784c6da1a07818491b0ffd63c6bbe5a33deaa0e25a20e1b3ea20cf0e43f8046c"}, - {file = "numpy-1.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:d5036197ecae68d7f491fcdb4df90082b0d4960ca6599ba2659957aafced7c17"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:352ee00c7f8387b44d19f4cada524586f07379c0d49270f87233983bc5087ca0"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d6acc2e7524c9955e5c903160aa4ea083736fde7e91276b0e5d98e6332812"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:35400e6a8d102fd07c71ed7dcadd9eb62ee9a6e84ec159bd48c28235bbb0f8e4"}, - {file = "numpy-1.24.3.tar.gz", hash = "sha256:ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155"}, -] - -[[package]] -name = "packaging" -version = "23.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, -] - -[[package]] -name = "pillow" -version = "9.5.0" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, - {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, - {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, - {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, - {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, - {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, - {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, - {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, - {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, - {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, - {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, - {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, - {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, - {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, - {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, - {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, - {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "playwright" -version = "1.49.1" -description = "A high-level API to automate web browsers" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "playwright-1.49.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:1041ffb45a0d0bc44d698d3a5aa3ac4b67c9bd03540da43a0b70616ad52592b8"}, - {file = "playwright-1.49.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f38ed3d0c1f4e0a6d1c92e73dd9a61f8855133249d6f0cec28648d38a7137be"}, - {file = "playwright-1.49.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3be48c6d26dc819ca0a26567c1ae36a980a0303dcd4249feb6f59e115aaddfb8"}, - {file = "playwright-1.49.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:753ca90ee31b4b03d165cfd36e477309ebf2b4381953f2a982ff612d85b147d2"}, - {file = "playwright-1.49.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd9bc8dab37aa25198a01f555f0a2e2c3813fe200fef018ac34dfe86b34994b9"}, - {file = "playwright-1.49.1-py3-none-win32.whl", hash = "sha256:43b304be67f096058e587dac453ece550eff87b8fbed28de30f4f022cc1745bb"}, - {file = "playwright-1.49.1-py3-none-win_amd64.whl", hash = "sha256:47b23cb346283278f5b4d1e1990bcb6d6302f80c0aa0ca93dd0601a1400191df"}, -] - -[package.dependencies] -greenlet = "3.1.1" -pyee = "12.0.0" - -[[package]] -name = "pydantic" -version = "2.10.5" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"}, - {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" -typing-extensions = ">=4.12.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pyee" -version = "12.0.0" -description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990"}, - {file = "pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145"}, -] - -[package.dependencies] -typing-extensions = "*" - -[package.extras] -dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] - -[[package]] -name = "pygments" -version = "2.15.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, -] - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.6.8" -groups = ["main"] -files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "reactpy" -version = "1.1.0" -description = "It's React, but in Python." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [] -develop = false - -[package.dependencies] -anyio = ">=3" -asgiref = ">=3" -colorlog = ">=6" -exceptiongroup = ">=1.0" -fastapi = {version = ">=0.63.0", optional = true, markers = "extra == \"all\""} -fastjsonschema = ">=2.14.5" -flask = {version = "*", optional = true, markers = "extra == \"all\""} -flask-cors = {version = "*", optional = true, markers = "extra == \"all\""} -flask-sock = {version = "*", optional = true, markers = "extra == \"all\""} -jsonpatch = ">=1.32" -lxml = ">=4" -markupsafe = {version = ">=1.1.1,<2.1", optional = true, markers = "extra == \"all\""} -mypy-extensions = ">=0.4.3" -playwright = {version = "*", optional = true, markers = "extra == \"all\""} -requests = ">=2" -sanic = {version = ">=21", optional = true, markers = "extra == \"all\""} -sanic-cors = {version = "*", optional = true, markers = "extra == \"all\""} -setuptools = {version = "*", optional = true, markers = "extra == \"all\""} -starlette = {version = ">=0.13.6", optional = true, markers = "extra == \"all\""} -tornado = {version = "*", optional = true, markers = "extra == \"all\""} -tracerite = {version = ">=1.1.1", optional = true, markers = "extra == \"all\""} -typing-extensions = ">=3.10" -uvicorn = {version = ">=0.19.0", extras = ["standard"], optional = true, markers = "extra == \"all\""} - -[package.extras] -all = ["fastapi (>=0.63.0)", "flask", "flask-cors", "flask-sock", "markupsafe (>=1.1.1,<2.1)", "playwright", "sanic (>=21)", "sanic-cors", "setuptools", "starlette (>=0.13.6)", "tornado", "tracerite (>=1.1.1)", "uvicorn[standard] (>=0.19.0)"] -fastapi = ["fastapi (>=0.63.0)", "uvicorn[standard] (>=0.19.0)"] -flask = ["flask", "flask-cors", "flask-sock", "markupsafe (>=1.1.1,<2.1)"] -sanic = ["sanic (>=21)", "sanic-cors", "setuptools", "tracerite (>=1.1.1)", "uvicorn[standard] (>=0.19.0)"] -starlette = ["starlette (>=0.13.6)", "uvicorn[standard] (>=0.19.0)"] -testing = ["playwright"] -tornado = ["tornado"] - -[package.source] -type = "directory" -url = ".." - -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "sanic" -version = "24.12.0" -description = "A web server and web framework that's written to go fast. Build fast. Run fast." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "sanic-24.12.0-py3-none-any.whl", hash = "sha256:3c2a01ec0b6c5926e3efe34eac1b497d31ed989038fe213eb25ad0c98687d388"}, - {file = "sanic-24.12.0.tar.gz", hash = "sha256:09c23aa917616c1e60e44c66dfd7582cb9fd6503f78298c309945909f5839836"}, -] - -[package.dependencies] -aiofiles = ">=0.6.0" -html5tagger = ">=1.2.1" -httptools = ">=0.0.10" -multidict = ">=5.0,<7.0" -sanic-routing = ">=23.12.0" -setuptools = ">=70.1.0" -tracerite = ">=1.0.0" -typing-extensions = ">=4.4.0" -ujson = {version = ">=1.35", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""} -uvloop = {version = ">=0.15.0", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""} -websockets = ">=10.0" - -[package.extras] -all = ["autodocsumm (>=0.2.11)", "bandit", "beautifulsoup4", "chardet (==3.*)", "coverage", "cryptography", "docutils", "enum-tools[sphinx]", "m2r2", "mistune (<2.0.0)", "mypy", "pygments", "pytest (>=8.2.2)", "pytest-benchmark", "pytest-sanic", "ruff", "sanic-testing (>=23.6.0)", "slotscheck (>=0.8.0,<1)", "sphinx (>=2.1.2)", "sphinx_rtd_theme (>=0.4.3)", "towncrier", "tox", "types-ujson", "uvicorn"] -dev = ["bandit", "beautifulsoup4", "chardet (==3.*)", "coverage", "cryptography", "docutils", "mypy", "pygments", "pytest (>=8.2.2)", "pytest-benchmark", "pytest-sanic", "ruff", "sanic-testing (>=23.6.0)", "slotscheck (>=0.8.0,<1)", "towncrier", "tox", "types-ujson", "uvicorn"] -docs = ["autodocsumm (>=0.2.11)", "docutils", "enum-tools[sphinx]", "m2r2", "mistune (<2.0.0)", "pygments", "sphinx (>=2.1.2)", "sphinx_rtd_theme (>=0.4.3)"] -ext = ["sanic-ext"] -http3 = ["aioquic"] -test = ["bandit", "beautifulsoup4", "chardet (==3.*)", "coverage", "docutils", "mypy", "pygments", "pytest (>=8.2.2)", "pytest-benchmark", "pytest-sanic", "ruff", "sanic-testing (>=23.6.0)", "slotscheck (>=0.8.0,<1)", "types-ujson", "uvicorn"] - -[[package]] -name = "sanic-cors" -version = "2.2.0" -description = "A Sanic extension adding a decorator for CORS support. Based on flask-cors by Cory Dolphin." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "Sanic-Cors-2.2.0.tar.gz", hash = "sha256:f8d7515da4c8b837871d422c66314c4b5704396a78894b59c50e26aa72a95873"}, - {file = "Sanic_Cors-2.2.0-py2.py3-none-any.whl", hash = "sha256:c3b133ff1f0bb609a53db35f727f5c371dc4ebeb6be4cc2c37c19dd8b9301115"}, -] - -[package.dependencies] -packaging = ">=21.3" -sanic = ">=21.9.3" - -[[package]] -name = "sanic-routing" -version = "23.12.0" -description = "Core routing component for Sanic" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "sanic-routing-23.12.0.tar.gz", hash = "sha256:1dcadc62c443e48c852392dba03603f9862b6197fc4cba5bbefeb1ace0848b04"}, - {file = "sanic_routing-23.12.0-py3-none-any.whl", hash = "sha256:1558a72afcb9046ed3134a5edae02fc1552cff08f0fff2e8d5de0877ea43ed73"}, -] - -[[package]] -name = "setuptools" -version = "75.8.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, - {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "simple-websocket" -version = "1.1.0" -description = "Simple WebSocket server and client for Python" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c"}, - {file = "simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4"}, -] - -[package.dependencies] -wsproto = "*" - -[package.extras] -dev = ["flake8", "pytest", "pytest-cov", "tox"] -docs = ["sphinx"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["main"] -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "soupsieve" -version = "2.4.1" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, - {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, -] - -[[package]] -name = "sphinx" -version = "4.5.0" -description = "Python documentation generator" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, - {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, -] - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=1.3" -colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.18" -imagesize = "*" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} -Jinja2 = ">=2.3" -packaging = "*" -Pygments = ">=2.0" -requests = ">=2.5.0" -snowballstemmer = ">=1.1" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"] - -[[package]] -name = "sphinx-autobuild" -version = "2021.3.14" -description = "Rebuild Sphinx documentation on changes, with live-reload in the browser." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, - {file = "sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac"}, -] - -[package.dependencies] -colorama = "*" -livereload = "*" -sphinx = "*" - -[package.extras] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "1.19.1" -description = "Type hints (PEP 484) support for the Sphinx autodoc extension" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sphinx_autodoc_typehints-1.19.1-py3-none-any.whl", hash = "sha256:9be46aeeb1b315eb5df1f3a7cb262149895d16c7d7dcd77b92513c3c3a1e85e6"}, - {file = "sphinx_autodoc_typehints-1.19.1.tar.gz", hash = "sha256:6c841db55e0e9be0483ff3962a2152b60e79306f4288d8c4e7e86ac84486a5ea"}, -] - -[package.dependencies] -Sphinx = ">=4.5" - -[package.extras] -testing = ["covdefaults (>=2.2)", "coverage (>=6.3)", "diff-cover (>=6.4)", "nptyping (>=2.1.2)", "pytest (>=7.1)", "pytest-cov (>=3)", "sphobjinv (>=2)", "typing-extensions (>=4.1)"] -type-comments = ["typed-ast (>=1.5.2)"] - -[[package]] -name = "sphinx-copybutton" -version = "0.5.2" -description = "Add a copy button to each of your code cells." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, - {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, -] - -[package.dependencies] -sphinx = ">=1.8" - -[package.extras] -code-style = ["pre-commit (==2.12.1)"] -rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] - -[[package]] -name = "sphinx-design" -version = "0.4.1" -description = "A sphinx extension for designing beautiful, view size responsive web components." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sphinx_design-0.4.1-py3-none-any.whl", hash = "sha256:23bf5705eb31296d4451f68b0222a698a8a84396ffe8378dfd9319ba7ab8efd9"}, - {file = "sphinx_design-0.4.1.tar.gz", hash = "sha256:5b6418ba4a2dc3d83592ea0ff61a52a891fe72195a4c3a18b2fa1c7668ce4708"}, -] - -[package.dependencies] -sphinx = ">=4,<7" - -[package.extras] -code-style = ["pre-commit (>=2.12,<3.0)"] -rtd = ["myst-parser (>=0.18.0,<2)"] -testing = ["myst-parser (>=0.18.0,<2)", "pytest (>=7.1,<8.0)", "pytest-cov", "pytest-regressions"] -theme-furo = ["furo (>=2022.06.04,<2022.07)"] -theme-pydata = ["pydata-sphinx-theme (>=0.9.0,<0.10.0)"] -theme-rtd = ["sphinx-rtd-theme (>=1.0,<2.0)"] -theme-sbt = ["sphinx-book-theme (>=0.3.0,<0.4.0)"] - -[[package]] -name = "sphinx-reredirects" -version = "0.1.2" -description = "Handles redirects for moved pages in Sphinx documentation projects" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "sphinx_reredirects-0.1.2-py3-none-any.whl", hash = "sha256:3a22161771aadd448bb608a4fe7277252182a337af53c18372b7104531d71489"}, - {file = "sphinx_reredirects-0.1.2.tar.gz", hash = "sha256:a0e7213304759b01edc22f032f1715a1c61176fc8f167164e7a52b9feec9ac64"}, -] - -[package.dependencies] -sphinx = "*" - -[[package]] -name = "sphinx-resolve-py-references" -version = "0.1.0" -description = "Better python object resolution in Sphinx" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sphinx_resolve_py_references-0.1.0-py2.py3-none-any.whl", hash = "sha256:ccf44a6b62d75c3a568285f4e1815734088c1a7cab7bbb7935bb22fbf0d78bc2"}, - {file = "sphinx_resolve_py_references-0.1.0.tar.gz", hash = "sha256:0f87c06b29ec128964aee2e40d170d1d3c0e5f4955b2618a89ca724f42385372"}, -] - -[package.dependencies] -sphinx = "*" - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.4" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.1" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxext-opengraph" -version = "0.8.2" -description = "Sphinx Extension to enable OGP support" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sphinxext-opengraph-0.8.2.tar.gz", hash = "sha256:45a693b6704052c426576f0a1f630649c55b4188bc49eb63e9587e24a923db39"}, - {file = "sphinxext_opengraph-0.8.2-py3-none-any.whl", hash = "sha256:6a05bdfe5176d9dd0a1d58a504f17118362ab976631213cd36fb44c4c40544c9"}, -] - -[package.dependencies] -matplotlib = "*" -sphinx = ">=4.0" - -[[package]] -name = "starlette" -version = "0.41.3" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, - {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} - -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] - -[[package]] -name = "tornado" -version = "6.3.2" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = false -python-versions = ">= 3.8" -groups = ["main"] -files = [ - {file = "tornado-6.3.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829"}, - {file = "tornado-6.3.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c"}, - {file = "tornado-6.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f"}, - {file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4"}, - {file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe"}, - {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d"}, - {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0"}, - {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411"}, - {file = "tornado-6.3.2-cp38-abi3-win32.whl", hash = "sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2"}, - {file = "tornado-6.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf"}, - {file = "tornado-6.3.2.tar.gz", hash = "sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba"}, -] - -[[package]] -name = "tracerite" -version = "1.1.1" -description = "Human-readable HTML tracebacks for Python exceptions" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "tracerite-1.1.1-py3-none-any.whl", hash = "sha256:3a787a9ecb1a136ea9ce17e6328e414ec414a4f644130af4e1e330bec2dece29"}, - {file = "tracerite-1.1.1.tar.gz", hash = "sha256:6400a35a187747189e4bb8d4a8e471bd86d14dbdcc94bcad23f4eda023f41356"}, -] - -[package.dependencies] -html5tagger = ">=1.2.1" - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "ujson" -version = "5.10.0" -description = "Ultra fast JSON encoder and decoder for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "sys_platform != \"win32\" and implementation_name == \"cpython\"" -files = [ - {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, - {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, - {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"}, - {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"}, - {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"}, - {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"}, - {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"}, - {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"}, - {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"}, - {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"}, - {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"}, - {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"}, - {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"}, - {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"}, - {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"}, - {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"}, - {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"}, - {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"}, - {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"}, - {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"}, - {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"}, - {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"}, - {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"}, - {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"}, - {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"}, - {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"}, - {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"}, - {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"}, - {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"}, - {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"}, - {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"}, - {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"}, - {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"}, - {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"}, - {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"}, - {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"}, - {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"}, - {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"}, - {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"}, - {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"}, - {file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"}, - {file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"}, - {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"}, - {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"}, - {file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"}, - {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"}, - {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"}, - {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"}, - {file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"}, - {file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"}, - {file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"}, - {file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"}, - {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"}, - {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"}, - {file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"}, - {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"}, - {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"}, - {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"}, - {file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"}, - {file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"}, - {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, -] - -[[package]] -name = "urllib3" -version = "2.0.2" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"}, - {file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "uvicorn" -version = "0.34.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, - {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} -h11 = ">=0.8" -httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} -python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} - -[package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "uvloop" -version = "0.21.0" -description = "Fast implementation of asyncio event loop on top of libuv" -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -markers = "implementation_name == \"cpython\" and sys_platform != \"win32\" or (sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\"" -files = [ - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, - {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, -] - -[package.extras] -dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] - -[[package]] -name = "watchfiles" -version = "1.0.4" -description = "Simple, modern and high performance file watching and code reload in python." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08"}, - {file = "watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899"}, - {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff"}, - {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f"}, - {file = "watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f"}, - {file = "watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161"}, - {file = "watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19"}, - {file = "watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c"}, - {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1"}, - {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226"}, - {file = "watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105"}, - {file = "watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74"}, - {file = "watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3"}, - {file = "watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2"}, - {file = "watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a"}, - {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff"}, - {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e"}, - {file = "watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94"}, - {file = "watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c"}, - {file = "watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90"}, - {file = "watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9"}, - {file = "watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902"}, - {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1"}, - {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303"}, - {file = "watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80"}, - {file = "watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc"}, - {file = "watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21"}, - {file = "watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf"}, - {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a"}, - {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b"}, - {file = "watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27"}, - {file = "watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43"}, - {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18"}, - {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817"}, - {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0"}, - {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d"}, - {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3"}, - {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e"}, - {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb"}, - {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42"}, - {file = "watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205"}, -] - -[package.dependencies] -anyio = ">=3.0.0" - -[[package]] -name = "websockets" -version = "14.2" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885"}, - {file = "websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397"}, - {file = "websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610"}, - {file = "websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3"}, - {file = "websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980"}, - {file = "websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8"}, - {file = "websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7"}, - {file = "websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f"}, - {file = "websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d"}, - {file = "websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d"}, - {file = "websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2"}, - {file = "websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166"}, - {file = "websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f"}, - {file = "websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910"}, - {file = "websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c"}, - {file = "websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473"}, - {file = "websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473"}, - {file = "websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56"}, - {file = "websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142"}, - {file = "websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d"}, - {file = "websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a"}, - {file = "websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b"}, - {file = "websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c"}, - {file = "websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967"}, - {file = "websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990"}, - {file = "websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda"}, - {file = "websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95"}, - {file = "websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3"}, - {file = "websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9"}, - {file = "websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267"}, - {file = "websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe"}, - {file = "websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205"}, - {file = "websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce"}, - {file = "websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e"}, - {file = "websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad"}, - {file = "websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03"}, - {file = "websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f"}, - {file = "websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5"}, - {file = "websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a"}, - {file = "websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20"}, - {file = "websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2"}, - {file = "websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307"}, - {file = "websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc"}, - {file = "websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f"}, - {file = "websockets-14.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe"}, - {file = "websockets-14.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12"}, - {file = "websockets-14.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7"}, - {file = "websockets-14.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5"}, - {file = "websockets-14.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0"}, - {file = "websockets-14.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258"}, - {file = "websockets-14.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0"}, - {file = "websockets-14.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4"}, - {file = "websockets-14.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc"}, - {file = "websockets-14.2-cp39-cp39-win32.whl", hash = "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661"}, - {file = "websockets-14.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef"}, - {file = "websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29"}, - {file = "websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c"}, - {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"}, - {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c"}, - {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a"}, - {file = "websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3"}, - {file = "websockets-14.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f"}, - {file = "websockets-14.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42"}, - {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f"}, - {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574"}, - {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270"}, - {file = "websockets-14.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365"}, - {file = "websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b"}, - {file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"}, -] - -[[package]] -name = "werkzeug" -version = "2.1.2" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"}, - {file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"}, -] - -[package.extras] -watchdog = ["watchdog"] - -[[package]] -name = "wsproto" -version = "1.2.0" -description = "WebSockets state-machine based protocol implementation" -optional = false -python-versions = ">=3.7.0" -groups = ["main"] -files = [ - {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, - {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, -] - -[package.dependencies] -h11 = ">=0.9.0,<1" - -[[package]] -name = "zipp" -version = "3.15.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.7" -groups = ["main"] -markers = "python_version < \"3.10\"" -files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "2.1" -python-versions = "^3.9" -content-hash = "ce11f3670471f6ab3537180188001140700188469fbc1a03a25a630b2c6abbef" diff --git a/docs/pyproject.toml b/docs/pyproject.toml deleted file mode 100644 index 3be4b4a8a..000000000 --- a/docs/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[tool.poetry] -name = "docs_app" -version = "0.0.0" -description = "docs" -authors = ["rmorshea <ryan.morshead@gmail.com>"] -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.9" -furo = "2022.04.07" -reactpy = { path = "..", extras = ["all"], develop = false } -sphinx = "*" -sphinx-autodoc-typehints = "*" -sphinx-copybutton = "*" -sphinx-autobuild = "*" -sphinx-reredirects = "*" -sphinx-design = "*" -sphinx-resolve-py-references = "*" -sphinxext-opengraph = "*" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/docs/source/_custom_js/README.md b/docs/source/_custom_js/README.md deleted file mode 100644 index ef30b9a76..000000000 --- a/docs/source/_custom_js/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Custom Javascript for ReactPy's Docs - -Build the javascript with - -``` -bun run build -``` - -This will drop a javascript bundle into `../_static/custom.js` diff --git a/docs/source/_custom_js/bun.lockb b/docs/source/_custom_js/bun.lockb deleted file mode 100644 index 39e1393c3..000000000 Binary files a/docs/source/_custom_js/bun.lockb and /dev/null differ diff --git a/docs/source/_custom_js/package.json b/docs/source/_custom_js/package.json deleted file mode 100644 index 78d72b961..000000000 --- a/docs/source/_custom_js/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "reactpy-docs-example-loader", - "version": "1.0.0", - "description": "simple javascript client for ReactPy's documentation", - "main": "index.js", - "scripts": { - "build": "rollup --config", - "format": "prettier --ignore-path .gitignore --write ." - }, - "devDependencies": { - "@rollup/plugin-commonjs": "^21.0.1", - "@rollup/plugin-node-resolve": "^13.1.1", - "@rollup/plugin-replace": "^3.0.0", - "prettier": "^2.2.1", - "rollup": "^2.35.1" - }, - "dependencies": { - "@reactpy/client": "file:../../../src/js/packages/@reactpy/client" - } -} diff --git a/docs/source/_custom_js/rollup.config.js b/docs/source/_custom_js/rollup.config.js deleted file mode 100644 index 48dd535cf..000000000 --- a/docs/source/_custom_js/rollup.config.js +++ /dev/null @@ -1,26 +0,0 @@ -import resolve from "@rollup/plugin-node-resolve"; -import commonjs from "@rollup/plugin-commonjs"; -import replace from "@rollup/plugin-replace"; - -export default { - input: "src/index.js", - output: { - file: "../_static/custom.js", - format: "esm", - }, - plugins: [ - resolve(), - commonjs(), - replace({ - "process.env.NODE_ENV": JSON.stringify("production"), - preventAssignment: true, - }), - ], - onwarn: function (warning) { - if (warning.code === "THIS_IS_UNDEFINED") { - // skip warning where `this` is undefined at the top level of a module - return; - } - console.warn(warning.message); - }, -}; diff --git a/docs/source/_custom_js/src/index.js b/docs/source/_custom_js/src/index.js deleted file mode 100644 index 505adedd0..000000000 --- a/docs/source/_custom_js/src/index.js +++ /dev/null @@ -1,94 +0,0 @@ -import { SimpleReactPyClient, mount } from "@reactpy/client"; - -let didMountDebug = false; - -export function mountWidgetExample( - mountID, - viewID, - reactpyServerHost, - useActivateButton, -) { - let reactpyHost, reactpyPort; - if (reactpyServerHost) { - [reactpyHost, reactpyPort] = reactpyServerHost.split(":", 2); - } else { - reactpyHost = window.location.hostname; - reactpyPort = window.location.port; - } - - const client = new SimpleReactPyClient({ - serverLocation: { - url: `${window.location.protocol}//${reactpyHost}:${reactpyPort}`, - route: "/", - query: `?view_id=${viewID}`, - }, - }); - - const mountEl = document.getElementById(mountID); - let isMounted = false; - triggerIfInViewport(mountEl, () => { - if (!isMounted) { - activateView(mountEl, client, useActivateButton); - isMounted = true; - } - }); -} - -function activateView(mountEl, client, useActivateButton) { - if (!useActivateButton) { - mount(mountEl, client); - return; - } - - const enableWidgetButton = document.createElement("button"); - enableWidgetButton.appendChild(document.createTextNode("Activate")); - enableWidgetButton.setAttribute("class", "enable-widget-button"); - - enableWidgetButton.addEventListener("click", () => - fadeOutElementThenCallback(enableWidgetButton, () => { - { - mountEl.removeChild(enableWidgetButton); - mountEl.setAttribute("class", "interactive widget-container"); - mountWithLayoutServer(mountEl, serverInfo); - } - }), - ); - - function fadeOutElementThenCallback(element, callback) { - { - var op = 1; // initial opacity - var timer = setInterval(function () { - { - if (op < 0.001) { - { - clearInterval(timer); - element.style.display = "none"; - callback(); - } - } - element.style.opacity = op; - element.style.filter = "alpha(opacity=" + op * 100 + ")"; - op -= op * 0.5; - } - }, 50); - } - } - - mountEl.appendChild(enableWidgetButton); -} - -function triggerIfInViewport(element, callback) { - const observer = new window.IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { - callback(); - } - }, - { - root: null, - threshold: 0.1, // set offset 0.1 means trigger if at least 10% of element in viewport - }, - ); - - observer.observe(element); -} diff --git a/docs/source/_exts/async_doctest.py b/docs/source/_exts/async_doctest.py deleted file mode 100644 index 96024d488..000000000 --- a/docs/source/_exts/async_doctest.py +++ /dev/null @@ -1,47 +0,0 @@ -from doctest import DocTest, DocTestRunner -from textwrap import indent -from typing import Any - -from sphinx.application import Sphinx -from sphinx.ext.doctest import DocTestBuilder -from sphinx.ext.doctest import setup as doctest_setup - -test_template = """ -import asyncio as __test_template_asyncio - -async def __test_template__main(): - - {test} - - globals().update(locals()) - -__test_template_asyncio.run(__test_template__main()) -""" - - -class TestRunnerWrapper: - def __init__(self, runner: DocTestRunner): - self._runner = runner - - def __getattr__(self, name: str) -> Any: - return getattr(self._runner, name) - - def run(self, test: DocTest, *args: Any, **kwargs: Any) -> Any: - for ex in test.examples: - ex.source = test_template.format(test=indent(ex.source, " ").strip()) - return self._runner.run(test, *args, **kwargs) - - -class AsyncDoctestBuilder(DocTestBuilder): - @property - def test_runner(self) -> DocTestRunner: - return self._test_runner - - @test_runner.setter - def test_runner(self, value: DocTestRunner) -> None: - self._test_runner = TestRunnerWrapper(value) - - -def setup(app: Sphinx) -> None: - doctest_setup(app) - app.add_builder(AsyncDoctestBuilder, override=True) diff --git a/docs/source/_exts/autogen_api_docs.py b/docs/source/_exts/autogen_api_docs.py deleted file mode 100644 index cdd1f184e..000000000 --- a/docs/source/_exts/autogen_api_docs.py +++ /dev/null @@ -1,146 +0,0 @@ -from __future__ import annotations - -import sys -from collections.abc import Collection, Iterator -from pathlib import Path - -from sphinx.application import Sphinx - -HERE = Path(__file__).parent -SRC = HERE.parent.parent.parent / "src" -PYTHON_PACKAGE = SRC / "reactpy" - -AUTO_DIR = HERE.parent / "_auto" -AUTO_DIR.mkdir(exist_ok=True) - -API_FILE = AUTO_DIR / "apis.rst" - -# All valid RST section symbols - it shouldn't be realistically possible to exhaust them -SECTION_SYMBOLS = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" - -AUTODOC_TEMPLATE_WITH_MEMBERS = """\ -.. automodule:: {module} - :members: - :ignore-module-all: -""" - -AUTODOC_TEMPLATE_WITHOUT_MEMBERS = """\ -.. automodule:: {module} - :ignore-module-all: -""" - -TITLE = """\ -========== -Python API -========== -""" - - -def generate_api_docs(): - content = [TITLE] - - for file in walk_python_files(PYTHON_PACKAGE, ignore_dirs={"__pycache__"}): - if file.name == "__init__.py": - if file.parent != PYTHON_PACKAGE: - content.append(make_package_section(file)) - elif not file.name.startswith("_"): - content.append(make_module_section(file)) - - API_FILE.write_text("\n".join(content)) - - -def make_package_section(file: Path) -> str: - parent_dir = file.parent - symbol = get_section_symbol(parent_dir) - section_name = f"``{parent_dir.name}``" - module_name = get_module_name(parent_dir) - return ( - section_name - + "\n" - + (symbol * len(section_name)) - + "\n" - + AUTODOC_TEMPLATE_WITHOUT_MEMBERS.format(module=module_name) - ) - - -def make_module_section(file: Path) -> str: - symbol = get_section_symbol(file) - section_name = f"``{file.stem}``" - module_name = get_module_name(file) - return ( - section_name - + "\n" - + (symbol * len(section_name)) - + "\n" - + AUTODOC_TEMPLATE_WITH_MEMBERS.format(module=module_name) - ) - - -def get_module_name(path: Path) -> str: - return ".".join(path.with_suffix("").relative_to(PYTHON_PACKAGE.parent).parts) - - -def get_section_symbol(path: Path) -> str: - rel_path = path.relative_to(PYTHON_PACKAGE) - rel_path_parts = rel_path.parts - if len(rel_path_parts) > len(SECTION_SYMBOLS): - msg = f"package structure is too deep - ran out of section symbols: {rel_path}" - raise RuntimeError(msg) - return SECTION_SYMBOLS[len(rel_path_parts) - 1] - - -def walk_python_files(root: Path, ignore_dirs: Collection[str]) -> Iterator[Path]: - """Iterate over Python files - - We yield in a particular order to get the correction title section structure. Given - a directory structure of the form:: - - project/ - __init__.py - /package - __init__.py - module_a.py - module_b.py - - We yield the files in this order:: - - project / __init__.py - project / package / __init__.py - project / package / module_a.py - project / module_b.py - - In this way we generate the section titles in the appropriate order:: - - project - ======= - - project.package - --------------- - - project.package.module_a - ------------------------ - - """ - for path in sorted( - root.iterdir(), - key=lambda path: ( - # __init__.py files first - int(not path.name == "__init__.py"), - # then directories - int(not path.is_dir()), - # sort by file name last - path.name, - ), - ): - if path.is_dir(): - if (path / "__init__.py").exists() and path.name not in ignore_dirs: - yield from walk_python_files(path, ignore_dirs) - elif path.suffix == ".py": - yield path - - -def setup(app: Sphinx) -> None: - if sys.platform == "win32" and sys.version_info[:2] == (3, 7): - return None - generate_api_docs() - return None diff --git a/docs/source/_exts/build_custom_js.py b/docs/source/_exts/build_custom_js.py deleted file mode 100644 index a12c5c7f7..000000000 --- a/docs/source/_exts/build_custom_js.py +++ /dev/null @@ -1,12 +0,0 @@ -import subprocess -from pathlib import Path - -from sphinx.application import Sphinx - -SOURCE_DIR = Path(__file__).parent.parent -CUSTOM_JS_DIR = SOURCE_DIR / "_custom_js" - - -def setup(app: Sphinx) -> None: - subprocess.run("bun install", cwd=CUSTOM_JS_DIR, shell=True) # noqa S607 - subprocess.run("bun run build", cwd=CUSTOM_JS_DIR, shell=True) # noqa S607 diff --git a/docs/source/_exts/copy_vdom_json_schema.py b/docs/source/_exts/copy_vdom_json_schema.py deleted file mode 100644 index 38fc171ac..000000000 --- a/docs/source/_exts/copy_vdom_json_schema.py +++ /dev/null @@ -1,17 +0,0 @@ -import json -from pathlib import Path - -from sphinx.application import Sphinx - -from reactpy.core.vdom import VDOM_JSON_SCHEMA - - -def setup(app: Sphinx) -> None: - schema_file = Path(__file__).parent.parent / "vdom-json-schema.json" - current_schema = json.dumps(VDOM_JSON_SCHEMA, indent=2, sort_keys=True) - - # We need to make this check because the autoreload system for the docs checks - # to see if the file has changed to determine whether to re-build. Thus we should - # only write to the file if its contents will be different. - if not schema_file.exists() or schema_file.read_text() != current_schema: - schema_file.write_text(current_schema) diff --git a/docs/source/_exts/custom_autosectionlabel.py b/docs/source/_exts/custom_autosectionlabel.py deleted file mode 100644 index 92ff5e2df..000000000 --- a/docs/source/_exts/custom_autosectionlabel.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Mostly copied from sphinx.ext.autosectionlabel - -See Sphinx BSD license: -https://github.com/sphinx-doc/sphinx/blob/f9968594206e538f13fa1c27c065027f10d4ea27/LICENSE -""" - -from __future__ import annotations - -from fnmatch import fnmatch -from typing import Any, cast - -from docutils import nodes -from docutils.nodes import Node -from sphinx.application import Sphinx -from sphinx.domains.std import StandardDomain -from sphinx.locale import __ -from sphinx.util import logging -from sphinx.util.nodes import clean_astext - -logger = logging.getLogger(__name__) - - -def get_node_depth(node: Node) -> int: - i = 0 - cur_node = node - while cur_node.parent != node.document: - cur_node = cur_node.parent - i += 1 - return i - - -def register_sections_as_label(app: Sphinx, document: Node) -> None: - docname = app.env.docname - - for pattern in app.config.autosectionlabel_skip_docs: - if fnmatch(docname, pattern): - return None - - domain = cast(StandardDomain, app.env.get_domain("std")) - for node in document.traverse(nodes.section): - if ( - app.config.autosectionlabel_maxdepth - and get_node_depth(node) >= app.config.autosectionlabel_maxdepth - ): - continue - labelid = node["ids"][0] - - title = cast(nodes.title, node[0]) - ref_name = getattr(title, "rawsource", title.astext()) - if app.config.autosectionlabel_prefix_document: - name = nodes.fully_normalize_name(docname + ":" + ref_name) - else: - name = nodes.fully_normalize_name(ref_name) - sectname = clean_astext(title) - - if name in domain.labels: - logger.warning( - __("duplicate label %s, other instance in %s"), - name, - app.env.doc2path(domain.labels[name][0]), - location=node, - type="autosectionlabel", - subtype=docname, - ) - - domain.anonlabels[name] = docname, labelid - domain.labels[name] = docname, labelid, sectname - - -def setup(app: Sphinx) -> dict[str, Any]: - app.add_config_value("autosectionlabel_prefix_document", False, "env") - app.add_config_value("autosectionlabel_maxdepth", None, "env") - app.add_config_value("autosectionlabel_skip_docs", [], "env") - app.connect("doctree-read", register_sections_as_label) - - return { - "version": "builtin", - "parallel_read_safe": True, - "parallel_write_safe": True, - } diff --git a/docs/source/_exts/patched_html_translator.py b/docs/source/_exts/patched_html_translator.py deleted file mode 100644 index e2f8ed9a4..000000000 --- a/docs/source/_exts/patched_html_translator.py +++ /dev/null @@ -1,24 +0,0 @@ -from sphinx.util.docutils import is_html5_writer_available -from sphinx.writers.html import HTMLTranslator -from sphinx.writers.html5 import HTML5Translator - - -class PatchedHTMLTranslator( - HTML5Translator if is_html5_writer_available() else HTMLTranslator -): - def starttag(self, node, tagname, *args, **attrs): - if ( - tagname == "a" - and "target" not in attrs - and ( - "external" in attrs.get("class", "") - or "external" in attrs.get("classes", []) - ) - ): - attrs["target"] = "_blank" - attrs["ref"] = "noopener noreferrer" - return super().starttag(node, tagname, *args, **attrs) - - -def setup(app): - app.set_translator("html", PatchedHTMLTranslator) diff --git a/docs/source/_exts/reactpy_example.py b/docs/source/_exts/reactpy_example.py deleted file mode 100644 index 1171d32e0..000000000 --- a/docs/source/_exts/reactpy_example.py +++ /dev/null @@ -1,180 +0,0 @@ -from __future__ import annotations - -import re -from pathlib import Path -from typing import Any, ClassVar - -from docs_app.examples import ( - SOURCE_DIR, - get_example_files_by_name, - get_normalized_example_name, -) -from docutils.parsers.rst import directives -from docutils.statemachine import StringList -from sphinx.application import Sphinx -from sphinx.util.docutils import SphinxDirective -from sphinx_design.tabs import TabSetDirective - - -class WidgetExample(SphinxDirective): - has_content = False - required_arguments = 1 - _next_id = 0 - - option_spec: ClassVar[dict[str, Any]] = { - "result-is-default-tab": directives.flag, - "activate-button": directives.flag, - } - - def run(self): - example_name = get_normalized_example_name( - self.arguments[0], - # only used if example name starts with "/" - self.get_source_info()[0], - ) - - show_linenos = "linenos" in self.options - live_example_is_default_tab = "result-is-default-tab" in self.options - activate_result = "activate-button" not in self.options - - ex_files = get_example_files_by_name(example_name) - if not ex_files: - src_file, line_num = self.get_source_info() - msg = f"Missing example named {example_name!r} referenced by document {src_file}:{line_num}" - raise ValueError(msg) - - labeled_tab_items: list[tuple[str, Any]] = [] - if len(ex_files) == 1: - labeled_tab_items.append( - ( - "main.py", - _literal_include( - path=ex_files[0], - linenos=show_linenos, - ), - ) - ) - else: - for path in sorted( - ex_files, key=lambda p: "" if p.name == "main.py" else p.name - ): - labeled_tab_items.append( - ( - path.name, - _literal_include( - path=path, - linenos=show_linenos, - ), - ) - ) - - result_tab_item = ( - "🚀 result", - _interactive_widget( - name=example_name, - with_activate_button=not activate_result, - ), - ) - if live_example_is_default_tab: - labeled_tab_items.insert(0, result_tab_item) - else: - labeled_tab_items.append(result_tab_item) - - return TabSetDirective( - "WidgetExample", - [], - {}, - _make_tab_items(labeled_tab_items), - self.lineno - 2, - self.content_offset, - "", - self.state, - self.state_machine, - ).run() - - -def _make_tab_items(labeled_content_tuples): - tab_items = "" - for label, content in labeled_content_tuples: - tab_items += _tab_item_template.format( - label=label, - content=content.replace("\n", "\n "), - ) - return _string_to_nested_lines(tab_items) - - -def _literal_include(path: Path, linenos: bool): - try: - language = { - ".py": "python", - ".js": "javascript", - ".json": "json", - }[path.suffix] - except KeyError: - msg = f"Unknown extension type {path.suffix!r}" - raise ValueError(msg) from None - - return _literal_include_template.format( - name=str(path.relative_to(SOURCE_DIR)), - language=language, - options=_join_options(_get_file_options(path)), - ) - - -def _join_options(option_strings: list[str]) -> str: - return "\n ".join(option_strings) - - -OPTION_PATTERN = re.compile(r"#\s:[\w-]+:.*") - - -def _get_file_options(file: Path) -> list[str]: - options = [] - - for line in file.read_text().split("\n"): - if not line.strip(): - continue - if not line.startswith("#"): - break - if not OPTION_PATTERN.match(line): - continue - option_string = line[1:].strip() - if option_string: - options.append(option_string) - - return options - - -def _interactive_widget(name, with_activate_button): - return _interactive_widget_template.format( - name=name, - activate_button_opt=":activate-button:" if with_activate_button else "", - ) - - -_tab_item_template = """ -.. tab-item:: {label} - - {content} -""" - - -_interactive_widget_template = """ -.. reactpy-view:: {name} - {activate_button_opt} -""" - - -_literal_include_template = """ -.. literalinclude:: /{name} - :language: {language} - {options} -""" - - -def _string_to_nested_lines(content): - return StringList(content.split("\n")) - - -def setup(app: Sphinx) -> None: - app.add_directive("reactpy", WidgetExample) diff --git a/docs/source/_exts/reactpy_view.py b/docs/source/_exts/reactpy_view.py deleted file mode 100644 index 6a583998f..000000000 --- a/docs/source/_exts/reactpy_view.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -from typing import Any, ClassVar - -from docs_app.examples import get_normalized_example_name -from docutils.nodes import raw -from docutils.parsers.rst import directives -from sphinx.application import Sphinx -from sphinx.util.docutils import SphinxDirective - -_REACTPY_EXAMPLE_HOST = os.environ.get("REACTPY_DOC_EXAMPLE_SERVER_HOST", "") -_REACTPY_STATIC_HOST = os.environ.get("REACTPY_DOC_STATIC_SERVER_HOST", "/docs").rstrip( - "/" -) - - -class IteractiveWidget(SphinxDirective): - has_content = False - required_arguments = 1 - _next_id = 0 - - option_spec: ClassVar[dict[str, Any]] = { - "activate-button": directives.flag, - "margin": float, - } - - def run(self): - IteractiveWidget._next_id += 1 - container_id = f"reactpy-widget-{IteractiveWidget._next_id}" - view_id = get_normalized_example_name( - self.arguments[0], - # only used if example name starts with "/" - self.get_source_info()[0], - ) - return [ - raw( - "", - f""" - <div> - <div - id="{container_id}" - class="interactive widget-container" - style="margin-bottom: {self.options.get("margin", 0)}px;" - /> - <script type="module"> - import {{ mountWidgetExample }} from "{_REACTPY_STATIC_HOST}/_static/custom.js"; - mountWidgetExample( - "{container_id}", - "{view_id}", - "{_REACTPY_EXAMPLE_HOST}", - {"true" if "activate-button" in self.options else "false"}, - ); - </script> - </div> - """, - format="html", - ) - ] - - -def setup(app: Sphinx) -> None: - app.add_directive("reactpy-view", IteractiveWidget) diff --git a/docs/source/_static/css/furo-theme-overrides.css b/docs/source/_static/css/furo-theme-overrides.css deleted file mode 100644 index a258e025e..000000000 --- a/docs/source/_static/css/furo-theme-overrides.css +++ /dev/null @@ -1,6 +0,0 @@ -.sidebar-container { - width: 18em; -} -.sidebar-brand-text { - display: none; -} diff --git a/docs/source/_static/css/larger-api-margins.css b/docs/source/_static/css/larger-api-margins.css deleted file mode 100644 index f8b75d592..000000000 --- a/docs/source/_static/css/larger-api-margins.css +++ /dev/null @@ -1,7 +0,0 @@ -:is(.data, .function, .class, .exception).py { - margin-top: 3em; -} - -:is(.attribute, .method).py { - margin-top: 1.8em; -} diff --git a/docs/source/_static/css/larger-headings.css b/docs/source/_static/css/larger-headings.css deleted file mode 100644 index 297ab7202..000000000 --- a/docs/source/_static/css/larger-headings.css +++ /dev/null @@ -1,9 +0,0 @@ -h1, -h2, -h3, -h4, -h5, -h6 { - margin-top: 1.5em !important; - font-weight: 900 !important; -} diff --git a/docs/source/_static/css/reactpy-view.css b/docs/source/_static/css/reactpy-view.css deleted file mode 100644 index 56df74970..000000000 --- a/docs/source/_static/css/reactpy-view.css +++ /dev/null @@ -1,43 +0,0 @@ -.interactive { - -webkit-transition: 0.1s ease-out; - -moz-transition: 0.1s ease-out; - -o-transition: 0.1s ease-out; - transition: 0.1s ease-out; -} -.widget-container { - padding: 15px; - overflow: auto; - background-color: var(--color-code-background); - min-height: 75px; -} - -.widget-container .printout { - margin-top: 20px; - border-top: solid 2px var(--color-foreground-border); - padding-top: 20px; -} - -.widget-container > div { - width: 100%; -} - -.enable-widget-button { - padding: 10px; - color: #ffffff !important; - text-transform: uppercase; - text-decoration: none; - background: #526cfe; - border: 2px solid #526cfe !important; - transition: all 0.1s ease 0s; - box-shadow: 0 5px 10px var(--color-foreground-border); -} -.enable-widget-button:hover { - color: #526cfe !important; - background: #ffffff; - transition: all 0.1s ease 0s; -} -.enable-widget-button:focus { - outline: 0 !important; - transform: scale(0.98); - transition: all 0.1s ease 0s; -} diff --git a/docs/source/_static/css/sphinx-design-overrides.css b/docs/source/_static/css/sphinx-design-overrides.css deleted file mode 100644 index 767d9d16c..000000000 --- a/docs/source/_static/css/sphinx-design-overrides.css +++ /dev/null @@ -1,14 +0,0 @@ -.sd-card-body { - display: flex; - flex-direction: column; - align-items: stretch; -} - -.sd-tab-content .highlight pre { - max-height: 700px; - overflow: auto; -} - -.sd-card-title .sd-badge { - font-size: 1em; -} diff --git a/docs/source/_static/css/widget-output-css-overrides.css b/docs/source/_static/css/widget-output-css-overrides.css deleted file mode 100644 index 7ddf1a792..000000000 --- a/docs/source/_static/css/widget-output-css-overrides.css +++ /dev/null @@ -1,8 +0,0 @@ -.widget-container h1, -.widget-container h2, -.widget-container h3, -.widget-container h4, -.widget-container h5, -.widget-container h6 { - margin: 0 !important; -} diff --git a/docs/source/_static/install-and-run-reactpy.gif b/docs/source/_static/install-and-run-reactpy.gif deleted file mode 100644 index 49d431341..000000000 Binary files a/docs/source/_static/install-and-run-reactpy.gif and /dev/null differ diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst deleted file mode 100644 index 97eea0bfc..000000000 --- a/docs/source/about/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -.. THIS CHANGELOG HAS BEEN DEPRECATED. SEE CHANGELOG.md FILE INSTEAD. --- diff --git a/docs/source/about/contributor-guide.rst b/docs/source/about/contributor-guide.rst deleted file mode 100644 index 45ce4a512..000000000 --- a/docs/source/about/contributor-guide.rst +++ /dev/null @@ -1,161 +0,0 @@ -Contributor Guide -================= - -Creating a development environment ----------------------------------- - -If you plan to make code changes to this repository, you will need to install the following dependencies first: - -- `Git <https://www.python.org/downloads/>`__ -- `Python 3.9+ <https://www.python.org/downloads/>`__ -- `Hatch <https://hatch.pypa.io/latest/>`__ -- `Bun <https://bun.sh/>`__ -- `Docker <https://docs.docker.com/get-docker/>`__ (optional) - -Once you finish installing these dependencies, you can clone this repository: - -.. code-block:: bash - - git clone https://github.com/reactive-python/reactpy.git - cd reactpy - -Executing test environment commands ------------------------------------ - -By utilizing ``hatch``, the following commands are available to manage the development environment. - -Python Tests -............ - -.. list-table:: - :header-rows: 1 - - * - Command - - Description - * - ``hatch test`` - - Run Python tests using the current environment's Python version - * - ``hatch test --all`` - - Run tests using all compatible Python versions - * - ``hatch test --python 3.9`` - - Run tests using a specific Python version - * - ``hatch test -k test_use_connection`` - - Run only a specific test - -Python Package -.............. - -.. list-table:: - :header-rows: 1 - - * - Command - - Description - * - ``hatch fmt`` - - Run all linters and formatters - * - ``hatch fmt --check`` - - Run all linters and formatters, but do not save fixes to the disk - * - ``hatch fmt --linter`` - - Run only linters - * - ``hatch fmt --formatter`` - - Run only formatters - * - ``hatch run python:type_check`` - - Run the Python type checker - -JavaScript Packages -................... - -.. list-table:: - :header-rows: 1 - - * - Command - - Description - * - ``hatch run javascript:check`` - - Run the JavaScript linter/formatter - * - ``hatch run javascript:fix`` - - Run the JavaScript linter/formatter and write fixes to disk - * - ``hatch run javascript:test`` - - Run the JavaScript tests - * - ``hatch run javascript:build`` - - Build all JavaScript packages - * - ``hatch run javascript:build_event_to_object`` - - Build the ``event-to-object`` package - * - ``hatch run javascript:build_client`` - - Build the ``@reactpy/client`` package - * - ``hatch run javascript:build_app`` - - Build the ``@reactpy/app`` package - -Documentation -............. - -.. list-table:: - :header-rows: 1 - - * - Command - - Description - * - ``hatch run docs:serve`` - - Start the documentation preview webserver - * - ``hatch run docs:build`` - - Build the documentation - * - ``hatch run docs:check`` - - Check the documentation for build errors - * - ``hatch run docs:docker_serve`` - - Start the documentation preview webserver using Docker - * - ``hatch run docs:docker_build`` - - Build the documentation using Docker - -Environment Management -...................... - -.. list-table:: - :header-rows: 1 - - * - Command - - Description - * - ``hatch build --clean`` - - Build the package from source - * - ``hatch env prune`` - - Delete all virtual environments created by ``hatch`` - * - ``hatch python install 3.12`` - - Install a specific Python version to your system - -Other ReactPy Repositories --------------------------- - -ReactPy has several external packages that can be installed to enhance your user experience. For documentation on them -you should refer to their respective documentation in the links below: - -- `reactpy-router <https://github.com/reactive-python/reactpy-router>`__ - ReactPy support for URL - routing -- `reactpy-js-component-template - <https://github.com/reactive-python/reactpy-js-component-template>`__ - Template repo - for making :ref:`Custom Javascript Components`. -- `reactpy-django <https://github.com/reactive-python/reactpy-django>`__ - ReactPy integration for - Django -- `reactpy-jupyter <https://github.com/reactive-python/reactpy-jupyter>`__ - ReactPy integration for - Jupyter - -.. Links -.. ===== - -.. _Hatch: https://hatch.pypa.io/ -.. _Invoke: https://www.pyinvoke.org/ -.. _Google Chrome: https://www.google.com/chrome/ -.. _Docker: https://docs.docker.com/get-docker/ -.. _Git: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git -.. _Git Bash: https://gitforwindows.org/ -.. _NPM: https://www.npmjs.com/get-npm -.. _PyPI: https://pypi.org/project/reactpy -.. _pip: https://pypi.org/project/pip/ -.. _PyTest: pytest <https://docs.pytest.org -.. _Playwright: https://playwright.dev/python/ -.. _React: https://reactjs.org/ -.. _Heroku: https://www.heroku.com/what -.. _GitHub Actions: https://github.com/features/actions -.. _pre-commit: https://pre-commit.com/ -.. _GitHub Flow: https://guides.github.com/introduction/flow/ -.. _MyPy: http://mypy-lang.org/ -.. _Black: https://github.com/psf/black -.. _Flake8: https://flake8.pycqa.org/en/latest/ -.. _Ruff: https://github.com/charliermarsh/ruff -.. _UVU: https://github.com/lukeed/uvu -.. _Prettier: https://prettier.io/ -.. _ESLint: https://eslint.org/ diff --git a/docs/source/about/credits-and-licenses.rst b/docs/source/about/credits-and-licenses.rst deleted file mode 100644 index bc66cb11f..000000000 --- a/docs/source/about/credits-and-licenses.rst +++ /dev/null @@ -1,16 +0,0 @@ -Credits and Licenses -==================== - -Much of this documentation, including its layout and content, was created with heavy -influence from https://reactjs.org which uses the `Creative Commons Attribution 4.0 -International -<https://raw.githubusercontent.com/reactjs/reactjs.org/b2d5613b6ae20855ced7c83067b604034bebbb44/LICENSE-DOCS.md>`__ -license. While many things have been transformed, we paraphrase and, in some places, -copy language or examples where ReactPy's behavior mirrors that of React's. - - -Source Code License -------------------- - -.. literalinclude:: ../../../LICENSE - :language: text diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 08addad8d..000000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,330 +0,0 @@ -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config - -import sys -from doctest import DONT_ACCEPT_TRUE_FOR_1, ELLIPSIS, NORMALIZE_WHITESPACE -from pathlib import Path - -# -- Path Setup -------------------------------------------------------------- - -THIS_DIR = Path(__file__).parent -ROOT_DIR = THIS_DIR.parent.parent -DOCS_DIR = THIS_DIR.parent - -# extension path -sys.path.insert(0, str(DOCS_DIR)) -sys.path.insert(0, str(THIS_DIR / "_exts")) - - -# -- Project information ----------------------------------------------------- - -project = "ReactPy" -title = "ReactPy" -description = ( - "ReactPy is a Python web framework for building interactive websites without needing " - "a single line of Javascript. It can be run standalone, in a Jupyter Notebook, or " - "as part of an existing application." -) -copyright = "2023, Ryan Morshead" # noqa: A001 -author = "Ryan Morshead" - -# -- Common External Links --------------------------------------------------- - -extlinks = { - "issue": ( - "https://github.com/reactive-python/reactpy/issues/%s", - "#%s", - ), - "pull": ( - "https://github.com/reactive-python/reactpy/pull/%s", - "#%s", - ), - "discussion": ( - "https://github.com/reactive-python/reactpy/discussions/%s", - "#%s", - ), - "discussion-type": ( - "https://github.com/reactive-python/reactpy/discussions/categories/%s", - "%s", - ), - "commit": ( - "https://github.com/reactive-python/reactpy/commit/%s", - "%s", - ), -} -extlinks_detect_hardcoded_links = True - - -# -- General configuration --------------------------------------------------- - -# If your documentatirston needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", - "sphinx.ext.coverage", - "sphinx.ext.viewcode", - "sphinx.ext.napoleon", - "sphinx.ext.extlinks", - # third party extensions - "sphinx_copybutton", - "sphinx_reredirects", - "sphinx_design", - "sphinxext.opengraph", - # custom extensions - "async_doctest", - "autogen_api_docs", - "copy_vdom_json_schema", - "reactpy_view", - "patched_html_translator", - "reactpy_example", - "build_custom_js", - "custom_autosectionlabel", -] - -# Add any paths that contain templates here, relative to this directory. -# templates_path = ["templates"] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = ".rst" - -# The master toctree document. -master_doc = "index" - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "en" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [ - "_custom_js", -] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = None - -# The default language to highlight source code in. -highlight_language = "python3" - -# Controls how sphinx.ext.autodoc represents typehints in the function signature -autodoc_typehints = "description" - -# -- Doc Test Configuration ------------------------------------------------------- - -doctest_default_flags = NORMALIZE_WHITESPACE | ELLIPSIS | DONT_ACCEPT_TRUE_FOR_1 - -# -- Extension Configuration ------------------------------------------------------ - - -# -- sphinx.ext.autosectionlabel --- - -autosectionlabel_skip_docs = ["_auto/apis"] - - -# -- sphinx.ext.autodoc -- - -# show base classes for autodoc -autodoc_default_options = { - "show-inheritance": True, - "member-order": "bysource", -} -# order autodoc members by their order in the source -autodoc_member_order = "bysource" - - -# -- sphinx_reredirects -- - -redirects = { - "package-api": "_autogen/user-apis.html", - "configuration-options": "_autogen/dev-apis.html#configuration-options", - "examples": "creating-interfaces/index.html", -} - - -# -- sphinxext.opengraph -- - -ogp_site_url = "https://reactpy.dev/" -ogp_image = "https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/png/reactpy-logo-landscape-padded.png" -# We manually specify this below -# ogp_description_length = 200 -ogp_type = "website" -ogp_custom_meta_tags = [ - # Open Graph Meta Tags - f'<meta property="og:title" content="{title}">', - f'<meta property="og:description" content="{description}">', - # Twitter Meta Tags - '<meta name="twitter:card" content="summary_large_image">', - '<meta name="twitter:creator" content="@rmorshea">', - '<meta name="twitter:site" content="@rmorshea">', -] - - -# -- Options for HTML output ------------------------------------------------- - -# Set the page title -html_title = title - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "furo" -html_logo = str(ROOT_DIR / "branding" / "svg" / "reactpy-logo-landscape.svg") -html_favicon = str(ROOT_DIR / "branding" / "ico" / "reactpy-logo.ico") - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. - -html_theme_options = { - "light_css_variables": { - # furo - "admonition-title-font-size": "1rem", - "admonition-font-size": "1rem", - # sphinx-design - "sd-color-info": "var(--color-admonition-title-background--note)", - "sd-color-warning": "var(--color-admonition-title-background--warning)", - "sd-color-danger": "var(--color-admonition-title-background--danger)", - "sd-color-info-text": "var(--color-admonition-title--note)", - "sd-color-warning-text": "var(--color-admonition-title--warning)", - "sd-color-danger-text": "var(--color-admonition-title--danger)", - }, -} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -# These paths are either relative to html_static_path -# or fully qualified paths (eg. https://...) -css_dir = THIS_DIR / "_static" / "css" -html_css_files = [ - str(p.relative_to(THIS_DIR / "_static")) for p in css_dir.glob("*.css") -] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -# html_sidebars = {} - - -# -- Options for Sphinx Panels ----------------------------------------------- - -panels_css_variables = { - "tabs-color-label-active": "rgb(106, 176, 221)", - "tabs-color-label-inactive": "rgb(201, 225, 250)", - "tabs-color-overline": "rgb(201, 225, 250)", - "tabs-color-underline": "rgb(201, 225, 250)", -} - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = "ReactPydoc" - - -# -- Options for LaTeX output ------------------------------------------------ - -# latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -# -# 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -# -# 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -# -# 'preamble': '', -# Latex figure (float) alignment -# -# 'figure_align': 'htbp', -# } - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [(master_doc, "ReactPy.tex", html_title, "Ryan Morshead", "manual")] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "reactpy", html_title, [author], 1)] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "ReactPy", - html_title, - author, - "ReactPy", - "One line description of project.", - "Miscellaneous", - ) -] - -# -- Options for Sphinx-Autodoc-Typehints output ------------------------------------------------- - -set_type_checking_flag = False - -# -- Options for Epub output ------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ["search.html"] - -# -- Options for intersphinx extension --------------------------------------- - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "pyalect": ("https://pyalect.readthedocs.io/en/latest", None), - "sanic": ("https://sanic.readthedocs.io/en/latest/", None), - "tornado": ("https://www.tornadoweb.org/en/stable/", None), - "flask": ("https://flask.palletsprojects.com/en/1.1.x/", None), -} - -# -- Options for todo extension ---------------------------------------------- - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/data.json b/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/data.json deleted file mode 100644 index b1315912d..000000000 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/data.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "name": "Homenaje a la Neurocirugía", - "artist": "Marta Colvin Andrade", - "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg", - "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips." - }, - { - "name": "Eternal Presence", - "artist": "John Woodrow Wilson", - "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"", - "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg", - "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity." - }, - { - "name": "Moai", - "artist": "Unknown Artist", - "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.", - "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG", - "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces." - }, - { - "name": "Blue Nana", - "artist": "Niki de Saint Phalle", - "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg", - "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy." - }, - { - "name": "Cavaliere", - "artist": "Lamidi Olonade Fakeye", - "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.", - "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg", - "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns." - }, - { - "name": "Big Bellies", - "artist": "Alina Szapocznikow", - "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG", - "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures." - }, - { - "name": "Terracotta Army", - "artist": "Unknown Artist", - "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg", - "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor." - }, - { - "name": "Lunar Landscape", - "artist": "Louise Nevelson", - "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg", - "alt": "A black matte sculpture where the individual elements are initially indistinguishable." - }, - { - "name": "Aureole", - "artist": "Ranjani Shettar", - "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg", - "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light." - }, - { - "name": "Hippos", - "artist": "Taipei Zoo", - "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg", - "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming." - } -] diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/main.py b/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/main.py deleted file mode 100644 index a919a2354..000000000 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/main.py +++ /dev/null @@ -1,35 +0,0 @@ -import json -from pathlib import Path - -from reactpy import component, hooks, html, run - -HERE = Path(__file__) -DATA_PATH = HERE.parent / "data.json" -sculpture_data = json.loads(DATA_PATH.read_text()) - - -@component -def Gallery(): - index, set_index = hooks.use_state(0) - - def handle_click(event): - set_index(index + 1) - - bounded_index = index % len(sculpture_data) - sculpture = sculpture_data[bounded_index] - alt = sculpture["alt"] - artist = sculpture["artist"] - description = sculpture["description"] - name = sculpture["name"] - url = sculpture["url"] - - return html.div( - html.button({"on_click": handle_click}, "Next"), - html.h2(name, " by ", artist), - html.p(f"({bounded_index + 1} of {len(sculpture_data)})"), - html.img({"src": url, "alt": alt, "style": {"height": "200px"}}), - html.p(description), - ) - - -run(Gallery) diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/data.json b/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/data.json deleted file mode 100644 index b1315912d..000000000 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/data.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "name": "Homenaje a la Neurocirugía", - "artist": "Marta Colvin Andrade", - "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg", - "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips." - }, - { - "name": "Eternal Presence", - "artist": "John Woodrow Wilson", - "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"", - "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg", - "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity." - }, - { - "name": "Moai", - "artist": "Unknown Artist", - "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.", - "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG", - "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces." - }, - { - "name": "Blue Nana", - "artist": "Niki de Saint Phalle", - "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg", - "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy." - }, - { - "name": "Cavaliere", - "artist": "Lamidi Olonade Fakeye", - "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.", - "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg", - "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns." - }, - { - "name": "Big Bellies", - "artist": "Alina Szapocznikow", - "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG", - "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures." - }, - { - "name": "Terracotta Army", - "artist": "Unknown Artist", - "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg", - "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor." - }, - { - "name": "Lunar Landscape", - "artist": "Louise Nevelson", - "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg", - "alt": "A black matte sculpture where the individual elements are initially indistinguishable." - }, - { - "name": "Aureole", - "artist": "Ranjani Shettar", - "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg", - "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light." - }, - { - "name": "Hippos", - "artist": "Taipei Zoo", - "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg", - "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming." - } -] diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/main.py b/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/main.py deleted file mode 100644 index d07b87140..000000000 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/main.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -from pathlib import Path - -from reactpy import component, hooks, html, run - -HERE = Path(__file__) -DATA_PATH = HERE.parent / "data.json" -sculpture_data = json.loads(DATA_PATH.read_text()) - - -@component -def Gallery(): - index, set_index = hooks.use_state(0) - show_more, set_show_more = hooks.use_state(False) - - def handle_next_click(event): - set_index(index + 1) - - def handle_more_click(event): - set_show_more(not show_more) - - bounded_index = index % len(sculpture_data) - sculpture = sculpture_data[bounded_index] - alt = sculpture["alt"] - artist = sculpture["artist"] - description = sculpture["description"] - name = sculpture["name"] - url = sculpture["url"] - - return html.div( - html.button({"on_click": handle_next_click}, "Next"), - html.h2(name, " by ", artist), - html.p(f"({bounded_index + 1} or {len(sculpture_data)})"), - html.img({"src": url, "alt": alt, "style": {"height": "200px"}}), - html.div( - html.button( - {"on_click": handle_more_click}, - f"{('Show' if show_more else 'Hide')} details", - ), - (html.p(description) if show_more else ""), - ), - ) - - -@component -def App(): - return html.div( - html.section({"style": {"width": "50%", "float": "left"}}, Gallery()), - html.section({"style": {"width": "50%", "float": "left"}}, Gallery()), - ) - - -run(App) diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/data.json b/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/data.json deleted file mode 100644 index b1315912d..000000000 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/data.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "name": "Homenaje a la Neurocirugía", - "artist": "Marta Colvin Andrade", - "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg", - "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips." - }, - { - "name": "Eternal Presence", - "artist": "John Woodrow Wilson", - "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"", - "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg", - "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity." - }, - { - "name": "Moai", - "artist": "Unknown Artist", - "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.", - "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG", - "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces." - }, - { - "name": "Blue Nana", - "artist": "Niki de Saint Phalle", - "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg", - "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy." - }, - { - "name": "Cavaliere", - "artist": "Lamidi Olonade Fakeye", - "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.", - "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg", - "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns." - }, - { - "name": "Big Bellies", - "artist": "Alina Szapocznikow", - "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG", - "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures." - }, - { - "name": "Terracotta Army", - "artist": "Unknown Artist", - "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg", - "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor." - }, - { - "name": "Lunar Landscape", - "artist": "Louise Nevelson", - "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg", - "alt": "A black matte sculpture where the individual elements are initially indistinguishable." - }, - { - "name": "Aureole", - "artist": "Ranjani Shettar", - "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg", - "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light." - }, - { - "name": "Hippos", - "artist": "Taipei Zoo", - "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg", - "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming." - } -] diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/main.py b/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/main.py deleted file mode 100644 index 87f9651be..000000000 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/main.py +++ /dev/null @@ -1,45 +0,0 @@ -import json -from pathlib import Path - -from reactpy import component, hooks, html, run - -HERE = Path(__file__) -DATA_PATH = HERE.parent / "data.json" -sculpture_data = json.loads(DATA_PATH.read_text()) - - -@component -def Gallery(): - index, set_index = hooks.use_state(0) - show_more, set_show_more = hooks.use_state(False) - - def handle_next_click(event): - set_index(index + 1) - - def handle_more_click(event): - set_show_more(not show_more) - - bounded_index = index % len(sculpture_data) - sculpture = sculpture_data[bounded_index] - alt = sculpture["alt"] - artist = sculpture["artist"] - description = sculpture["description"] - name = sculpture["name"] - url = sculpture["url"] - - return html.div( - html.button({"on_click": handle_next_click}, "Next"), - html.h2(name, " by ", artist), - html.p(f"({bounded_index + 1} or {len(sculpture_data)})"), - html.img({"src": url, "alt": alt, "style": {"height": "200px"}}), - html.div( - html.button( - {"on_click": handle_more_click}, - f"{('Show' if show_more else 'Hide')} details", - ), - (html.p(description) if show_more else ""), - ), - ) - - -run(Gallery) diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/data.json b/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/data.json deleted file mode 100644 index b1315912d..000000000 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/data.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "name": "Homenaje a la Neurocirugía", - "artist": "Marta Colvin Andrade", - "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg", - "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips." - }, - { - "name": "Eternal Presence", - "artist": "John Woodrow Wilson", - "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"", - "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg", - "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity." - }, - { - "name": "Moai", - "artist": "Unknown Artist", - "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.", - "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG", - "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces." - }, - { - "name": "Blue Nana", - "artist": "Niki de Saint Phalle", - "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg", - "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy." - }, - { - "name": "Cavaliere", - "artist": "Lamidi Olonade Fakeye", - "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.", - "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg", - "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns." - }, - { - "name": "Big Bellies", - "artist": "Alina Szapocznikow", - "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG", - "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures." - }, - { - "name": "Terracotta Army", - "artist": "Unknown Artist", - "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg", - "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor." - }, - { - "name": "Lunar Landscape", - "artist": "Louise Nevelson", - "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg", - "alt": "A black matte sculpture where the individual elements are initially indistinguishable." - }, - { - "name": "Aureole", - "artist": "Ranjani Shettar", - "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg", - "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light." - }, - { - "name": "Hippos", - "artist": "Taipei Zoo", - "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg", - "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming." - } -] diff --git a/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/main.py b/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/main.py deleted file mode 100644 index c617586de..000000000 --- a/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/main.py +++ /dev/null @@ -1,42 +0,0 @@ -# flake8: noqa -# errors F841,F823 for `index = index + 1` inside the closure - -# :lines: 7- -# :linenos: - -import json -from pathlib import Path - -from reactpy import component, html, run - - -HERE = Path(__file__) -DATA_PATH = HERE.parent / "data.json" -sculpture_data = json.loads(DATA_PATH.read_text()) - - -@component -def Gallery(): - index = 0 - - def handle_click(event): - index = index + 1 - - bounded_index = index % len(sculpture_data) - sculpture = sculpture_data[bounded_index] - alt = sculpture["alt"] - artist = sculpture["artist"] - description = sculpture["description"] - name = sculpture["name"] - url = sculpture["url"] - - return html.div( - html.button({"on_click": handle_click}, "Next"), - html.h2(name, " by ", artist), - html.p(f"({bounded_index + 1} or {len(sculpture_data)})"), - html.img({"src": url, "alt": alt, "style": {"height": "200px"}}), - html.p(description), - ) - - -run(Gallery) diff --git a/docs/source/guides/adding-interactivity/components-with-state/index.rst b/docs/source/guides/adding-interactivity/components-with-state/index.rst deleted file mode 100644 index f8235ac0d..000000000 --- a/docs/source/guides/adding-interactivity/components-with-state/index.rst +++ /dev/null @@ -1,351 +0,0 @@ -Components With State -===================== - -Components often need to change what’s on the screen as a result of an interaction. For -example, typing into the form should update the input field, clicking “next” on an image -carousel should change which image is displayed, clicking “buy” should put a product in -the shopping cart. Components need to “remember” things like the current input value, -the current image, the shopping cart. In ReactPy, this kind of component-specific memory is -called state. - - -When Variables Aren't Enough ----------------------------- - -Below is a gallery of images about sculpture. Clicking the "Next" button should -increment the ``index`` and, as a result, change what image is displayed. However, this -does not work: - -.. reactpy:: _examples/when_variables_are_not_enough - -.. note:: - - Try clicking the button to see that it does not cause a change. - -After clicking "Next", if you check your web server's logs, you'll discover an -``UnboundLocalError`` error. It turns out that in this case, the ``index = index + 1`` -statement is similar to `trying to set global variables -<https://stackoverflow.com/questions/9264763/dont-understand-why-unboundlocalerror-occurs-closure>`__. -Technically there's a way to `fix this error -<https://docs.python.org/3/reference/simple_stmts.html#nonlocal>`__, but even if we did, -that still wouldn't fix the underlying problems: - -1. **Local variables do not persist across component renders** - when a component is - updated, its associated function gets called again. That is, it renders. As a result, - all the local state that was created the last time the function was called gets - destroyed when it updates. - -2. **Changes to local variables do not cause components to re-render** - there's no way - for ReactPy to observe when these variables change. Thus ReactPy is not aware that - something has changed and that a re-render should take place. - -To address these problems, ReactPy provides the :func:`~reactpy.core.hooks.use_state` "hook" -which provides: - -1. A **state variable** whose data is retained across renders. - -2. A **state setter** function that can be used to update that variable and trigger a - render. - - -Adding State to Components --------------------------- - -To create a state variable and state setter with :func:`~reactpy.core.hooks.use_state` hook -as described above, we'll begin by importing it: - -.. testcode:: - - from reactpy import use_state - -Then we'll make the following changes to our code :ref:`from before <When Variables -Aren't Enough>`: - -.. code-block:: diff - - - index = 0 - + index, set_index = use_state - - def handle_click(event): - - index = index + 1 - + set_index(index + 1) - -After making those changes we should get: - -.. code-block:: - :linenos: - :lineno-start: 14 - - index, set_index = use_state(0) - - def handle_click(event): - set_index(index + 1) - -We'll talk more about what this is doing :ref:`shortly <your first hook>`, but for -now let's just verify that this does in fact fix the problems from before: - -.. reactpy:: _examples/adding_state_variable - - -Your First Hook ---------------- - -In ReactPy, ``use_state``, as well as any other function whose name starts with ``use``, is -called a "hook". These are special functions that should only be called while ReactPy is -:ref:`rendering <the rendering process>`. They let you "hook into" the different -capabilities of ReactPy's components of which ``use_state`` is just one (well get into the -other :ref:`later <managing state>`). - -While hooks are just normal functions, but it's helpful to think of them as -:ref:`unconditioned <rules of hooks>` declarations about a component's needs. In other -words, you'll "use" hooks at the top of your component in the same way you might -"import" modules at the top of your Python files. - - -.. _Introduction to use_state: - -Introduction to ``use_state`` ------------------------------ - -When you call :func:`~reactpy.core.hooks.use_state` inside the body of a component's render -function, you're declaring that this component needs to remember something. That -"something" which needs to be remembered, is known as **state**. So when we look at an -assignment expression like the one below - -.. code-block:: - - index, set_index = use_state(0) - -we should read it as saying that ``index`` is a piece of state which must be -remembered by the component that declared it. The argument to ``use_state`` (in this -case ``0``) is then conveying what the initial value for ``index`` is. - -We should then understand that each time the component which owns this state renders -``use_state`` will return a tuple containing two values - the current value of the state -(``index``) and a function to change that value the next time the component is rendered. -Thus, in this example: - -- ``index`` - is a **state variable** containing the currently stored value. -- ``set_index`` - is a **state setter** for changing that value and triggering a re-render - of the component. - -The convention is that, if you name your state variable ``thing``, your state setter -should be named ``set_thing``. While you could name them anything you want, adhering to -the convention makes things easier to understand across projects. - ----- - -To understand how this works in context, let's break down our example by examining key -moments in the execution of the ``Gallery`` component. Each numbered tab in the section -below highlights a line of code where something of interest occurs: - -.. hint:: - - Try clicking through the numbered tabs to each highlighted step of execution - -.. tab-set:: - - .. tab-item:: 1 - - .. raw:: html - - <h2>Initial render</h2> - - .. literalinclude:: _examples/adding_state_variable/main.py - :lines: 12-33 - :emphasize-lines: 2 - - At this point, we've just begun to render the ``Gallery`` component. As yet, - ReactPy is not aware that this component has any state or what view it will - display. This will change in a moment though when we move to the next line... - - .. tab-item:: 2 - - .. raw:: html - - <h2>Initial state declaration</h2> - - .. literalinclude:: _examples/adding_state_variable/main.py - :lines: 12-33 - :emphasize-lines: 3 - - The ``Gallery`` component has just declared some state. ReactPy now knows that it - must remember the ``index`` and trigger an update of this component when - ``set_index`` is called. Currently the value of ``index`` is ``0`` as per the - default value given to ``use_state``. Thus, the resulting view will display - information about the first item in our ``sculpture_data`` list. - - .. tab-item:: 3 - - .. raw:: html - - <h2>Define event handler</h2> - - .. literalinclude:: _examples/adding_state_variable/main.py - :lines: 12-33 - :emphasize-lines: 5 - - We've now defined an event handler that we intend to assign to a button in the - view. This will respond once the user clicks that button. The action this - handler performs is to update the value of ``index`` and schedule our ``Gallery`` - component to update. - - .. tab-item:: 4 - - .. raw:: html - - <h2>Return the view</h2> - - .. literalinclude:: _examples/adding_state_variable/main.py - :lines: 12-33 - :emphasize-lines: 16 - - The ``handle_click`` function we defined above has now been assigned to a button - in the view and we are about to display information about the first item in out - ``sculpture_data`` list. When the view is ultimately displayed, if a user clicks - the "Next" button, the handler we just assigned will be triggered. Until that - point though, the application will remain static. - - .. tab-item:: 5 - - .. raw:: html - - <h2>User interaction</h2> - - .. literalinclude:: _examples/adding_state_variable/main.py - :lines: 12-33 - :emphasize-lines: 5 - - A user has just clicked the button 🖱️! ReactPy has sent information about the event - to the ``handle_click`` function and it is about to execute. In a moment we will - update the state of this component and schedule a re-render. - - .. tab-item:: 6 - - .. raw:: html - - <h2>New state is set</h2> - - .. literalinclude:: _examples/adding_state_variable/main.py - :lines: 12-33 - :emphasize-lines: 6 - - We've just now told ReactPy that we want to update the state of our ``Gallery`` and - that it needs to be re-rendered. More specifically, we are incrementing its - ``index``, and once ``Gallery`` re-renders the index *will* be ``1``. - Importantly, at this point, the value of ``index`` is still ``0``! This will - only change once the component begins to re-render. - - .. tab-item:: 7 - - .. raw:: html - - <h2>Next render begins</h2> - - .. literalinclude:: _examples/adding_state_variable/main.py - :lines: 12-33 - :emphasize-lines: 2 - - The scheduled re-render of ``Gallery`` has just begun. ReactPy has now updated its - internal state store such that, the next time we call ``use_state`` we will get - back the updated value of ``index``. - - .. tab-item:: 8 - - .. raw:: html - - <h2>Next state is acquired</h2> - - .. literalinclude:: _examples/adding_state_variable/main.py - :lines: 12-33 - :emphasize-lines: 3 - - With ReactPy's state store updated, as we call ``use_state``, instead of returning - ``0`` for the value of ``index`` as it did before, ReactPy now returns the value - ``1``. With this change the view we display will be altered - instead of - displaying data for the first item in our ``sculpture_data`` list we will now - display information about the second. - - .. tab-item:: 9 - - .. raw:: html - - <h2>Repeat...</h2> - - .. literalinclude:: _examples/adding_state_variable/main.py - :lines: 12-33 - - From this point on, the steps remain the same. The only difference being the - progressively incrementing ``index`` each time the user clicks the "Next" button - and the view which is altered to to reflect the currently indexed item in the - ``sculpture_data`` list. - - .. note:: - - Once we reach the end of the ``sculpture_data`` list the view will return - back to the first item since we create a ``bounded_index`` by doing a modulo - of the index with the length of the list (``index % len(sculpture_data)``). - Ideally we would do this bounding at the time we call ``set_index`` to - prevent ``index`` from incrementing to infinity, but to keep things simple - in this examples, we've kept this logic separate. - - -Multiple State Declarations ---------------------------- - -The powerful thing about hooks like :func:`~reactpy.core.hooks.use_state` is that you're -not limited to just one state declaration. You can call ``use_state()`` as many times as -you need to in one component. For example, in the example below we've added a -``show_more`` state variable along with a few other modifications (e.g. renaming -``handle_click``) to make the description for each sculpture optionally displayed. Only -when the user clicks the "Show details" button is this description shown: - -.. reactpy:: _examples/multiple_state_variables - -It's generally a good idea to define separate state variables if the data they represent -is unrelated. In this case, ``index`` corresponds to what sculpture information is being -displayed and ``show_more`` is solely concerned with whether the description for a given -sculpture is shown. Put other way ``index`` is concerned with *what* information is -displayed while ``show_more`` is concerned with *how* it is displayed. Conversely -though, if you have a form with many fields, it probably makes sense to have a single -object that holds the data for all the fields rather than an object per-field. - -.. note:: - - This topic is discussed more in the :ref:`structuring your state` section. - - -State is Isolated and Private ------------------------------ - -State is local to a component instance on the screen. In other words, if you render the -same component twice, each copy will have completely isolated state! Changing one of -them will not affect the other. - -In this example, the ``Gallery`` component from earlier is rendered twice with no -changes to its logic. Try clicking the buttons inside each of the galleries. Notice that -their state is independent: - -.. reactpy:: _examples/isolated_state - :result-is-default-tab: - -This is what makes state different from regular variables that you might declare at the -top of your module. State is not tied to a particular function call or a place in the -code, but it’s “local” to the specific place on the screen. You rendered two ``Gallery`` -components, so their state is stored separately. - -Also notice how the Page component doesn’t “know” anything about the Gallery state or -even whether it has any. Unlike props, state is fully private to the component declaring -it. The parent component can’t change it. This lets you add state to any component or -remove it without impacting the rest of the components. - -.. card:: - :link: /guides/managing-state/sharing-component-state/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - What if you wanted both galleries to keep their states in sync? The right way to do - it in ReactPy is to remove state from child components and add it to their closest - shared parent. diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_remove.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_remove.py deleted file mode 100644 index 6c3c783da..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_remove.py +++ /dev/null @@ -1,58 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def Definitions(): - term_to_add, set_term_to_add = use_state(None) - definition_to_add, set_definition_to_add = use_state(None) - all_terms, set_all_terms = use_state({}) - - def handle_term_to_add_change(event): - set_term_to_add(event["target"]["value"]) - - def handle_definition_to_add_change(event): - set_definition_to_add(event["target"]["value"]) - - def handle_add_click(event): - if term_to_add and definition_to_add: - set_all_terms({**all_terms, term_to_add: definition_to_add}) - set_term_to_add(None) - set_definition_to_add(None) - - def make_delete_click_handler(term_to_delete): - def handle_click(event): - set_all_terms({t: d for t, d in all_terms.items() if t != term_to_delete}) - - return handle_click - - return html.div( - html.button({"on_click": handle_add_click}, "add term"), - html.label( - "Term: ", - html.input({"value": term_to_add, "on_change": handle_term_to_add_change}), - ), - html.label( - "Definition: ", - html.input( - { - "value": definition_to_add, - "on_change": handle_definition_to_add_change, - } - ), - ), - html.hr(), - [ - html.div( - {"key": term}, - html.button( - {"on_click": make_delete_click_handler(term)}, "delete term" - ), - html.dt(term), - html.dd(definition), - ) - for term, definition in all_terms.items() - ], - ) - - -run(Definitions) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_update.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_update.py deleted file mode 100644 index 32dd4073a..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_update.py +++ /dev/null @@ -1,44 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def Form(): - person, set_person = use_state( - { - "first_name": "Barbara", - "last_name": "Hepworth", - "email": "bhepworth@sculpture.com", - } - ) - - def handle_first_name_change(event): - set_person({**person, "first_name": event["target"]["value"]}) - - def handle_last_name_change(event): - set_person({**person, "last_name": event["target"]["value"]}) - - def handle_email_change(event): - set_person({**person, "email": event["target"]["value"]}) - - return html.div( - html.label( - "First name: ", - html.input( - {"value": person["first_name"], "on_change": handle_first_name_change} - ), - ), - html.label( - "Last name: ", - html.input( - {"value": person["last_name"], "on_change": handle_last_name_change} - ), - ), - html.label( - "Email: ", - html.input({"value": person["email"], "on_change": handle_email_change}), - ), - html.p(f"{person['first_name']} {person['last_name']} {person['email']}"), - ) - - -run(Form) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_insert.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_insert.py deleted file mode 100644 index 1f4072e0b..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_insert.py +++ /dev/null @@ -1,25 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def ArtistList(): - artist_to_add, set_artist_to_add = use_state("") - artists, set_artists = use_state([]) - - def handle_change(event): - set_artist_to_add(event["target"]["value"]) - - def handle_click(event): - if artist_to_add and artist_to_add not in artists: - set_artists([*artists, artist_to_add]) - set_artist_to_add("") - - return html.div( - html.h1("Inspiring sculptors:"), - html.input({"value": artist_to_add, "on_change": handle_change}), - html.button({"on_click": handle_click}, "add"), - html.ul([html.li({"key": name}, name) for name in artists]), - ) - - -run(ArtistList) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_re_order.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_re_order.py deleted file mode 100644 index 3bd2fd601..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_re_order.py +++ /dev/null @@ -1,24 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def ArtistList(): - artists, set_artists = use_state( - ["Marta Colvin Andrade", "Lamidi Olonade Fakeye", "Louise Nevelson"] - ) - - def handle_sort_click(event): - set_artists(sorted(artists)) - - def handle_reverse_click(event): - set_artists(list(reversed(artists))) - - return html.div( - html.h1("Inspiring sculptors:"), - html.button({"on_click": handle_sort_click}, "sort"), - html.button({"on_click": handle_reverse_click}, "reverse"), - html.ul([html.li({"key": name}, name) for name in artists]), - ) - - -run(ArtistList) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_remove.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_remove.py deleted file mode 100644 index 6223284f6..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_remove.py +++ /dev/null @@ -1,44 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def ArtistList(): - artist_to_add, set_artist_to_add = use_state("") - artists, set_artists = use_state( - ["Marta Colvin Andrade", "Lamidi Olonade Fakeye", "Louise Nevelson"] - ) - - def handle_change(event): - set_artist_to_add(event["target"]["value"]) - - def handle_add_click(event): - if artist_to_add not in artists: - set_artists([*artists, artist_to_add]) - set_artist_to_add("") - - def make_handle_delete_click(index): - def handle_click(event): - set_artists(artists[:index] + artists[index + 1 :]) - - return handle_click - - return html.div( - html.h1("Inspiring sculptors:"), - html.input({"value": artist_to_add, "on_change": handle_change}), - html.button({"on_click": handle_add_click}, "add"), - html.ul( - [ - html.li( - {"key": name}, - name, - html.button( - {"on_click": make_handle_delete_click(index)}, "delete" - ), - ) - for index, name in enumerate(artists) - ] - ), - ) - - -run(ArtistList) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_replace.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_replace.py deleted file mode 100644 index 5577da5e1..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_replace.py +++ /dev/null @@ -1,27 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def CounterList(): - counters, set_counters = use_state([0, 0, 0]) - - def make_increment_click_handler(index): - def handle_click(event): - new_value = counters[index] + 1 - set_counters([*counters[:index], new_value, *counters[index + 1 :]]) - - return handle_click - - return html.ul( - [ - html.li( - {"key": index}, - count, - html.button({"on_click": make_increment_click_handler(index)}, "+1"), - ) - for index, count in enumerate(counters) - ] - ) - - -run(CounterList) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot.py deleted file mode 100644 index e5ab54dca..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot.py +++ /dev/null @@ -1,45 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def MovingDot(): - position, set_position = use_state({"x": 0, "y": 0}) - - async def handle_pointer_move(event): - outer_div_info = event["currentTarget"] - outer_div_bounds = outer_div_info["boundingClientRect"] - set_position( - { - "x": event["clientX"] - outer_div_bounds["x"], - "y": event["clientY"] - outer_div_bounds["y"], - } - ) - - return html.div( - { - "on_pointer_move": handle_pointer_move, - "style": { - "position": "relative", - "height": "200px", - "width": "100%", - "background_color": "white", - }, - }, - html.div( - { - "style": { - "position": "absolute", - "background_color": "red", - "border_radius": "50%", - "width": "20px", - "height": "20px", - "left": "-10px", - "top": "-10px", - "transform": f"translate({position['x']}px, {position['y']}px)", - } - } - ), - ) - - -run(MovingDot) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot_broken.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot_broken.py deleted file mode 100644 index 8972ce74e..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot_broken.py +++ /dev/null @@ -1,43 +0,0 @@ -# :linenos: - -from reactpy import component, html, run, use_state - - -@component -def MovingDot(): - position, _ = use_state({"x": 0, "y": 0}) - - def handle_pointer_move(event): - outer_div_info = event["currentTarget"] - outer_div_bounds = outer_div_info["boundingClientRect"] - position["x"] = event["clientX"] - outer_div_bounds["x"] - position["y"] = event["clientY"] - outer_div_bounds["y"] - - return html.div( - { - "on_pointer_move": handle_pointer_move, - "style": { - "position": "relative", - "height": "200px", - "width": "100%", - "background_color": "white", - }, - }, - html.div( - { - "style": { - "position": "absolute", - "background_color": "red", - "border_radius": "50%", - "width": "20px", - "height": "20px", - "left": "-10px", - "top": "-10px", - "transform": f"translate({position['x']}px, {position['y']}px)", - } - } - ), - ) - - -run(MovingDot) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_remove.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_remove.py deleted file mode 100644 index abe55a918..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_remove.py +++ /dev/null @@ -1,41 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def Grid(): - line_size = 5 - selected_indices, set_selected_indices = use_state({1, 2, 4}) - - def make_handle_click(index): - def handle_click(event): - if index in selected_indices: - set_selected_indices(selected_indices - {index}) - else: - set_selected_indices(selected_indices | {index}) - - return handle_click - - return html.div( - {"style": {"display": "flex", "flex-direction": "row"}}, - [ - html.div( - { - "on_click": make_handle_click(index), - "style": { - "height": "30px", - "width": "30px", - "background_color": ( - "black" if index in selected_indices else "white" - ), - "outline": "1px solid grey", - "cursor": "pointer", - }, - "key": index, - } - ) - for index in range(line_size) - ], - ) - - -run(Grid) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_update.py b/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_update.py deleted file mode 100644 index 27f170a42..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_update.py +++ /dev/null @@ -1,38 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def Grid(): - line_size = 5 - selected_indices, set_selected_indices = use_state(set()) - - def make_handle_click(index): - def handle_click(event): - set_selected_indices(selected_indices | {index}) - - return handle_click - - return html.div( - {"style": {"display": "flex", "flex-direction": "row"}}, - [ - html.div( - { - "on_click": make_handle_click(index), - "style": { - "height": "30px", - "width": "30px", - "background_color": ( - "black" if index in selected_indices else "white" - ), - "outline": "1px solid grey", - "cursor": "pointer", - }, - "key": index, - } - ) - for index in range(line_size) - ], - ) - - -run(Grid) diff --git a/docs/source/guides/adding-interactivity/dangers-of-mutability/index.rst b/docs/source/guides/adding-interactivity/dangers-of-mutability/index.rst deleted file mode 100644 index bcac79e18..000000000 --- a/docs/source/guides/adding-interactivity/dangers-of-mutability/index.rst +++ /dev/null @@ -1,569 +0,0 @@ -Dangers of Mutability -===================== - -While state can hold any type of value, you should be careful to avoid directly -modifying objects that you declare as state with ReactPy. In other words, you must not -:ref:`"mutate" <What is a Mutation>` values which are held as state. Rather, to change -these values you should use new ones or create copies. - - -.. _what is a mutation: - -What is a Mutation? -------------------- - -In Python, values may be either "mutable" or "immutable". Mutable objects are those -whose underlying data can be changed after they are created, and immutable objects are -those which cannot. A "mutation" then, is the act of changing the underlying data of a -mutable value. In particular, a :class:`dict` is a mutable type of value. In the code -below, an initially empty dictionary is created. Then, a key and value is added to it: - -.. code-block:: - - x = {} - x["a"] = 1 - assert x == {"a": 1} - -This is different from something like a :class:`str` which is immutable. Instead of -modifying the underlying data of an existing value, a new one must be created to -facilitate change: - -.. code-block:: - - x = "Hello" - y = x + " world!" - assert x is not y - -.. note:: - - In Python, the ``is`` and ``is not`` operators check whether two values are - identitcal. This `is distinct - <https://realpython.com/python-is-identity-vs-equality>`__ from checking whether two - values are equivalent with the ``==`` or ``!=`` operators. - -Thus far, all the values we've been working with have been immutable. These include -:class:`int`, :class:`float`, :class:`str`, and :class:`bool` values. As a result, we -have not had to consider the consequences of mutations. - - -.. _Why Avoid Mutation: - -Why Avoid Mutation? -------------------- - -Unfortunately, ReactPy does not understand that when a value is mutated, it may have -changed. As a result, mutating values will not trigger re-renders. Thus, you must be -careful to avoid mutation whenever you want ReactPy to re-render a component. For example, -the intention of the code below is to make the red dot move when you touch or hover over -the preview area. However it doesn't - the dot remains stationary: - -.. reactpy:: _examples/moving_dot_broken - -The problem is with this section of code: - -.. literalinclude:: _examples/moving_dot_broken.py - :language: python - :lines: 13-14 - :linenos: - :lineno-start: 13 - -This code mutates the ``position`` dictionary from the prior render instead of using the -state variable's associated state setter. Without calling setter ReactPy has no idea that -the variable's data has been modified. While it can be possible to get away with -mutating state variables, it's highly dicsouraged. Doing so can cause strange and -unpredictable behavior. As a result, you should always treat the data within a state -variable as immutable. - -To actually trigger a render we need to call the state setter. To do that we'll assign -it to ``set_position`` instead of the unused ``_`` variable we have above. Then we can -call it by passing a *new* dictionary with the values for the next render. Notice how, -by making these alterations to the code, that the dot now follows your pointer when -you touch or hover over the preview: - -.. reactpy:: _examples/moving_dot - - -.. dropdown:: Local mutation can be alright - :color: info - :animate: fade-in - - While code like this causes problems: - - .. code-block:: - - position["x"] = event["clientX"] - outer_div_bounds["x"] - position["y"] = event["clientY"] - outer_div_bounds["y"] - - It's ok if you mutate a fresh dictionary that you have *just* created before calling - the state setter: - - .. code-block:: - - new_position = {} - new_position["x"] = event["clientX"] - outer_div_bounds["x"] - new_position["y"] = event["clientY"] - outer_div_bounds["y"] - set_position(new_position) - - It's actually nearly equivalent to having written: - - .. code-block:: - - set_position( - { - "x": event["clientX"] - outer_div_bounds["x"], - "y": event["clientY"] - outer_div_bounds["y"], - } - ) - - Mutation is only a problem when you change data assigned to existing state - variables. Mutating an object you’ve just created is okay because no other code - references it yet. Changing it isn’t going to accidentally impact something that - depends on it. This is called a “local mutation.” You can even do local mutation - while rendering. Very convenient and completely okay! - - -Working with Dictionaries -------------------------- - -Below are some ways to update dictionaries without mutating them: - -.. card:: Updating Items - :link: updating-dictionary-items - :link-type: ref - - Avoid using item assignment, ``dict.update``, or ``dict.setdefault``. Instead try - the strategies below: - - .. code-block:: - - {**d, "key": value} - - # Python >= 3.9 - d | {"key": value} - - # Equivalent to dict.setdefault() - {"key": value, **d} - -.. card:: Removing Items - :link: removing-dictionary-items - :link-type: ref - - Avoid using item deletion or ``dict.pop``. Instead try the strategies below: - - .. code-block:: - - { - k: v - for k, v in d.items() - if k != key - } - - # Better for removing multiple items - { - k: d[k] - for k in set(d).difference([key]) - } - - ----- - - -.. _updating-dictionary-items: - -Updating Dictionary Items -......................... - -.. grid:: 1 1 1 2 - :gutter: 1 - - .. grid-item-card:: :bdg-danger:`Avoid` - - .. code-block:: - - d[key] = value - - d.update({key: value}) - - d.setdefault(key, value) - - .. grid-item-card:: :bdg-info:`Prefer` - - .. code-block:: - - {**d, key: value} - - # Python >= 3.9 - d | {key: value} - - # Equivalent to setdefault() - {key: value, **d} - -As we saw in an :ref:`earlier example <why avoid mutation>`, instead of mutating -dictionaries to update their items you should instead create a copy that contains the -desired changes. - -However, sometimes you may only want to update some of the information in a dictionary -which is held by a state variable. Consider the case below where we have a form for -updating user information with a preview of the currently entered data. We can -accomplish this using `"unpacking" <https://www.python.org/dev/peps/pep-0448/>`__ with -the ``**`` syntax: - -.. reactpy:: _examples/dict_update - - -.. _removing-dictionary-items: - -Removing Dictionary Items -......................... - -.. grid:: 1 1 1 2 - :gutter: 1 - - .. grid-item-card:: :bdg-danger:`Avoid` - - .. code-block:: - - del d[key] - - d.pop(key) - - .. grid-item-card:: :bdg-info:`Prefer` - - .. code-block:: - - { - k: v - for k, v in d.items() - if k != key - } - - # Better for removing multiple items - { - k: d[k] - for k in set(d).difference([key]) - } - -This scenario doesn't come up very frequently. When it does though, the best way to -remove items from dictionaries is to create a copy of the original, but with a filtered -set of keys. One way to do this is with a dictionary comprehension. The example below -shows an interface where you're able to enter a new term and definition. Once added, -you can click a delete button to remove the term and definition: - -.. reactpy:: _examples/dict_remove - - -Working with Lists ------------------- - -Below are some ways to update lists without mutating them: - -.. card:: Inserting Items - :link: inserting-list-items - :link-type: ref - - Avoid using ``list.append``, ``list.extend``, and ``list.insert``. Instead try the - strategies below: - - .. code-block:: - - [*l, value] - - l + [value] - - l + values - - l[:index] + [value] + l[index:] - -.. card:: Removing Items - :link: removing-list-items - :link-type: ref - - Avoid using item deletion or ``list.pop``. Instead try the strategy below: - - .. code-block:: - - l[:index - 1] + l[index:] - -.. card:: Replacing Items - :link: replacing-list-items - :link-type: ref - - Avoid using item or slice assignment. Instead try the strategies below: - - .. code-block:: - - l[:index] + [value] + l[index + 1:] - - l[:start] + values + l[end + 1:] - -.. card:: Re-ordering Items - :link: re-ordering-list-items - :link-type: ref - - Avoid using ``list.sort`` or ``list.reverse``. Instead try the strategies below: - - .. code-block:: - - list(sorted(l)) - - list(reversed(l)) - - ----- - - -.. _inserting-list-items: - -Inserting List Items -.................... - -.. grid:: 1 1 1 2 - - .. grid-item-card:: :bdg-danger:`Avoid` - - .. code-block:: - - l.append(value) - - l.extend(values) - - l.insert(index, value) - - # Adding a list "in-place" mutates! - l += [value] - - .. grid-item-card:: :bdg-info:`Prefer` - - .. code-block:: - - [*l, value] - - l + [value] - - l + values - - l[:index] + [value] + l[index:] - -Instead of mutating a list to add items to it, we need to create a new list which has -the items we want to append instead. There are several ways to do this for one or more -values however it's often simplest to use `"unpacking" -<https://www.python.org/dev/peps/pep-0448/>`__ with the ``*`` syntax. - -.. reactpy:: _examples/list_insert - - -.. _removing-list-items: - -Removing List Items -................... - -.. grid:: 1 1 1 2 - - .. grid-item-card:: :bdg-danger:`Avoid` - - .. code-block:: - - del l[index] - - l.pop(index) - - .. grid-item-card:: :bdg-info:`Prefer` - - .. code-block:: - - l[:index] + l[index + 1:] - -Unfortunately, the syntax for creating a copy of a list with one of its items removed is -not quite as clean. You must select the portion the list prior to the item which should -be removed (``l[:index]``) and the portion after the item (``l[index + 1:]``) and add -them together: - -.. reactpy:: _examples/list_remove - - -.. _replacing-list-items: - -Replacing List Items -.................... - -.. grid:: 1 1 1 2 - - .. grid-item-card:: :bdg-danger:`Avoid` - - .. code-block:: - - l[index] = value - - l[start:end] = values - - .. grid-item-card:: :bdg-info:`Prefer` - - .. code-block:: - - l[:index] + [value] + l[index + 1:] - - l[:start] + values + l[end + 1:] - -In a similar manner to :ref:`removing list items`, to replace an item in a list, you -must select the portion before and after the item in question. But this time, instead -of adding those two selections together, you must insert that values you want to replace -between them: - -.. reactpy:: _examples/list_replace - - -.. _re-ordering-list-items: - -Re-ordering List Items -...................... - -.. grid:: 1 1 1 2 - - .. grid-item-card:: :bdg-danger:`Avoid` - - .. code-block:: - - l.sort() - - l.reverse() - - .. grid-item-card:: :bdg-info:`Prefer` - - .. code-block:: - - list(sorted(l)) - - list(reversed(l)) - -There are many different ways that list items could be re-ordered, but two of the most -common are reversing or sorting items. Instead of calling the associated methods on a -list object, you should use the builtin functions :func:`sorted` and :func:`reversed` -and pass the resulting iterator into the :class:`list` constructor to create a sorted -or reversed copy of the given list: - -.. reactpy:: _examples/list_re_order - - -Working with Sets ------------------ - -Below are ways to update sets without mutating them: - -.. card:: Adding Items - :link: adding-set-items - :link-type: ref - - Avoid using item assignment, ``set.add`` or ``set.update``. Instead try the - strategies below: - - .. code-block:: - - s.union({value}) - - s.union(values) - -.. card:: Removing Items - :link: removing-set-items - :link-type: ref - - Avoid using item deletion or ``dict.pop``. Instead try the strategies below: - - .. code-block:: - - s.difference({value}) - - s.difference(values) - - s.intersection(values) - - ----- - - -.. _adding-set-items: - -Adding Set Items -................ - -.. grid:: 1 1 1 2 - - .. grid-item-card:: :bdg-danger:`Avoid` - - .. code-block:: - - s.add(value) - s |= {value} # "in-place" operators mutate! - - s.update(values) - s |= values # "in-place" operators mutate! - - .. grid-item-card:: :bdg-info:`Prefer` - - .. code-block:: - - s.union({value}) - s | {value} - - s.union(values) - s | values - -Sets have some nice ways for evolving them without requiring mutation. The binary -or operator ``|`` serves as a succinct way to compute the union of two sets. However, -you should be careful to not use an in-place assignment with this operator as that will -(counterintuitively) mutate the original set rather than creating a new one. - -.. reactpy:: _examples/set_update - - -.. _removing-set-items: - -Removing Set Items -.................. - -.. grid:: 1 1 1 2 - - .. grid-item-card:: :bdg-danger:`Avoid` - - .. code-block:: - - s.remove(value) - - s.difference_update(values) - s -= values # "in-place" operators mutate! - - s.symmetric_difference_update(values) - s ^= values # "in-place" operators mutate! - - s.intersection_update(values) - s &= values # "in-place" operators mutate! - - - .. grid-item-card:: :bdg-info:`Prefer` - - .. code-block:: - - s.difference({value}) - - s.difference(values) - s - values - - s.symmetric_difference(values) - s ^ values - - s.intersection(values) - s & values - -To remove items from sets you can use the various binary operators or their associated -methods to return new sets without mutating them. As before when :ref:`adding set items` -you need to avoid using the inline assignment operators since that will -(counterintuitively) mutate the original set rather than given you a new one: - -.. reactpy:: _examples/set_remove - - -Useful Packages ---------------- - -Under construction 🚧 - -https://pypi.org/project/pyrsistent/ diff --git a/docs/source/guides/adding-interactivity/index.rst b/docs/source/guides/adding-interactivity/index.rst deleted file mode 100644 index af84fab88..000000000 --- a/docs/source/guides/adding-interactivity/index.rst +++ /dev/null @@ -1,210 +0,0 @@ -Adding Interactivity -==================== - -.. toctree:: - :hidden: - - responding-to-events/index - components-with-state/index - state-as-a-snapshot/index - multiple-state-updates/index - dangers-of-mutability/index - - -.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn - :color: info - :animate: fade-in - :open: - - .. grid:: 1 2 2 2 - - .. grid-item-card:: :octicon:`bell` Responding to Events - :link: responding-to-events/index - :link-type: doc - - Define event handlers and learn about the available event types they can be - bound to. - - .. grid-item-card:: :octicon:`package-dependencies` Components With State - :link: components-with-state/index - :link-type: doc - - Allow components to change what they display by saving and updating their - state. - - .. grid-item-card:: :octicon:`device-camera-video` State as a Snapshot - :link: state-as-a-snapshot/index - :link-type: doc - - Learn why state updates schedules a re-render, instead of being applied - immediately. - - .. grid-item-card:: :octicon:`versions` Multiple State Updates - :link: multiple-state-updates/index - :link-type: doc - - Learn how updates to a components state can be batched, or applied - incrementally. - - .. grid-item-card:: :octicon:`issue-opened` Dangers of Mutability - :link: dangers-of-mutability/index - :link-type: doc - - See the pitfalls of working with mutable data types and how to avoid them. - - -Section 1: Responding to Events -------------------------------- - -ReactPy lets you add event handlers to your parts of the interface. This means that you can -define synchronous or asynchronous functions that are triggered when a particular user -interaction occurs like clicking, hovering, focusing on form inputs, and more. - -.. reactpy:: responding-to-events/_examples/button_prints_message - -It may feel weird to define a function within a function like this, but doing so allows -the ``handle_event`` function to access information from within the scope of the -component. That's important if you want to use any arguments that may have been passed -your component in the handler. - -.. card:: - :link: responding-to-events/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Define event handlers and learn about the available event types they can be bound - to. - - -Section 2: Components with State --------------------------------- - -Components often need to change what’s on the screen as a result of an interaction. For -example, typing into the form should update the input field, clicking a “Comment” button -should bring up a text input field, clicking “Buy” should put a product in the shopping -cart. Components need to “remember” things like the current input value, the current -image, the shopping cart. In ReactPy, this kind of component-specific memory is created and -updated with a "hook" called ``use_state()`` that creates a **state variable** and -**state setter** respectively: - -.. reactpy:: components-with-state/_examples/adding_state_variable - -In ReactPy, ``use_state``, as well as any other function whose name starts with ``use``, is -called a "hook". These are special functions that should only be called while ReactPy is -:ref:`rendering <the rendering process>`. They let you "hook into" the different -capabilities of ReactPy's components of which ``use_state`` is just one (well get into the -other :ref:`later <managing state>`). - -.. card:: - :link: components-with-state/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Allow components to change what they display by saving and updating their state. - - -Section 3: State as a Snapshot ------------------------------- - -As we :ref:`learned earlier <Components with State>`, state setters behave a little -differently than you might expect at first glance. Instead of updating your current -handle on the setter's corresponding variable, it schedules a re-render of the component -which owns the state. - -.. code-block:: - - count, set_count = use_state(0) - print(count) # prints: 0 - set_count(count + 1) # schedule a re-render where count is 1 - print(count) # still prints: 0 - -This behavior of ReactPy means that each render of a component is like taking a snapshot of -the UI based on the component's state at that time. Treating state in this way can help -reduce subtle bugs. For instance, in the code below there's a simple chat app with a -message input and recipient selector. The catch is that the message actually gets sent 5 -seconds after the "Send" button is clicked. So what would happen if we changed the -recipient between the time the "Send" button was clicked and the moment the message is -actually sent? - -.. reactpy:: state-as-a-snapshot/_examples/print_chat_message - -As it turns out, changing the message recipient after pressing send does not change -where the message ultimately goes. However, one could imagine a bug where the recipient -of a message is determined at the time the message is sent rather than at the time the -"Send" button it clicked. Thus changing the recipient after pressing send would change -where the message got sent. - -In many cases, ReactPy avoids this class of bug entirely because it treats state as a -snapshot. - -.. card:: - :link: state-as-a-snapshot/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Learn why state updates schedules a re-render, instead of being applied immediately. - - -Section 4: Multiple State Updates ---------------------------------- - -As we saw in an earlier example, :ref:`setting state triggers renders`. In other words, -changes to state only take effect in the next render, not in the current one. Further, -changes to state are batched, calling a particular state setter 3 times won't trigger 3 -renders, it will only trigger 1. This means that multiple state assignments are batched -- so long as the event handler is synchronous (i.e. the event handler is not an -``async`` function), ReactPy waits until all the code in an event handler has run before -processing state and starting the next render: - -.. reactpy:: multiple-state-updates/_examples/set_color_3_times - -Sometimes though, you need to update a state variable more than once before the next -render. In these cases, instead of having updates batched, you instead want them to be -applied incrementally. That is, the next update can be made to depend on the prior one. -To accomplish this, instead of passing the next state value directly (e.g. -``set_state(new_state)``), we may pass an **"updater function"** of the form -``compute_new_state(old_state)`` to the state setter (e.g. -``set_state(compute_new_state)``): - -.. reactpy:: multiple-state-updates/_examples/set_state_function - -.. card:: - :link: multiple-state-updates/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Learn how updates to a components state can be batched, or applied incrementally. - - -Section 5: Dangers of Mutability --------------------------------- - -While state can hold any type of value, you should be careful to avoid directly -modifying objects that you declare as state with ReactPy. In other words, you must not -:ref:`"mutate" <What is a Mutation>` values which are held as state. Rather, to change -these values you should use new ones or create copies. - -This is because ReactPy does not understand that when a value is mutated, it may have -changed. As a result, mutating values will not trigger re-renders. Thus, you must be -careful to avoid mutation whenever you want ReactPy to re-render a component. For example, -instead of mutating dictionaries to update their items you should instead create a -copy that contains the desired changes: - -.. reactpy:: dangers-of-mutability/_examples/dict_update - -.. card:: - :link: dangers-of-mutability/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - See the pitfalls of working with mutable data types and how to avoid them. diff --git a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_count_updater.py b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_count_updater.py deleted file mode 100644 index e53c5b1ad..000000000 --- a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_count_updater.py +++ /dev/null @@ -1,20 +0,0 @@ -import asyncio - -from reactpy import component, html, run, use_state - - -@component -def Counter(): - number, set_number = use_state(0) - - async def handle_click(event): - await asyncio.sleep(3) - set_number(lambda old_number: old_number + 1) - - return html.div( - html.h1(number), - html.button({"on_click": handle_click}, "Increment"), - ) - - -run(Counter) diff --git a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_set_count.py b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_set_count.py deleted file mode 100644 index bb64724f1..000000000 --- a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_set_count.py +++ /dev/null @@ -1,20 +0,0 @@ -import asyncio - -from reactpy import component, html, run, use_state - - -@component -def Counter(): - number, set_number = use_state(0) - - async def handle_click(event): - await asyncio.sleep(3) - set_number(number + 1) - - return html.div( - html.h1(number), - html.button({"on_click": handle_click}, "Increment"), - ) - - -run(Counter) diff --git a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_color_3_times.py b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_color_3_times.py deleted file mode 100644 index 59d7d0f20..000000000 --- a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_color_3_times.py +++ /dev/null @@ -1,27 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def ColorButton(): - color, set_color = use_state("gray") - - def handle_click(event): - set_color("orange") - set_color("pink") - set_color("blue") - - def handle_reset(event): - set_color("gray") - - return html.div( - html.button( - {"on_click": handle_click, "style": {"background_color": color}}, - "Set Color", - ), - html.button( - {"on_click": handle_reset, "style": {"background_color": color}}, "Reset" - ), - ) - - -run(ColorButton) diff --git a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_state_function.py b/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_state_function.py deleted file mode 100644 index 56bbe80e3..000000000 --- a/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_state_function.py +++ /dev/null @@ -1,24 +0,0 @@ -from reactpy import component, html, run, use_state - - -def increment(old_number): - new_number = old_number + 1 - return new_number - - -@component -def Counter(): - number, set_number = use_state(0) - - def handle_click(event): - set_number(increment) - set_number(increment) - set_number(increment) - - return html.div( - html.h1(number), - html.button({"on_click": handle_click}, "Increment"), - ) - - -run(Counter) diff --git a/docs/source/guides/adding-interactivity/multiple-state-updates/index.rst b/docs/source/guides/adding-interactivity/multiple-state-updates/index.rst deleted file mode 100644 index bc3245d7e..000000000 --- a/docs/source/guides/adding-interactivity/multiple-state-updates/index.rst +++ /dev/null @@ -1,109 +0,0 @@ -Multiple State Updates -====================== - -Setting a state variable will queue another render. But sometimes you might want to -perform multiple operations on the value before queueing the next render. To do this, it -helps to understand how React batches state updates. - - -Batched Updates ---------------- - -As we learned :ref:`previously <state as a snapshot>`, state variables remain fixed -inside each render as if state were a snapshot taken at the beginning of each render. -This is why, in the example below, even though it might seem like clicking the -"Increment" button would cause the ``number`` to increase by ``3``, it only does by -``1``: - -.. reactpy:: ../state-as-a-snapshot/_examples/set_counter_3_times - -The reason this happens is because, so long as the event handler is synchronous (i.e. -the event handler is not an ``async`` function), ReactPy waits until all the code in an -event handler has run before processing state and starting the next render. Thus, it's -the last call to a given state setter that matters. In the example below, even though we -set the color of the button to ``"orange"`` and then ``"pink"`` before ``"blue"``, -the color does not quickly flash orange and pink before blue - it always remains blue: - -.. reactpy:: _examples/set_color_3_times - -This behavior let's you make multiple state changes without triggering unnecessary -renders or renders with inconsistent state where only some of the variables have been -updated. With that said, it also means that the UI won't change until after synchronous -handlers have finished running. - -.. note:: - - For asynchronous event handlers, ReactPy will not render until you ``await`` something. - As we saw in :ref:`prior examples <State And Delayed Reactions>`, if you introduce - an asynchronous delay to an event handler after changing state, renders may take - place before the remainder of the event handler completes. However, state variables - within handlers, even async ones, always remains static. - -This behavior of ReactPy to "batch" state changes that take place inside a single event -handler, do not extend across event handlers. In other words, distinct events will -always produce distinct renders. To give an example, if clicking a button increments a -counter by one, no matter how fast the user clicks, the view will never jump from 1 to 3 -- it will always display 1, then 2, and then 3. - - -Incremental Updates -------------------- - -While it's uncommon, you need to update a state variable more than once before the next -render. In these cases, instead of having updates batched, you instead want them to be -applied incrementally. That is, the next update can be made to depend on the prior one. -For example, what it we wanted to make it so that, in our ``Counter`` example :ref:`from -before <Batched Updates>`, each call to ``set_number`` did in fact increment -``number`` by one causing the view to display ``0``, then ``3``, then ``6``, and so on? - -To accomplish this, instead of passing the next state value as in ``set_number(number + -1)``, we may pass an **"updater function"** to ``set_number`` that computes the next -state based on the previous state. This would look like ``set_number(lambda number: -number + 1)``. In other words we need a function of the form: - -.. code-block:: - - def compute_new_state(old_state): - ... - return new_state - -In our case, ``new_state = old_state + 1``. So we might define: - -.. code-block:: - - def increment(old_number): - new_number = old_number + 1 - return new_number - -Which we can use to replace ``set_number(number + 1)`` with ``set_number(increment)``: - -.. reactpy:: _examples/set_state_function - -The way to think about how ReactPy runs though this series of ``set_state(increment)`` -calls is to imagine that each one updates the internally managed state with its return -value, then that return value is being passed to the next updater function. Ultimately, -this is functionally equivalent to the following: - -.. code-block:: - - set_number(increment(increment(increment(number)))) - -So why might you want to do this? Why not just compute ``set_number(number + 3)`` from -the start? The easiest way to explain the use case is with an example. Imagine that we -introduced a delay before ``set_number(number + 1)``. What would happen if we clicked -the "Increment" button more than once before the delay in the first triggered event -completed? - -.. reactpy:: _examples/delay_before_set_count - -From an :ref:`earlier lesson <State And Delayed Reactions>`, we learned that introducing -delays do not change the fact that state variables do not change until the next render. -As a result, despite clicking many times before the delay completes, the ``number`` only -increments by one. To solve this we can use updater functions: - -.. reactpy:: _examples/delay_before_count_updater - -Now when you click the "Increment" button, each click, though delayed, corresponds to -``number`` being increased. This is because the ``old_number`` in the updater function -uses the value which was assigned by the last call to ``set_number`` rather than relying -in the static ``number`` state variable. diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/audio_player.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/audio_player.py deleted file mode 100644 index 82826c50d..000000000 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/audio_player.py +++ /dev/null @@ -1,21 +0,0 @@ -import json - -import reactpy - - -@reactpy.component -def PlayDinosaurSound(): - event, set_event = reactpy.hooks.use_state(None) - return reactpy.html.div( - reactpy.html.audio( - { - "controls": True, - "on_time_update": lambda e: set_event(e), - "src": "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", - } - ), - reactpy.html.pre(json.dumps(event, indent=2)), - ) - - -reactpy.run(PlayDinosaurSound) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_async_handlers.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_async_handlers.py deleted file mode 100644 index 992641e00..000000000 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_async_handlers.py +++ /dev/null @@ -1,23 +0,0 @@ -import asyncio - -from reactpy import component, html, run - - -@component -def ButtonWithDelay(message, delay): - async def handle_event(event): - await asyncio.sleep(delay) - print(message) - - return html.button({"on_click": handle_event}, message) - - -@component -def App(): - return html.div( - ButtonWithDelay("print 3 seconds later", delay=3), - ButtonWithDelay("print immediately", delay=0), - ) - - -run(App) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_does_nothing.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_does_nothing.py deleted file mode 100644 index ea8313263..000000000 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_does_nothing.py +++ /dev/null @@ -1,9 +0,0 @@ -from reactpy import component, html, run - - -@component -def Button(): - return html.button("I don't do anything yet") - - -run(Button) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_handler_as_arg.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_handler_as_arg.py deleted file mode 100644 index e5276bef3..000000000 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_handler_as_arg.py +++ /dev/null @@ -1,33 +0,0 @@ -from reactpy import component, html, run - - -@component -def Button(display_text, on_click): - return html.button({"on_click": on_click}, display_text) - - -@component -def PlayButton(movie_name): - def handle_click(event): - print(f"Playing {movie_name}") - - return Button(f"Play {movie_name}", on_click=handle_click) - - -@component -def FastForwardButton(): - def handle_click(event): - print("Skipping ahead") - - return Button("Fast forward", on_click=handle_click) - - -@component -def App(): - return html.div( - PlayButton("Buena Vista Social Club"), - FastForwardButton(), - ) - - -run(App) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_event.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_event.py deleted file mode 100644 index 38638db4b..000000000 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_event.py +++ /dev/null @@ -1,12 +0,0 @@ -from reactpy import component, html, run - - -@component -def Button(): - def handle_event(event): - print(event) - - return html.button({"on_click": handle_event}, "Click me!") - - -run(Button) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_message.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_message.py deleted file mode 100644 index 56118a57f..000000000 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_message.py +++ /dev/null @@ -1,20 +0,0 @@ -from reactpy import component, html, run - - -@component -def PrintButton(display_text, message_text): - def handle_event(event): - print(message_text) - - return html.button({"on_click": handle_event}, display_text) - - -@component -def App(): - return html.div( - PrintButton("Play", "Playing"), - PrintButton("Pause", "Paused"), - ) - - -run(App) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/prevent_default_event_actions.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/prevent_default_event_actions.py deleted file mode 100644 index d3f0941bd..000000000 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/prevent_default_event_actions.py +++ /dev/null @@ -1,18 +0,0 @@ -from reactpy import component, event, html, run - - -@component -def DoNotChangePages(): - return html.div( - html.p("Normally clicking this link would take you to a new page"), - html.a( - { - "on_click": event(lambda event: None, prevent_default=True), - "href": "https://google.com", - }, - "https://google.com", - ), - ) - - -run(DoNotChangePages) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/_examples/stop_event_propagation.py b/docs/source/guides/adding-interactivity/responding-to-events/_examples/stop_event_propagation.py deleted file mode 100644 index 41c575042..000000000 --- a/docs/source/guides/adding-interactivity/responding-to-events/_examples/stop_event_propagation.py +++ /dev/null @@ -1,42 +0,0 @@ -from reactpy import component, event, hooks, html, run - - -@component -def DivInDiv(): - stop_propagatation, set_stop_propagatation = hooks.use_state(True) - inner_count, set_inner_count = hooks.use_state(0) - outer_count, set_outer_count = hooks.use_state(0) - - div_in_div = html.div( - { - "on_click": lambda event: set_outer_count(outer_count + 1), - "style": {"height": "100px", "width": "100px", "background_color": "red"}, - }, - html.div( - { - "on_click": event( - lambda event: set_inner_count(inner_count + 1), - stop_propagation=stop_propagatation, - ), - "style": { - "height": "50px", - "width": "50px", - "background_color": "blue", - }, - } - ), - ) - - return html.div( - html.button( - {"on_click": lambda event: set_stop_propagatation(not stop_propagatation)}, - "Toggle Propagation", - ), - html.pre(f"Will propagate: {not stop_propagatation}"), - html.pre(f"Inner click count: {inner_count}"), - html.pre(f"Outer click count: {outer_count}"), - div_in_div, - ) - - -run(DivInDiv) diff --git a/docs/source/guides/adding-interactivity/responding-to-events/index.rst b/docs/source/guides/adding-interactivity/responding-to-events/index.rst deleted file mode 100644 index 89f567ca0..000000000 --- a/docs/source/guides/adding-interactivity/responding-to-events/index.rst +++ /dev/null @@ -1,144 +0,0 @@ -Responding to Events -==================== - -ReactPy lets you add event handlers to your parts of the interface. These events handlers -are functions which can be assigned to a part of a UI such that, when a user iteracts -with the interface, those functions get triggered. Examples of interaction include -clicking, hovering, of focusing on form inputs, and more. - - -Adding Event Handlers ---------------------- - -To start out we'll just display a button that, for the moment, doesn't do anything: - -.. reactpy:: _examples/button_does_nothing - -To add an event handler to this button we'll do three things: - -1. Declare a function called ``handle_event(event)`` inside the body of our ``Button`` component -2. Add logic to ``handle_event`` that will print the ``event`` it receives to the console. -3. Add an ``"onClick": handle_event`` attribute to the ``<button>`` element. - -.. reactpy:: _examples/button_prints_event - -.. note:: - - Normally print statements will only be displayed in the terminal where you launched - ReactPy. - -It may feel weird to define a function within a function like this, but doing so allows -the ``handle_event`` function to access information from within the scope of the -component. That's important if you want to use any arguments that may have been passed -your component in the handler: - -.. reactpy:: _examples/button_prints_message - -With all that said, since our ``handle_event`` function isn't doing that much work, if -we wanted to streamline our component definition, we could pass in our event handler as a -lambda: - -.. code-block:: - - html.button({"onClick": lambda event: print(message_text)}, "Click me!") - - -Supported Event Types ---------------------- - -Since ReactPy's event information comes from React, most the the information (:ref:`with -some exceptions <event data Serialization>`) about how React handles events translates -directly to ReactPy. Follow the links below to learn about each category of event: - -- :ref:`Clipboard Events` -- :ref:`Composition Events` -- :ref:`Keyboard Events` -- :ref:`Focus Events` -- :ref:`Form Events` -- :ref:`Generic Events` -- :ref:`Mouse Events` -- :ref:`Pointer Events` -- :ref:`Selection Events` -- :ref:`Touch Events` -- :ref:`UI Events` -- :ref:`Wheel Events` -- :ref:`Media Events` -- :ref:`Image Events` -- :ref:`Animation Events` -- :ref:`Transition Events` -- :ref:`Other Events` - - -Passing Handlers to Components ------------------------------- - -A common pattern when factoring out common logic is to pass event handlers into a more -generic component definition. This allows the component to focus on the things which are -common while still giving its usages customizablity. Consider the case below where we -want to create a generic ``Button`` component that can be used for a variety of purpose: - -.. reactpy:: _examples/button_handler_as_arg - - -.. _Async Event Handler: - -Async Event Handlers --------------------- - -Sometimes event handlers need to execute asynchronous tasks when they are triggered. -Behind the scenes, ReactPy is running an :mod:`asyncio` event loop for just this purpose. -By defining your event handler as an asynchronous function instead of a normal -synchronous one. In the layout below we sleep for several seconds before printing out a -message in the first button. However, because the event handler is asynchronous, the -handler for the second button is still able to respond: - -.. reactpy:: _examples/button_async_handlers - - -Event Data Serialization ------------------------- - -Not all event data is serialized. The most notable example of this is the lack of a -``target`` key in the dictionary sent back to the handler. Instead, data which is not -inherently JSON serializable must be treated on a case-by-case basis. A simple case -to demonstrate this is the ``currentTime`` attribute of ``audio`` and ``video`` -elements. Normally this would be accessible via ``event.target.currentTime``, but here -it's simply passed in under the key ``currentTime``: - -.. reactpy:: _examples/audio_player - - -Client-side Event Behavior --------------------------- - -Because ReactPy operates server-side, there are inevitable limitations that prevent it from -achieving perfect parity with all the behaviors of React. With that said, any feature -that cannot be achieved in Python with ReactPy, can be done by creating -:ref:`Custom Javascript Components`. - - -Preventing Default Event Actions -................................ - -Instead of calling an ``event.preventDefault()`` method as you would do in React, you -must declare whether to prevent default behavior ahead of time. This can be accomplished -using the :func:`~reactpy.core.events.event` decorator and setting ``prevent_default``. For -example, we can stop a link from going to the specified URL: - -.. reactpy:: _examples/prevent_default_event_actions - -Unfortunately this means you cannot conditionally prevent default behavior in response -to event data without writing :ref:`Custom Javascript Components`. - - -Stop Event Propagation -...................... - -Similarly to :ref:`preventing default behavior <Preventing Default Event Actions>`, you -can use the :func:`~reactpy.core.events.event` decorator to prevent events originating in a -child element from propagating to parent elements by setting ``stop_propagation``. In -the example below we place a red ``div`` inside a parent blue ``div``. When propagation -is turned on, clicking the red element will cause the handler for the outer blue one to -trigger. Conversely, when it's off, only the handler for the red element will trigger. - -.. reactpy:: _examples/stop_event_propagation diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/delayed_print_after_set.py b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/delayed_print_after_set.py deleted file mode 100644 index b6295b09f..000000000 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/delayed_print_after_set.py +++ /dev/null @@ -1,22 +0,0 @@ -import asyncio - -from reactpy import component, html, run, use_state - - -@component -def Counter(): - number, set_number = use_state(0) - - async def handle_click(event): - set_number(number + 5) - print("about to print...") - await asyncio.sleep(3) - print(number) - - return html.div( - html.h1(number), - html.button({"on_click": handle_click}, "Increment"), - ) - - -run(Counter) diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_chat_message.py b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_chat_message.py deleted file mode 100644 index ecbad9381..000000000 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_chat_message.py +++ /dev/null @@ -1,44 +0,0 @@ -import asyncio - -from reactpy import component, event, html, run, use_state - - -@component -def App(): - recipient, set_recipient = use_state("Alice") - message, set_message = use_state("") - - @event(prevent_default=True) - async def handle_submit(event): - set_message("") - print("About to send message...") - await asyncio.sleep(5) - print(f"Sent '{message}' to {recipient}") - - return html.form( - {"on_submit": handle_submit, "style": {"display": "inline-grid"}}, - html.label( - {}, - "To: ", - html.select( - { - "value": recipient, - "on_change": lambda event: set_recipient(event["target"]["value"]), - }, - html.option({"value": "Alice"}, "Alice"), - html.option({"value": "Bob"}, "Bob"), - ), - ), - html.input( - { - "type": "text", - "placeholder": "Your message...", - "value": message, - "on_change": lambda event: set_message(event["target"]["value"]), - } - ), - html.button({"type": "submit"}, "Send"), - ) - - -run(App) diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_count_after_set.py b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_count_after_set.py deleted file mode 100644 index 40ee78259..000000000 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_count_after_set.py +++ /dev/null @@ -1,18 +0,0 @@ -from reactpy import component, html, run, use_state - - -@component -def Counter(): - number, set_number = use_state(0) - - def handle_click(event): - set_number(number + 5) - print(number) - - return html.div( - html.h1(number), - html.button({"on_click": handle_click}, "Increment"), - ) - - -run(Counter) diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/send_message.py b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/send_message.py deleted file mode 100644 index 4702a7464..000000000 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/send_message.py +++ /dev/null @@ -1,35 +0,0 @@ -from reactpy import component, event, html, run, use_state - - -@component -def App(): - is_sent, set_is_sent = use_state(False) - message, set_message = use_state("") - - if is_sent: - return html.div( - html.h1("Message sent!"), - html.button( - {"on_click": lambda event: set_is_sent(False)}, "Send new message?" - ), - ) - - @event(prevent_default=True) - def handle_submit(event): - set_message("") - set_is_sent(True) - - return html.form( - {"on_submit": handle_submit, "style": {"display": "inline-grid"}}, - html.textarea( - { - "placeholder": "Your message here...", - "value": message, - "on_change": lambda event: set_message(event["target"]["value"]), - } - ), - html.button({"type": "submit"}, "Send"), - ) - - -run(App) diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/direct-state-change.png b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/direct-state-change.png deleted file mode 100644 index cfcd9c87f..000000000 Binary files a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/direct-state-change.png and /dev/null differ diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/reactpy-state-change.png b/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/reactpy-state-change.png deleted file mode 100644 index b36b0514a..000000000 Binary files a/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/reactpy-state-change.png and /dev/null differ diff --git a/docs/source/guides/adding-interactivity/state-as-a-snapshot/index.rst b/docs/source/guides/adding-interactivity/state-as-a-snapshot/index.rst deleted file mode 100644 index a677a3e68..000000000 --- a/docs/source/guides/adding-interactivity/state-as-a-snapshot/index.rst +++ /dev/null @@ -1,158 +0,0 @@ -State as a Snapshot -=================== - -When you watch the user interfaces you build change as you interact with them, it's easy -to imagining that they do so because there's some bit of code that modifies the relevant -parts of the view directly. As an illustration, you may think that when a user clicks a -"Send" button, there's code which reaches into the view and adds some text saying -"Message sent!": - -.. image:: _static/direct-state-change.png - -ReactPy works a bit differently though - user interactions cause event handlers to -:ref:`"set state" <Introduction to use_state>` triggering ReactPy to re-render a new -version of the view rather then mutating the existing one. - -.. image:: _static/reactpy-state-change.png - -Given this, when ReactPy "renders" something, it's as if ReactPy has taken a snapshot of the -UI where all the event handlers, local variables and the view itself were calculated -using what state was present at the time of that render. Then, when user interactions -trigger state setters, ReactPy is made away of the newly set state and schedules a -re-render. When this subsequent renders occurs it performs all the same calculations as -before, but with this new state. - -As we've :ref:`already seen <When Variables Aren't Enough>`, state variables are not -like normal variables. Instead, they live outside your components and are managed by -ReactPy. When a component is rendered, ReactPy provides the component a snapshot of the state -in that exact moment. As a result, the view returned by that component is itself a -snapshot of the UI at that time. - - -Setting State Triggers Renders ------------------------------- - -Setting state does not impact the current render, instead it schedules a re-render. It's -only in this subsequent render that changes to state take effect. As a result, setting -state more than once in the context of the same render will not cause those changes to -compound. This makes it easier to reason about how your UI will react to user -interactions because state does not change until the next render. - -Let's experiment with this behaviors of state to see why we should think about it with -respect to these "snapshots" in time. Take a look at the example below and try to guess -how it will behave. **What will the count be after you click the "Increment" button?** - -.. reactpy:: _examples/set_counter_3_times - -Despite the fact that we called ``set_count(count + 1)`` three times, the count only -increments by ``1``! This is perhaps a surprising result, but let's break what's -happening inside the event handler to see why this is happening: - -.. code-block:: - - set_count(count + 1) - set_count(count + 1) - set_count(count + 1) - -On the initial render of your ``Counter`` the ``number`` variable is ``0``. Because we -know that state variables do not change until the next render we ought to be able to -substitute ``number`` with ``0`` everywhere it's referenced within the component until -then. That includes the event handler too we should be able to rewrite the three lines -above as: - -.. code-block:: - - set_count(0 + 1) - set_count(0 + 1) - set_count(0 + 1) - -Even though, we called ``set_count`` three times with what might have seemed like -different values, every time we were actually just doing ``set_count(1)`` on each call. -Only after the event handler returns will ReactPy actually perform the next render where -count is ``1``. When it does, ``number`` will be ``1`` and we'll be able to perform the -same substitution as before to see what the next number will be after we click -"Increment": - -.. code-block:: - - set_count(1 + 1) - set_count(1 + 1) - set_count(1 + 1) - - -State And Delayed Reactions ---------------------------- - -Given what we :ref:`learned above <setting state triggers renders>`, we ought to be able -to reason about what should happen in the example below. What will be printed when the -"Increment" button is clicked? - -.. reactpy:: _examples/print_count_after_set - -If we use the same substitution trick we saw before, we can rewrite these lines: - -.. code-block:: - - set_number(number + 5) - print(number) - -Using the value of ``number`` in the initial render which is ``0``: - -.. code-block:: - - set_number(0 + 5) - print(0) - -Thus when we click the button we should expect that the next render will show ``5``, but -we will ``print`` the number ``0`` instead. The next time we click the view will show -``10`` and the printout will be ``5``. In this sense the print statement, because it -lives within the prior snapshot, trails what is displayed in the next render. - -What if we slightly modify this example, by introducing a delay between when we call -``set_number`` and when we print? Will this behavior remain the same? To add this delay -we'll use an :ref:`async event handler` and :func:`~asyncio.sleep` for some time: - -.. reactpy:: _examples/delayed_print_after_set - -Even though the render completed before the print statement took place, the behavior -remained the same! Despite the fact that the next render took place before the print -statement did, the print statement still relies on the state snapshot from the initial -render. Thus we can continue to use our substitution trick to analyze what's happening: - -.. code-block:: - - set_number(0 + 5) - print("about to print...") - await asyncio.sleep(3) - print(0) - -This property of state, that it remains static within the context of particular render, -while unintuitive at first, is actually an important tool for preventing subtle bugs. -Let's consider the example below where there's a form that sends a message with a 5 -second delay. Imagine a scenario where the user: - -1. Presses the "Send" button with the message "Hello" where "Alice" is the recipient. -2. Then, before the five-second delay ends, the user changes the "To" field to "Bob". - -The first question to ask is "What should happen?" In this case, the user's expectation -is that after they press "Send", changing the recipient, even if the message has not -been sent yet, should not impact where the message is ultimately sent. We then need to -ask what actually happens. Will it print “You said Hello to Alice” or “You said Hello to -Bob”? - -.. reactpy:: _examples/print_chat_message - -As it turns out, the code above matches the user's expectation. This is because ReactPy -keeps the state values fixed within the event handlers defined during a particular -render. As a result, you don't need to worry about whether state has changed while -code in an event handler is running. - -.. card:: - :link: ../multiple-state-updates/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - What if you wanted to read the latest state values before the next render? You’ll - want to use a state updater function, covered on the next page! diff --git a/docs/source/guides/creating-interfaces/html-with-reactpy/index.rst b/docs/source/guides/creating-interfaces/html-with-reactpy/index.rst deleted file mode 100644 index 4a8ba4957..000000000 --- a/docs/source/guides/creating-interfaces/html-with-reactpy/index.rst +++ /dev/null @@ -1,131 +0,0 @@ -HTML With ReactPy -================= - -In a typical Python-based web application the responsibility of defining the view along -with its backing data and logic are distributed between a client and server -respectively. With ReactPy, both these tasks are centralized in a single place. This is -done by allowing HTML interfaces to be constructed in Python. Take a look at the two -code examples below. The first one shows how to make a basic title and todo list using -standard HTML, the second uses ReactPy in Python, and below is a view of what the HTML -would look like if displayed: - -.. grid:: 1 1 2 2 - :margin: 0 - :padding: 0 - - .. grid-item:: - - .. code-block:: html - - <h1>My Todo List</h1> - <ul> - <li>Build a cool new app</li> - <li>Share it with the world!</li> - </ul> - - .. grid-item:: - - .. testcode:: - - from reactpy import html - - html.h1("My Todo List") - html.ul( - html.li("Build a cool new app"), - html.li("Share it with the world!"), - ) - - .. grid-item-card:: - :columns: 12 - - .. raw:: html - - <div style="width: 50%; margin: auto;"> - <h2 style="margin-top: 0px !important;">My Todo List</h2> - <ul> - <li>Build a cool new app</li> - <li>Share it with the world!</li> - </ul> - </div> - -What this shows is that you can recreate the same HTML layouts with ReactPy using functions -from the :mod:`reactpy.html` module. These function share the same names as their -corresponding HTML tags. For instance, the ``<h1/>`` element above has a similarly named -:func:`~reactpy.html.h1` function. With that said, while the code above looks similar, it's -not very useful because we haven't captured the results from these function calls in a -variable. To do this we need to wrap up the layout above into a single -:func:`~reactpy.html.div` and assign it to a variable: - -.. testcode:: - - layout = html.div( - html.h1("My Todo List"), - html.ul( - html.li("Build a cool new app"), - html.li("Share it with the world!"), - ), - ) - - -Adding HTML Attributes ----------------------- - -That's all well and good, but there's more to HTML than just text. What if we wanted to -display an image? In HTMl we'd use the ``<img>`` element and add attributes to it order -to specify a URL to its ``src`` and use some ``style`` to modify and position it: - -.. code-block:: html - - <img - src="https://picsum.photos/id/237/500/300" - class="img-fluid" - style="width: 50%; margin-left: 25%;" - alt="Billie Holiday" - tabindex="0" - /> - -In ReactPy we add these attributes to elements using a dictionary: - -.. testcode:: - - html.img( - { - "src": "https://picsum.photos/id/237/500/300", - "class_name": "img-fluid", - "style": {"width": "50%", "margin_left": "25%"}, - "alt": "Billie Holiday", - } - ) - -.. raw:: html - - <!-- no tabindex since that would ruin accessibility of the page --> - <img - src="https://picsum.photos/id/237/500/300" - class="img-fluid" - style="width: 50%; margin-left: 25%;" - alt="Billie Holiday" - /> - -There are some notable differences. First, all names in ReactPy use ``snake_case`` instead -of dash-separated words. For example, ``tabindex`` and ``margin-left`` become -``tab_index`` and ``margin_left`` respectively. Second, instead of using a string to -specify the ``style`` attribute, we use a dictionary to describe the CSS properties we -want to apply to an element. This is done to avoid having to escape quotes and other -characters in the string. Finally, the ``class`` attribute is renamed to ``class_name`` -to avoid conflicting with the ``class`` keyword in Python. - -For full list of supported attributes and differences from HTML, see the -:ref:`HTML Attributes` reference. - ----------- - - -.. card:: - :link: /guides/understanding-reactpy/representing-html - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Dive into the data structures ReactPy uses to represent HTML diff --git a/docs/source/guides/creating-interfaces/index.rst b/docs/source/guides/creating-interfaces/index.rst deleted file mode 100644 index 78466eaef..000000000 --- a/docs/source/guides/creating-interfaces/index.rst +++ /dev/null @@ -1,128 +0,0 @@ -Creating Interfaces -=================== - -.. toctree:: - :hidden: - - html-with-reactpy/index - your-first-components/index - rendering-data/index - -.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn - :color: info - :animate: fade-in - :open: - - .. grid:: 1 2 2 2 - - .. grid-item-card:: :octicon:`code-square` HTML with ReactPy - :link: html-with-reactpy/index - :link-type: doc - - Construct HTML layouts from the basic units of user interface functionality. - - .. grid-item-card:: :octicon:`package` Your First Components - :link: your-first-components/index - :link-type: doc - - Define reusable building blocks that it easier to construct complex - interfaces. - - .. grid-item-card:: :octicon:`database` Rendering Data - :link: rendering-data/index - :link-type: doc - - Use data to organize and render HTML elements and components. - -ReactPy is a Python package for making user interfaces (UI). These interfaces are built -from small elements of functionality like buttons text and images. ReactPy allows you to -combine these elements into reusable, nestable :ref:`"components" <your first -components>`. In the sections that follow you'll learn how these UI elements are created -and organized into components. Then, you'll use components to customize and -conditionally display more complex UIs. - - -Section 1: HTML with ReactPy ----------------------------- - -In a typical Python-base web application the responsibility of defining the view along -with its backing data and logic are distributed between a client and server -respectively. With ReactPy, both these tasks are centralized in a single place. The most -foundational pilar of this capability is formed by allowing HTML interfaces to be -constructed in Python. Let's consider the HTML sample below: - -.. code-block:: html - - <h1>My Todo List</h1> - <ul> - <li>Build a cool new app</li> - <li>Share it with the world!</li> - </ul> - -To recreate the same thing in ReactPy you would write: - -.. code-block:: - - from reactpy import html - - html.div( - html.h1("My Todo List"), - html.ul( - html.li("Design a cool new app"), - html.li("Build it"), - html.li("Share it with the world!"), - ) - ) - -.. card:: - :link: html-with-reactpy/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Construct HTML layouts from the basic units of user interface functionality. - - -Section 2: Your First Components --------------------------------- - -The next building block in our journey with ReactPy are components. At their core, -components are just a normal Python functions that return :ref:`HTML <HTML with ReactPy>`. -The one special thing about them that we'll concern ourselves with now, is that to -create them we need to add an ``@component`` `decorator -<https://realpython.com/primer-on-python-decorators/>`__. To see what this looks like in -practice we'll quickly make a ``Photo`` component: - -.. reactpy:: your-first-components/_examples/simple_photo - -.. card:: - :link: your-first-components/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Define reusable building blocks that it easier to construct complex interfaces. - - -Section 3: Rendering Data -------------------------- - -The last pillar of knowledge you need before you can start making :ref:`interactive -interfaces <adding interactivity>` is the ability to render sections of the UI given a -collection of data. This will require you to understand how elements which are derived -from data in this way must be organized with :ref:`"keys" <Organizing Items With Keys>`. -One case where we might want to do this is if items in a todo list come from a list of -data that we want to sort and filter: - -.. reactpy:: rendering-data/_examples/todo_list_with_keys - -.. card:: - :link: rendering-data/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Use data to organize and render HTML elements and components. diff --git a/docs/source/guides/creating-interfaces/rendering-data/_examples/sorted_and_filtered_todo_list.py b/docs/source/guides/creating-interfaces/rendering-data/_examples/sorted_and_filtered_todo_list.py deleted file mode 100644 index 8be2b5f30..000000000 --- a/docs/source/guides/creating-interfaces/rendering-data/_examples/sorted_and_filtered_todo_list.py +++ /dev/null @@ -1,32 +0,0 @@ -from reactpy import component, html, run - - -@component -def DataList(items, filter_by_priority=None, sort_by_priority=False): - if filter_by_priority is not None: - items = [i for i in items if i["priority"] <= filter_by_priority] - if sort_by_priority: - items = sorted(items, key=lambda i: i["priority"]) - list_item_elements = [html.li(i["text"]) for i in items] - return html.ul(list_item_elements) - - -@component -def TodoList(): - tasks = [ - {"text": "Make breakfast", "priority": 0}, - {"text": "Feed the dog", "priority": 0}, - {"text": "Do laundry", "priority": 2}, - {"text": "Go on a run", "priority": 1}, - {"text": "Clean the house", "priority": 2}, - {"text": "Go to the grocery store", "priority": 2}, - {"text": "Do some coding", "priority": 1}, - {"text": "Read a book", "priority": 1}, - ] - return html.section( - html.h1("My Todo List"), - DataList(tasks, filter_by_priority=1, sort_by_priority=True), - ) - - -run(TodoList) diff --git a/docs/source/guides/creating-interfaces/rendering-data/_examples/todo_from_list.py b/docs/source/guides/creating-interfaces/rendering-data/_examples/todo_from_list.py deleted file mode 100644 index 85ba1d79d..000000000 --- a/docs/source/guides/creating-interfaces/rendering-data/_examples/todo_from_list.py +++ /dev/null @@ -1,28 +0,0 @@ -from reactpy import component, html, run - - -@component -def DataList(items): - list_item_elements = [html.li(text) for text in items] - return html.ul(list_item_elements) - - -@component -def TodoList(): - tasks = [ - "Make breakfast (important)", - "Feed the dog (important)", - "Do laundry", - "Go on a run (important)", - "Clean the house", - "Go to the grocery store", - "Do some coding", - "Read a book (important)", - ] - return html.section( - html.h1("My Todo List"), - DataList(tasks), - ) - - -run(TodoList) diff --git a/docs/source/guides/creating-interfaces/rendering-data/_examples/todo_list_with_keys.py b/docs/source/guides/creating-interfaces/rendering-data/_examples/todo_list_with_keys.py deleted file mode 100644 index 8afd2ae55..000000000 --- a/docs/source/guides/creating-interfaces/rendering-data/_examples/todo_list_with_keys.py +++ /dev/null @@ -1,32 +0,0 @@ -from reactpy import component, html, run - - -@component -def DataList(items, filter_by_priority=None, sort_by_priority=False): - if filter_by_priority is not None: - items = [i for i in items if i["priority"] <= filter_by_priority] - if sort_by_priority: - items = sorted(items, key=lambda i: i["priority"]) - list_item_elements = [html.li({"key": i["id"]}, i["text"]) for i in items] - return html.ul(list_item_elements) - - -@component -def TodoList(): - tasks = [ - {"id": 0, "text": "Make breakfast", "priority": 0}, - {"id": 1, "text": "Feed the dog", "priority": 0}, - {"id": 2, "text": "Do laundry", "priority": 2}, - {"id": 3, "text": "Go on a run", "priority": 1}, - {"id": 4, "text": "Clean the house", "priority": 2}, - {"id": 5, "text": "Go to the grocery store", "priority": 2}, - {"id": 6, "text": "Do some coding", "priority": 1}, - {"id": 7, "text": "Read a book", "priority": 1}, - ] - return html.section( - html.h1("My Todo List"), - DataList(tasks, filter_by_priority=1, sort_by_priority=True), - ) - - -run(TodoList) diff --git a/docs/source/guides/creating-interfaces/rendering-data/index.rst b/docs/source/guides/creating-interfaces/rendering-data/index.rst deleted file mode 100644 index 620426142..000000000 --- a/docs/source/guides/creating-interfaces/rendering-data/index.rst +++ /dev/null @@ -1,297 +0,0 @@ -Rendering Data -============== - -Frequently you need to construct a number of similar components from a collection of -data. Let's imagine that we want to create a todo list that can be ordered and filtered -on the priority of each item in the list. To start, we'll take a look at the kind of -view we'd like to display: - -.. code-block:: html - - <ul> - <li>Make breakfast (important)</li> - <li>Feed the dog (important)</li> - <li>Do laundry</li> - <li>Go on a run (important)</li> - <li>Clean the house</li> - <li>Go to the grocery store</li> - <li>Do some coding</li> - <li>Read a book (important)</li> - </ul> - -Based on this, our next step in achieving our goal is to break this view down into the -underlying data that we'd want to use to represent it. The most straightforward way to -do this would be to just put the text of each ``<li>`` into a list: - -.. testcode:: - - tasks = [ - "Make breakfast (important)", - "Feed the dog (important)", - "Do laundry", - "Go on a run (important)", - "Clean the house", - "Go to the grocery store", - "Do some coding", - "Read a book (important)", - ] - -We could then take this list and "render" it into a series of ``<li>`` elements: - -.. testcode:: - - from reactpy import html - - list_item_elements = [html.li(text) for text in tasks] - -This list of elements can then be passed into a parent ``<ul>`` element: - -.. testcode:: - - list_element = html.ul(list_item_elements) - -The last thing we have to do is return this from a component: - -.. reactpy:: _examples/todo_from_list - - -Filtering and Sorting Elements ------------------------------- - -Our representation of ``tasks`` worked fine to just get them on the screen, but it -doesn't extend well to the case where we want to filter and order them based on -priority. Thus, we need to change the data structure we're using to represent our tasks: - -.. testcode:: - - tasks = [ - {"text": "Make breakfast", "priority": 0}, - {"text": "Feed the dog", "priority": 0}, - {"text": "Do laundry", "priority": 2}, - {"text": "Go on a run", "priority": 1}, - {"text": "Clean the house", "priority": 2}, - {"text": "Go to the grocery store", "priority": 2}, - {"text": "Do some coding", "priority": 1}, - {"text": "Read a book", "priority": 1}, - ] - -With this we can now imaging writing some filtering and sorting logic using Python's -:func:`filter` and :func:`sorted` functions respectively. We'll do this by only -displaying items whose ``priority`` is less than or equal to some ``filter_by_priority`` -and then ordering the elements based on the ``priority``: - -.. testcode:: - - filter_by_priority = 1 - sort_by_priority = True - - filtered_tasks = tasks - if filter_by_priority is not None: - filtered_tasks = [t for t in filtered_tasks if t["priority"] <= filter_by_priority] - if sort_by_priority: - filtered_tasks = list(sorted(filtered_tasks, key=lambda t: t["priority"])) - - assert filtered_tasks == [ - {'text': 'Make breakfast', 'priority': 0}, - {'text': 'Feed the dog', 'priority': 0}, - {'text': 'Go on a run', 'priority': 1}, - {'text': 'Do some coding', 'priority': 1}, - {'text': 'Read a book', 'priority': 1}, - ] - -We could then add this code to our ``DataList`` component: - -.. warning:: - - The code below produces a bunch of warnings! Be sure to read the - :ref:`next section <Organizing Items With Keys>` to find out why. - -.. reactpy:: _examples/sorted_and_filtered_todo_list - - -Organizing Items With Keys --------------------------- - -If you run the examples above :ref:`in debug mode <Running ReactPy in Debug Mode>` you'll -see the server log a bunch of errors that look something like: - -.. code-block:: text - - Key not specified for child in list {'tagName': 'li', 'children': ...} - -What this is telling us is that we haven't specified a unique ``key`` for each of the -items in our todo list. In order to silence this warning we need to expand our data -structure even further to include a unique ID for each item in our todo list: - -.. testcode:: - - tasks = [ - {"id": 0, "text": "Make breakfast", "priority": 0}, - {"id": 1, "text": "Feed the dog", "priority": 0}, - {"id": 2, "text": "Do laundry", "priority": 2}, - {"id": 3, "text": "Go on a run", "priority": 1}, - {"id": 4, "text": "Clean the house", "priority": 2}, - {"id": 5, "text": "Go to the grocery store", "priority": 2}, - {"id": 6, "text": "Do some coding", "priority": 1}, - {"id": 7, "text": "Read a book", "priority": 1}, - ] - -Then, as we're constructing our ``<li>`` elements we'll declare a ``key`` attribute: - -.. code-block:: - - list_item_elements = [html.li({"key": t["id"]}, t["text"]) for t in tasks] - -This ``key`` tells ReactPy which ``<li>`` element corresponds to which item of data in our -``tasks`` list. This becomes important if the order or number of items in your list can -change. In our case, if we decided to change whether we want to ``filter_by_priority`` -or ``sort_by_priority`` the items in our ``<ul>`` element would change. Given this, -here's how we'd change our component: - -.. reactpy:: _examples/todo_list_with_keys - - -Keys for Components -................... - -Thus far we've been talking about passing keys to standard HTML elements. However, this -principle also applies to components too. Every function decorated with the -``@component`` decorator automatically gets a ``key`` parameter that operates in the -exact same way that it does for standard HTML elements: - -.. testcode:: - - from reactpy import component - - - @component - def ListItem(text): - return html.li(text) - - tasks = [ - {"id": 0, "text": "Make breakfast"}, - {"id": 1, "text": "Feed the dog"}, - {"id": 2, "text": "Do laundry"}, - {"id": 3, "text": "Go on a run"}, - {"id": 4, "text": "Clean the house"}, - {"id": 5, "text": "Go to the grocery store"}, - {"id": 6, "text": "Do some coding"}, - {"id": 7, "text": "Read a book"}, - ] - - list_element = [ListItem(t["text"], key=t["id"]) for t in tasks] - - -.. warning:: - - The ``key`` argument is reserved for this purpose. Defining a component with a - function that has a ``key`` parameter will cause an error: - - .. testcode:: - - from reactpy import component - - @component - def FunctionWithKeyParam(key): - ... - - .. testoutput:: - - Traceback (most recent call last): - ... - TypeError: Component render function ... uses reserved parameter 'key' - - -Rules of Keys -............. - -In order to avoid unexpected behaviors when rendering data with keys, there are a few -rules that need to be followed. These will ensure that each item of data is associated -with the correct UI element. - -.. dropdown:: Keys may be the same if their elements are not siblings - :color: info - - If two elements have different parents in the UI, they can use the same keys. - - .. testcode:: - - data_1 = [ - {"id": 1, "text": "Something"}, - {"id": 2, "text": "Something else"}, - ] - - data_2 = [ - {"id": 1, "text": "Another thing"}, - {"id": 2, "text": "Yet another thing"}, - ] - - html.section( - html.ul([html.li(data["text"], key=data["id"]) for data in data_1]), - html.ul([html.li(data["text"], key=data["id"]) for data in data_2]), - ) - -.. dropdown:: Keys must be unique amongst siblings - :color: danger - - Keys must be unique among siblings. - - .. testcode:: - - data = [ - {"id": 1, "text": "Something"}, - {"id": 2, "text": "Something else"}, - {"id": 1, "text": "Another thing"}, # BAD: has a duplicated id - {"id": 2, "text": "Yet another thing"}, # BAD: has a duplicated id - ] - - html.section( - html.ul([html.li(data["text"], key=data["id"]) for data in data]), - ) - -.. dropdown:: Keys must be fixed to their data. - :color: danger - - Don't generate random values for keys to avoid the warning. - - .. testcode:: - - from random import random - - data = [ - {"id": random(), "text": "Something"}, - {"id": random(), "text": "Something else"}, - {"id": random(), "text": "Another thing"}, - {"id": random(), "text": "Yet another thing"}, - ] - - html.section( - html.ul([html.li(data["text"], key=data["id"]) for data in data]), - ) - - Doing so will result in unexpected behavior. - -Since we've just been working with a small amount of sample data thus far, it was easy -enough for us to manually add an ``id`` key to each item of data. Often though, we have -to work with data that already exists. In those cases, how should we pick what value to -use for each ``key``? - -- If your data comes from your database you should use the keys and IDs generated by - that database since these are inherently unique. For example, you might use the - primary key of records in a relational database. - -- If your data is generated and persisted locally (e.g. notes in a note-taking app), use - an incrementing counter or :mod:`uuid` from the standard library when creating items. - - ----------- - - -.. card:: - :link: /guides/understanding-reactpy/why-reactpy-needs-keys - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Learn about why ReactPy needs keys in the first place. diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/bad_conditional_todo_list.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/bad_conditional_todo_list.py deleted file mode 100644 index 1ed3268b6..000000000 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/bad_conditional_todo_list.py +++ /dev/null @@ -1,24 +0,0 @@ -from reactpy import component, html, run - - -@component -def Item(name, done): - if done: - return html.li(name, " ✔") - else: - return html.li(name) - - -@component -def TodoList(): - return html.section( - html.h1("My Todo List"), - html.ul( - Item("Find a cool problem to solve", done=True), - Item("Build an app to solve it", done=True), - Item("Share that app with the world!", done=False), - ), - ) - - -run(TodoList) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/good_conditional_todo_list.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/good_conditional_todo_list.py deleted file mode 100644 index cd9ab6fc0..000000000 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/good_conditional_todo_list.py +++ /dev/null @@ -1,21 +0,0 @@ -from reactpy import component, html, run - - -@component -def Item(name, done): - return html.li(name, " ✔" if done else "") - - -@component -def TodoList(): - return html.section( - html.h1("My Todo List"), - html.ul( - Item("Find a cool problem to solve", done=True), - Item("Build an app to solve it", done=True), - Item("Share that app with the world!", done=False), - ), - ) - - -run(TodoList) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/nested_photos.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/nested_photos.py deleted file mode 100644 index 96f8531d3..000000000 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/nested_photos.py +++ /dev/null @@ -1,25 +0,0 @@ -from reactpy import component, html, run - - -@component -def Photo(): - return html.img( - { - "src": "https://picsum.photos/id/274/500/300", - "style": {"width": "30%"}, - "alt": "Ray Charles", - } - ) - - -@component -def Gallery(): - return html.section( - html.h1("Famous Musicians"), - Photo(), - Photo(), - Photo(), - ) - - -run(Gallery) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/parametrized_photos.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/parametrized_photos.py deleted file mode 100644 index 665dd8c86..000000000 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/parametrized_photos.py +++ /dev/null @@ -1,25 +0,0 @@ -from reactpy import component, html, run - - -@component -def Photo(alt_text, image_id): - return html.img( - { - "src": f"https://picsum.photos/id/{image_id}/500/200", - "style": {"width": "50%"}, - "alt": alt_text, - } - ) - - -@component -def Gallery(): - return html.section( - html.h1("Photo Gallery"), - Photo("Landscape", image_id=830), - Photo("City", image_id=274), - Photo("Puppy", image_id=237), - ) - - -run(Gallery) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/simple_photo.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/simple_photo.py deleted file mode 100644 index 94fa6633f..000000000 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/simple_photo.py +++ /dev/null @@ -1,15 +0,0 @@ -from reactpy import component, html, run - - -@component -def Photo(): - return html.img( - { - "src": "https://picsum.photos/id/237/500/300", - "style": {"width": "50%"}, - "alt": "Puppy", - } - ) - - -run(Photo) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/todo_list.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/todo_list.py deleted file mode 100644 index 2ffd09261..000000000 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/todo_list.py +++ /dev/null @@ -1,21 +0,0 @@ -from reactpy import component, html, run - - -@component -def Item(name, done): - return html.li(name) - - -@component -def TodoList(): - return html.section( - html.h1("My Todo List"), - html.ul( - Item("Find a cool problem to solve", done=True), - Item("Build an app to solve it", done=True), - Item("Share that app with the world!", done=False), - ), - ) - - -run(TodoList) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_div.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_div.py deleted file mode 100644 index 58ed79dd8..000000000 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_div.py +++ /dev/null @@ -1,13 +0,0 @@ -from reactpy import component, html, run - - -@component -def MyTodoList(): - return html.div( - html.h1("My Todo List"), - html.img({"src": "https://picsum.photos/id/0/500/300"}), - html.ul(html.li("The first thing I need to do is...")), - ) - - -run(MyTodoList) diff --git a/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_fragment.py b/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_fragment.py deleted file mode 100644 index cc54d8b71..000000000 --- a/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_fragment.py +++ /dev/null @@ -1,13 +0,0 @@ -from reactpy import component, html, run - - -@component -def MyTodoList(): - return html._( - html.h1("My Todo List"), - html.img({"src": "https://picsum.photos/id/0/500/200"}), - html.ul(html.li("The first thing I need to do is...")), - ) - - -run(MyTodoList) diff --git a/docs/source/guides/creating-interfaces/your-first-components/index.rst b/docs/source/guides/creating-interfaces/your-first-components/index.rst deleted file mode 100644 index 00b53d1b7..000000000 --- a/docs/source/guides/creating-interfaces/your-first-components/index.rst +++ /dev/null @@ -1,134 +0,0 @@ -Your First Components -===================== - -As we learned :ref:`earlier <HTML with ReactPy>` we can use ReactPy to make rich structured -documents out of standard HTML elements. As these documents become larger and more -complex though, working with these tiny UI elements can become difficult. When this -happens, ReactPy allows you to group these elements together info "components". These -components can then be reused throughout your application. - - -Defining a Component --------------------- - -At their core, components are just normal Python functions that return HTML. To define a -component you just need to add a ``@component`` `decorator -<https://realpython.com/primer-on-python-decorators/>`__ to a function. Functions -decorator in this way are known as **render function** and, by convention, we name them -like classes - with ``CamelCase``. So consider what we would do if we wanted to write, -and then :ref:`display <Running ReactPy>` a ``Photo`` component: - -.. reactpy:: _examples/simple_photo - -.. warning:: - - If we had not decorated our ``Photo``'s render function with the ``@component`` - decorator, the server would start, but as soon as we tried to view the page it would - be blank. The servers logs would then indicate: - - .. code-block:: text - - TypeError: Expected a ComponentType, not dict. - - -Using a Component ------------------ - -Having defined our ``Photo`` component we can now nest it inside of other components. We -can define a "parent" ``Gallery`` component that returns one or more ``Profile`` -components. This is part of what makes components so powerful - you can define a -component once and use it wherever and however you need to: - -.. reactpy:: _examples/nested_photos - - -Return a Single Root Element ----------------------------- - -Components must return a "single root element". That one root element may have children, -but you cannot for example, return a list of element from a component and expect it to -be rendered correctly. If you want to return multiple elements you must wrap them in -something like a :func:`html.div <reactpy.html.div>`: - -.. reactpy:: _examples/wrap_in_div - -If don't want to add an extra ``div`` you can use a "fragment" instead with the -:func:`html._ <reactpy.html._>` function: - -.. reactpy:: _examples/wrap_in_fragment - -Fragments allow you to group elements together without leaving any trace in the UI. For -example, the first code sample written with ReactPy will produce the second HTML code -block: - -.. grid:: 1 2 2 2 - :margin: 0 - :padding: 0 - - .. grid-item:: - - .. testcode:: - - from reactpy import html - - html.ul( - html._( - html.li("Group 1 Item 1"), - html.li("Group 1 Item 2"), - html.li("Group 1 Item 3"), - ), - html._( - html.li("Group 2 Item 1"), - html.li("Group 2 Item 2"), - html.li("Group 2 Item 3"), - ) - ) - - .. grid-item:: - - .. code-block:: html - - <ul> - <li>Group 1 Item 1</li> - <li>Group 1 Item 2</li> - <li>Group 1 Item 3</li> - <li>Group 2 Item 1</li> - <li>Group 2 Item 2</li> - <li>Group 2 Item 3</li> - </ul> - - - -Parametrizing Components ------------------------- - -Since components are just regular functions, you can add parameters to them. This allows -parent components to pass information to child components. Where standard HTML elements -are parametrized by dictionaries, since components behave like typical functions you can -give them positional and keyword arguments as you would normally: - -.. reactpy:: _examples/parametrized_photos - - -Conditional Rendering ---------------------- - -Your components will often need to display different things depending on different -conditions. Let's imagine that we had a basic todo list where only some of the items -have been completed. Below we have a basic implementation for such a list except that -the ``Item`` component doesn't change based on whether it's ``done``: - -.. reactpy:: _examples/todo_list - -Let's imagine that we want to add a ✔ to the items which have been marked ``done=True``. -One way to do this might be to write an ``if`` statement where we return one ``li`` -element if the item is ``done`` and a different one if it's not: - -.. reactpy:: _examples/bad_conditional_todo_list - -As you can see this accomplishes our goal! However, notice how similar ``html.li(name, " -✔")`` and ``html.li(name)`` are. While in this case it isn't especially harmful, we -could make our code a little easier to read and maintain by using an "inline" ``if`` -statement. - -.. reactpy:: _examples/good_conditional_todo_list diff --git a/docs/source/guides/escape-hatches/_examples/material_ui_button_no_action.py b/docs/source/guides/escape-hatches/_examples/material_ui_button_no_action.py deleted file mode 100644 index 3ad4dac5b..000000000 --- a/docs/source/guides/escape-hatches/_examples/material_ui_button_no_action.py +++ /dev/null @@ -1,16 +0,0 @@ -from reactpy import component, run, web - -mui = web.module_from_template( - "react@^17.0.0", - "@material-ui/core@4.12.4", - fallback="⌛", -) -Button = web.export(mui, "Button") - - -@component -def HelloWorld(): - return Button({"color": "primary", "variant": "contained"}, "Hello World!") - - -run(HelloWorld) diff --git a/docs/source/guides/escape-hatches/_examples/material_ui_button_on_click.py b/docs/source/guides/escape-hatches/_examples/material_ui_button_on_click.py deleted file mode 100644 index 3fc684005..000000000 --- a/docs/source/guides/escape-hatches/_examples/material_ui_button_on_click.py +++ /dev/null @@ -1,30 +0,0 @@ -import json - -import reactpy - -mui = reactpy.web.module_from_template( - "react@^17.0.0", - "@material-ui/core@4.12.4", - fallback="⌛", -) -Button = reactpy.web.export(mui, "Button") - - -@reactpy.component -def ViewButtonEvents(): - event, set_event = reactpy.hooks.use_state(None) - - return reactpy.html.div( - Button( - { - "color": "primary", - "variant": "contained", - "onClick": lambda event: set_event(event), - }, - "Click Me!", - ), - reactpy.html.pre(json.dumps(event, indent=2)), - ) - - -reactpy.run(ViewButtonEvents) diff --git a/docs/source/guides/escape-hatches/_examples/super_simple_chart/main.py b/docs/source/guides/escape-hatches/_examples/super_simple_chart/main.py deleted file mode 100644 index 4640785f8..000000000 --- a/docs/source/guides/escape-hatches/_examples/super_simple_chart/main.py +++ /dev/null @@ -1,32 +0,0 @@ -from pathlib import Path - -from reactpy import component, run, web - -file = Path(__file__).parent / "super-simple-chart.js" -ssc = web.module_from_file("super-simple-chart", file, fallback="⌛") -SuperSimpleChart = web.export(ssc, "SuperSimpleChart") - - -@component -def App(): - return SuperSimpleChart( - { - "data": [ - {"x": 1, "y": 2}, - {"x": 2, "y": 4}, - {"x": 3, "y": 7}, - {"x": 4, "y": 3}, - {"x": 5, "y": 5}, - {"x": 6, "y": 9}, - {"x": 7, "y": 6}, - ], - "height": 300, - "width": 500, - "color": "royalblue", - "lineWidth": 4, - "axisColor": "silver", - } - ) - - -run(App) diff --git a/docs/source/guides/escape-hatches/_examples/super_simple_chart/super-simple-chart.js b/docs/source/guides/escape-hatches/_examples/super_simple_chart/super-simple-chart.js deleted file mode 100644 index 486e5c363..000000000 --- a/docs/source/guides/escape-hatches/_examples/super_simple_chart/super-simple-chart.js +++ /dev/null @@ -1,82 +0,0 @@ -import { h, render } from "https://unpkg.com/preact?module"; -import htm from "https://unpkg.com/htm?module"; - -const html = htm.bind(h); - -export function bind(node, config) { - return { - create: (component, props, children) => h(component, props, ...children), - render: (element) => render(element, node), - unmount: () => render(null, node), - }; -} - -export function SuperSimpleChart(props) { - const data = props.data; - const lastDataIndex = data.length - 1; - - const options = { - height: props.height || 100, - width: props.width || 100, - color: props.color || "blue", - lineWidth: props.lineWidth || 2, - axisColor: props.axisColor || "black", - }; - - const xData = data.map((point) => point.x); - const yData = data.map((point) => point.y); - - const domain = { - xMin: Math.min(...xData), - xMax: Math.max(...xData), - yMin: Math.min(...yData), - yMax: Math.max(...yData), - }; - - return html`<svg - width="${options.width}px" - height="${options.height}px" - viewBox="0 0 ${options.width} ${options.height}" - > - ${makePath(props, domain, data, options)} ${makeAxis(props, options)} - </svg>`; -} - -function makePath(props, domain, data, options) { - const { xMin, xMax, yMin, yMax } = domain; - const { width, height } = options; - const getSvgX = (x) => ((x - xMin) / (xMax - xMin)) * width; - const getSvgY = (y) => height - ((y - yMin) / (yMax - yMin)) * height; - - let pathD = - `M ${getSvgX(data[0].x)} ${getSvgY(data[0].y)} ` + - data.map(({ x, y }, i) => `L ${getSvgX(x)} ${getSvgY(y)}`).join(" "); - - return html`<path - d="${pathD}" - style=${{ - stroke: options.color, - strokeWidth: options.lineWidth, - fill: "none", - }} - />`; -} - -function makeAxis(props, options) { - return html`<g> - <line - x1="0" - y1=${options.height} - x2=${options.width} - y2=${options.height} - style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }} - /> - <line - x1="0" - y1="0" - x2="0" - y2=${options.height} - style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }} - /> - </g>`; -} diff --git a/docs/source/guides/escape-hatches/distributing-javascript.rst b/docs/source/guides/escape-hatches/distributing-javascript.rst deleted file mode 100644 index 5333742ce..000000000 --- a/docs/source/guides/escape-hatches/distributing-javascript.rst +++ /dev/null @@ -1,307 +0,0 @@ -Distributing Javascript -======================= - -There are two ways that you can distribute your :ref:`Custom Javascript Components`: - -- Using a CDN_ -- In a Python package via PyPI_ - -These options are not mutually exclusive though, and it may be beneficial to support -both options. For example, if you upload your Javascript components to NPM_ and also -bundle your Javascript inside a Python package, in principle your users can determine -which work best for them. Regardless though, either you or, if you give then the choice, -your users, will have to consider the tradeoffs of either approach. - -- :ref:`Distributing Javascript via CDN_` - Most useful in production-grade applications - where its assumed the user has a network connection. In this scenario a CDN's `edge - network <https://en.wikipedia.org/wiki/Edge_computing>`__ can be used to bring the - Javascript source closer to the user in order to reduce page load times. - -- :ref:`Distributing Javascript via PyPI_` - This method is ideal for local usage since - the user can server all the Javascript components they depend on from their computer - without requiring a network connection. - - -Distributing Javascript via CDN_ --------------------------------- - -Under this approach, to simplify these instructions, we're going to ignore the problem -of distributing the Javascript since that must be handled by your CDN. For open source -or personal projects, a CDN like https://unpkg.com/ makes things easy by automatically -preparing any package that's been uploaded to NPM_. If you need to roll with your own -private CDN, this will likely be more complicated. - -In either case though, on the Python side, things are quite simple. You need only pass -the URL where your package can be found to :func:`~reactpy.web.module.module_from_url` -where you can then load any of its exports: - -.. code-block:: - - import reactpy - - your_module = ido.web.module_from_url("https://some.cdn/your-module") - YourComponent = reactpy.web.export(your_module, "YourComponent") - - -Distributing Javascript via PyPI_ ---------------------------------- - -This can be most easily accomplished by using the `template repository`_ that's been -purpose-built for this. However, to get a better sense for its inner workings, we'll -briefly look at what's required. At a high level, we must consider how to... - -1. bundle your Javascript into an `ECMAScript Module`) -2. include that Javascript bundle in a Python package -3. use it as a component in your application using ReactPy - -In the descriptions to follow we'll be assuming that: - -- NPM_ is the Javascript package manager -- The components are implemented with React_ -- Rollup_ bundles the Javascript module -- Setuptools_ builds the Python package - -To start, let's take a look at the file structure we'll be building: - -.. code-block:: text - - your-project - |-- js - | |-- src - | | \-- index.js - | |-- package.json - | \-- rollup.config.js - |-- your_python_package - | |-- __init__.py - | \-- widget.py - |-- Manifest.in - |-- pyproject.toml - \-- setup.py - -``index.js`` should contain the relevant exports (see -:ref:`Custom JavaScript Components` for more info): - -.. code-block:: javascript - - import * as React from "react"; - import * as ReactDOM from "react-dom"; - - export function bind(node, config) { - return { - create: (component, props, children) => - React.createElement(component, props, ...children), - render: (element) => ReactDOM.render(element, node), - unmount: () => ReactDOM.unmountComponentAtNode(node), - }; - } - - // exports for your components - export YourFirstComponent(props) {...}; - export YourSecondComponent(props) {...}; - export YourThirdComponent(props) {...}; - - -Your ``package.json`` should include the following: - -.. code-block:: python - - { - "name": "YOUR-PACKAGE-NAME", - "scripts": { - "build": "rollup --config", - ... - }, - "devDependencies": { - "rollup": "^2.35.1", - "rollup-plugin-commonjs": "^10.1.0", - "rollup-plugin-node-resolve": "^5.2.0", - "rollup-plugin-replace": "^2.2.0", - ... - }, - "dependencies": { - "react": "^17.0.1", - "react-dom": "^17.0.1", - "@reactpy/client": "^0.8.5", - ... - }, - ... - } - -Getting a bit more in the weeds now, your ``rollup.config.js`` file should be designed -such that it drops an ES Module at ``your-project/your_python_package/bundle.js`` since -we'll be writing ``widget.py`` under that assumption. - -.. note:: - - Don't forget to ignore this ``bundle.js`` file when committing code (with a - ``.gitignore`` if you're using Git) since it can always rebuild from the raw - Javascript source in ``your-project/js``. - -.. code-block:: javascript - - import resolve from "rollup-plugin-node-resolve"; - import commonjs from "rollup-plugin-commonjs"; - import replace from "rollup-plugin-replace"; - - export default { - input: "src/index.js", - output: { - file: "../your_python_package/bundle.js", - format: "esm", - }, - plugins: [ - resolve(), - commonjs(), - replace({ - "process.env.NODE_ENV": JSON.stringify("production"), - }), - ] - }; - -Your ``widget.py`` file should then load the neighboring bundle file using -:func:`~reactpy.web.module.module_from_file`. Then components from that bundle can be -loaded with :func:`~reactpy.web.module.export`. - -.. code-block:: - - from pathlib import Path - - import reactpy - - _BUNDLE_PATH = Path(__file__).parent / "bundle.js" - _WEB_MODULE = reactpy.web.module_from_file( - # Note that this is the same name from package.json - this must be globally - # unique since it must share a namespace with all other javascript packages. - name="YOUR-PACKAGE-NAME", - file=_BUNDLE_PATH, - # What to temporarily display while the module is being loaded - fallback="Loading...", - ) - - # Your module must provide a named export for YourFirstComponent - YourFirstComponent = reactpy.web.export(_WEB_MODULE, "YourFirstComponent") - - # It's possible to export multiple components at once - YourSecondComponent, YourThirdComponent = reactpy.web.export( - _WEB_MODULE, ["YourSecondComponent", "YourThirdComponent"] - ) - -.. note:: - - When :data:`reactpy.config.REACTPY_DEBUG` is active, named exports will be validated. - -The remaining files that we need to create are concerned with creating a Python package. -We won't cover all the details here, so refer to the Setuptools_ documentation for -more information. With that said, the first file to fill out is `pyproject.toml` since -we need to declare what our build tool is (in this case Setuptools): - -.. code-block:: toml - - [build-system] - requires = ["setuptools>=40.8.0", "wheel"] - build-backend = "setuptools.build_meta" - -Then, we can create the ``setup.py`` file which uses Setuptools. This will differ -substantially from a normal ``setup.py`` file since, as part of the build process we'll -need to use NPM to bundle our Javascript. This requires customizing some of the build -commands in Setuptools like ``build``, ``sdist``, and ``develop``: - -.. code-block:: python - - import subprocess - from pathlib import Path - - from setuptools import setup, find_packages - from distutils.command.build import build - from distutils.command.sdist import sdist - from setuptools.command.develop import develop - - PACKAGE_SPEC = {} # gets passed to setup() at the end - - - # ----------------------------------------------------------------------------- - # General Package Info - # ----------------------------------------------------------------------------- - - - PACKAGE_NAME = "your_python_package" - - PACKAGE_SPEC.update( - name=PACKAGE_NAME, - version="0.0.1", - packages=find_packages(exclude=["tests*"]), - classifiers=["Framework :: ReactPy", ...], - keywords=["ReactPy", "components", ...], - # install ReactPy with this package - install_requires=["reactpy"], - # required in order to include static files like bundle.js using MANIFEST.in - include_package_data=True, - # we need access to the file system, so cannot be run from a zip file - zip_safe=False, - ) - - - # ---------------------------------------------------------------------------- - # Build Javascript - # ---------------------------------------------------------------------------- - - - # basic paths used to gather files - PROJECT_ROOT = Path(__file__).parent - PACKAGE_DIR = PROJECT_ROOT / PACKAGE_NAME - JS_DIR = PROJECT_ROOT / "js" - - - def build_javascript_first(cls): - class Command(cls): - def run(self): - for cmd_str in ["npm install", "npm run build"]: - subprocess.run(cmd_str.split(), cwd=str(JS_DIR), check=True) - super().run() - - return Command - - - package["cmdclass"] = { - "sdist": build_javascript_first(sdist), - "build": build_javascript_first(build), - "develop": build_javascript_first(develop), - } - - - # ----------------------------------------------------------------------------- - # Run It - # ----------------------------------------------------------------------------- - - - if __name__ == "__main__": - setup(**package) - - -Finally, since we're using ``include_package_data`` you'll need a MANIFEST.in_ file that -includes ``bundle.js``: - -.. code-block:: text - - include your_python_package/bundle.js - -And that's it! While this might seem like a lot of work, you're always free to start -creating your custom components using the provided `template repository`_ so you can get -up and running as quickly as possible. - - -.. Links -.. ===== - -.. _NPM: https://www.npmjs.com -.. _install NPM: https://www.npmjs.com/get-npm -.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network -.. _PyPI: https://pypi.org/ -.. _template repository: https://github.com/reactive-python/reactpy-js-component-template -.. _web module: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules -.. _Rollup: https://rollupjs.org/guide/en/ -.. _Webpack: https://webpack.js.org/ -.. _Setuptools: https://setuptools.readthedocs.io/en/latest/userguide/index.html -.. _ECMAScript Module: https://tc39.es/ecma262/#sec-modules -.. _React: https://reactjs.org -.. _MANIFEST.in: https://packaging.python.org/guides/using-manifest-in/ diff --git a/docs/source/guides/escape-hatches/index.rst b/docs/source/guides/escape-hatches/index.rst deleted file mode 100644 index 3ef1b7122..000000000 --- a/docs/source/guides/escape-hatches/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Escape Hatches -============== - -.. toctree:: - :hidden: - - javascript-components - distributing-javascript - using-a-custom-backend - using-a-custom-client - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/escape-hatches/javascript-components.rst b/docs/source/guides/escape-hatches/javascript-components.rst deleted file mode 100644 index f0a71b6b7..000000000 --- a/docs/source/guides/escape-hatches/javascript-components.rst +++ /dev/null @@ -1,146 +0,0 @@ -.. _Javascript Component: - -Javascript Components -===================== - -While ReactPy is a great tool for displaying HTML and responding to browser events with -pure Python, there are other projects which already allow you to do this inside -`Jupyter Notebooks <https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Basics.html>`__ -or in standard -`web apps <https://blog.jupyter.org/and-voil%C3%A0-f6a2c08a4a93?gi=54b835a2fcce>`__. -The real power of ReactPy comes from its ability to seamlessly leverage the existing -Javascript ecosystem. This can be accomplished in different ways for different reasons: - -.. list-table:: - :header-rows: 1 - - * - Integration Method - - Use Case - - * - :ref:`Dynamically Loaded Components` - - You want to **quickly experiment** with ReactPy and the Javascript ecosystem. - - * - :ref:`Custom Javascript Components` - - You want to create polished software that can be **easily shared** with others. - - -.. _Dynamically Loaded Component: - -Dynamically Loaded Components ------------------------------ - -.. note:: - - This method is not recommended in production systems - see :ref:`Distributing - Javascript` for more info. Instead, it's best used during exploratory phases of - development. - -ReactPy makes it easy to draft your code when you're in the early stages of development by -using a CDN_ to dynamically load Javascript packages on the fly. In this example we'll -be using the ubiquitous React-based UI framework `Material UI`_. - -.. reactpy:: _examples/material_ui_button_no_action - -So now that we can display a Material UI Button we probably want to make it do -something. Thankfully there's nothing new to learn here, you can pass event handlers to -the button just as you did when :ref:`getting started <responding to events>`. Thus, all -we need to do is add an ``onClick`` handler to the component: - -.. reactpy:: _examples/material_ui_button_on_click - - -.. _Custom Javascript Component: - -Custom Javascript Components ----------------------------- - -For projects that will be shared with others, we recommend bundling your Javascript with -Rollup_ or Webpack_ into a `web module`_. ReactPy also provides a `template repository`_ -that can be used as a blueprint to build a library of React components. - -To work as intended, the Javascript bundle must export a function ``bind()`` that -adheres to the following interface: - -.. code-block:: typescript - - type EventData = { - target: string; - data: Array<any>; - } - - type LayoutContext = { - sendEvent(data: EventData) => void; - loadImportSource(source: string, sourceType: "NAME" | "URL") => Module; - } - - type bind = (node: HTMLElement, context: LayoutContext) => ({ - create(type: any, props: Object, children: Array<any>): any; - render(element): void; - unmount(): void; - }); - -.. note:: - - - ``node`` is the ``HTMLElement`` that ``render()`` should mount to. - - - ``context`` can send events back to the server and load "import sources" - (like a custom component module). - - - ``type`` is a named export of the current module, or a string (e.g. ``"div"``, - ``"button"``, etc.) - - - ``props`` is an object containing attributes and callbacks for the given - ``component``. - - - ``children`` is an array of elements which were constructed by recursively calling - ``create``. - -The interface returned by ``bind()`` can be thought of as being similar to that of -React. - -- ``create`` ➜ |React.createElement|_ -- ``render`` ➜ |ReactDOM.render|_ -- ``unmount`` ➜ |ReactDOM.unmountComponentAtNode|_ - -.. |React.createElement| replace:: ``React.createElement`` -.. _React.createElement: https://reactjs.org/docs/react-api.html#createelement - -.. |ReactDOM.render| replace:: ``ReactDOM.render`` -.. _ReactDOM.render: https://reactjs.org/docs/react-dom.html#render - -.. |ReactDOM.unmountComponentAtNode| replace:: ``ReactDOM.unmountComponentAtNode`` -.. _ReactDOM.unmountComponentAtNode: https://reactjs.org/docs/react-api.html#createelement - -It will be used in the following manner: - -.. code-block:: javascript - - // once on mount - const binding = bind(node, context); - - // on every render - let element = binding.create(type, props, children) - binding.render(element); - - // once on unmount - binding.unmount(); - -The simplest way to try this out yourself though, is to hook in a simple hand-crafted -Javascript module that has the requisite interface. In the example to follow we'll -create a very basic SVG line chart. The catch though is that we are limited to using -Javascript that can run directly in the browser. This means we can't use fancy syntax -like `JSX <https://reactjs.org/docs/introducing-jsx.html>`__ and instead will use -`htm <https://github.com/developit/htm>`__ to simulate JSX in plain Javascript. - -.. reactpy:: _examples/super_simple_chart - - -.. Links -.. ===== - -.. _Material UI: https://material-ui.com/ -.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network -.. _template repository: https://github.com/reactive-python/reactpy-js-component-template -.. _web module: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules -.. _Rollup: https://rollupjs.org/guide/en/ -.. _Webpack: https://webpack.js.org/ diff --git a/docs/source/guides/escape-hatches/using-a-custom-backend.rst b/docs/source/guides/escape-hatches/using-a-custom-backend.rst deleted file mode 100644 index f9d21208a..000000000 --- a/docs/source/guides/escape-hatches/using-a-custom-backend.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. _Writing Your Own Backend: -.. _Using a Custom Backend: - -Using a Custom Backend 🚧 -========================= - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/escape-hatches/using-a-custom-client.rst b/docs/source/guides/escape-hatches/using-a-custom-client.rst deleted file mode 100644 index 95de23e59..000000000 --- a/docs/source/guides/escape-hatches/using-a-custom-client.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. _Writing Your Own Client: -.. _Using a Custom Client: - -Using a Custom Client 🚧 -======================== - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/getting-started/_examples/debug_error_example.py b/docs/source/guides/getting-started/_examples/debug_error_example.py deleted file mode 100644 index dd0b212ab..000000000 --- a/docs/source/guides/getting-started/_examples/debug_error_example.py +++ /dev/null @@ -1,20 +0,0 @@ -from reactpy import component, html, run - - -@component -def App(): - return html.div(GoodComponent(), BadComponent()) - - -@component -def GoodComponent(): - return html.p("This component rendered successfully") - - -@component -def BadComponent(): - msg = "This component raised an error" - raise RuntimeError(msg) - - -run(App) diff --git a/docs/source/guides/getting-started/_examples/hello_world.py b/docs/source/guides/getting-started/_examples/hello_world.py deleted file mode 100644 index f38d9e38f..000000000 --- a/docs/source/guides/getting-started/_examples/hello_world.py +++ /dev/null @@ -1,9 +0,0 @@ -from reactpy import component, html, run - - -@component -def App(): - return html.h1("Hello, world!") - - -run(App) diff --git a/docs/source/guides/getting-started/_examples/run_fastapi.py b/docs/source/guides/getting-started/_examples/run_fastapi.py deleted file mode 100644 index bb02e9d6a..000000000 --- a/docs/source/guides/getting-started/_examples/run_fastapi.py +++ /dev/null @@ -1,22 +0,0 @@ -# :lines: 11- - -from reactpy import run -from reactpy.backend import fastapi as fastapi_server - -# the run() function is the entry point for examples -fastapi_server.configure = lambda _, cmpt: run(cmpt) - - -from fastapi import FastAPI - -from reactpy import component, html -from reactpy.backend.fastapi import configure - - -@component -def HelloWorld(): - return html.h1("Hello, world!") - - -app = FastAPI() -configure(app, HelloWorld) diff --git a/docs/source/guides/getting-started/_examples/run_flask.py b/docs/source/guides/getting-started/_examples/run_flask.py deleted file mode 100644 index f98753784..000000000 --- a/docs/source/guides/getting-started/_examples/run_flask.py +++ /dev/null @@ -1,22 +0,0 @@ -# :lines: 11- - -from reactpy import run -from reactpy.backend import flask as flask_server - -# the run() function is the entry point for examples -flask_server.configure = lambda _, cmpt: run(cmpt) - - -from flask import Flask - -from reactpy import component, html -from reactpy.backend.flask import configure - - -@component -def HelloWorld(): - return html.h1("Hello, world!") - - -app = Flask(__name__) -configure(app, HelloWorld) diff --git a/docs/source/guides/getting-started/_examples/run_sanic.py b/docs/source/guides/getting-started/_examples/run_sanic.py deleted file mode 100644 index 1dae9f6e0..000000000 --- a/docs/source/guides/getting-started/_examples/run_sanic.py +++ /dev/null @@ -1,26 +0,0 @@ -# :lines: 11- - -from reactpy import run -from reactpy.backend import sanic as sanic_server - -# the run() function is the entry point for examples -sanic_server.configure = lambda _, cmpt: run(cmpt) - - -from sanic import Sanic - -from reactpy import component, html -from reactpy.backend.sanic import configure - - -@component -def HelloWorld(): - return html.h1("Hello, world!") - - -app = Sanic("MyApp") -configure(app, HelloWorld) - - -if __name__ == "__main__": - app.run(port=8000) diff --git a/docs/source/guides/getting-started/_examples/run_starlette.py b/docs/source/guides/getting-started/_examples/run_starlette.py deleted file mode 100644 index 47a08fb47..000000000 --- a/docs/source/guides/getting-started/_examples/run_starlette.py +++ /dev/null @@ -1,22 +0,0 @@ -# :lines: 10- - -from reactpy import run -from reactpy.backend import starlette as starlette_server - -# the run() function is the entry point for examples -starlette_server.configure = lambda _, cmpt: run(cmpt) - - -from starlette.applications import Starlette - -from reactpy import component, html -from reactpy.backend.starlette import configure - - -@component -def HelloWorld(): - return html.h1("Hello, world!") - - -app = Starlette() -configure(app, HelloWorld) diff --git a/docs/source/guides/getting-started/_examples/run_tornado.py b/docs/source/guides/getting-started/_examples/run_tornado.py deleted file mode 100644 index b86126e63..000000000 --- a/docs/source/guides/getting-started/_examples/run_tornado.py +++ /dev/null @@ -1,31 +0,0 @@ -# :lines: 11- - -from reactpy import run -from reactpy.backend import tornado as tornado_server - -# the run() function is the entry point for examples -tornado_server.configure = lambda _, cmpt: run(cmpt) - - -import tornado.ioloop -import tornado.web - -from reactpy import component, html -from reactpy.backend.tornado import configure - - -@component -def HelloWorld(): - return html.h1("Hello, world!") - - -def make_app(): - app = tornado.web.Application() - configure(app, HelloWorld) - return app - - -if __name__ == "__main__": - app = make_app() - app.listen(8000) - tornado.ioloop.IOLoop.current().start() diff --git a/docs/source/guides/getting-started/_examples/sample_app.py b/docs/source/guides/getting-started/_examples/sample_app.py deleted file mode 100644 index a1cc34e6d..000000000 --- a/docs/source/guides/getting-started/_examples/sample_app.py +++ /dev/null @@ -1,3 +0,0 @@ -import reactpy - -reactpy.run(reactpy.sample.SampleApp) diff --git a/docs/source/guides/getting-started/_static/embed-doc-ex.html b/docs/source/guides/getting-started/_static/embed-doc-ex.html deleted file mode 100644 index 589cb5d80..000000000 --- a/docs/source/guides/getting-started/_static/embed-doc-ex.html +++ /dev/null @@ -1,8 +0,0 @@ -<div id="reactpy-app" /> -<script type="module"> - import { mountLayoutWithWebSocket } from "https://esm.sh/@reactpy/client"; - mountLayoutWithWebSocket( - document.getElementById("reactpy-app"), - "wss://reactpy.dev/_reactpy/stream?view_id=todo" - ); -</script> diff --git a/docs/source/guides/getting-started/_static/embed-reactpy-view/index.html b/docs/source/guides/getting-started/_static/embed-reactpy-view/index.html deleted file mode 100644 index 146d715e4..000000000 --- a/docs/source/guides/getting-started/_static/embed-reactpy-view/index.html +++ /dev/null @@ -1,31 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <meta charset="UTF-8" /> - <title>Example App - - -

This is an Example App

-

Just below is an embedded ReactPy view...

-
- - - diff --git a/docs/source/guides/getting-started/_static/embed-reactpy-view/main.py b/docs/source/guides/getting-started/_static/embed-reactpy-view/main.py deleted file mode 100644 index 6e3687f27..000000000 --- a/docs/source/guides/getting-started/_static/embed-reactpy-view/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from sanic import Sanic -from sanic.response import file - -from reactpy import component, html -from reactpy.backend.sanic import Options, configure - -app = Sanic("MyApp") - - -@app.route("/") -async def index(request): - return await file("index.html") - - -@component -def ReactPyView(): - return html.code("This text came from an ReactPy App") - - -configure(app, ReactPyView, Options(url_prefix="/_reactpy")) - -app.run(host="127.0.0.1", port=5000) diff --git a/docs/source/guides/getting-started/_static/embed-reactpy-view/screenshot.png b/docs/source/guides/getting-started/_static/embed-reactpy-view/screenshot.png deleted file mode 100644 index 7439c83cf..000000000 Binary files a/docs/source/guides/getting-started/_static/embed-reactpy-view/screenshot.png and /dev/null differ diff --git a/docs/source/guides/getting-started/_static/logo-django.svg b/docs/source/guides/getting-started/_static/logo-django.svg deleted file mode 100644 index 1538f0817..000000000 --- a/docs/source/guides/getting-started/_static/logo-django.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - -]> - - - - - - - - - - - - diff --git a/docs/source/guides/getting-started/_static/logo-jupyter.svg b/docs/source/guides/getting-started/_static/logo-jupyter.svg deleted file mode 100644 index fb2921a41..000000000 --- a/docs/source/guides/getting-started/_static/logo-jupyter.svg +++ /dev/null @@ -1,88 +0,0 @@ - -logo.svg -Created using Figma 0.90 - - - - - - - - - - - - - - - - - - diff --git a/docs/source/guides/getting-started/_static/logo-plotly.svg b/docs/source/guides/getting-started/_static/logo-plotly.svg deleted file mode 100644 index 3dd95459a..000000000 --- a/docs/source/guides/getting-started/_static/logo-plotly.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/docs/source/guides/getting-started/_static/reactpy-in-jupyterlab.gif b/docs/source/guides/getting-started/_static/reactpy-in-jupyterlab.gif deleted file mode 100644 index b420ecd8c..000000000 Binary files a/docs/source/guides/getting-started/_static/reactpy-in-jupyterlab.gif and /dev/null differ diff --git a/docs/source/guides/getting-started/_static/shared-client-state-server-slider.gif b/docs/source/guides/getting-started/_static/shared-client-state-server-slider.gif deleted file mode 100644 index 61bb8295f..000000000 Binary files a/docs/source/guides/getting-started/_static/shared-client-state-server-slider.gif and /dev/null differ diff --git a/docs/source/guides/getting-started/index.rst b/docs/source/guides/getting-started/index.rst deleted file mode 100644 index dd210be60..000000000 --- a/docs/source/guides/getting-started/index.rst +++ /dev/null @@ -1,123 +0,0 @@ -Getting Started -=============== - -.. toctree:: - :hidden: - - installing-reactpy - running-reactpy - -.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn - :color: info - :animate: fade-in - :open: - - .. grid:: 1 2 2 2 - - .. grid-item-card:: :octicon:`tools` Installing ReactPy - :link: installing-reactpy - :link-type: doc - - Learn how ReactPy can be installed in a variety of different ways - with - different web servers and even in different frameworks. - - .. grid-item-card:: :octicon:`play` Running ReactPy - :link: running-reactpy - :link-type: doc - - See how ReactPy can be run with a variety of different production servers or be - added to existing applications. - -The fastest way to get started with ReactPy is to try it out in a `Juptyer Notebook -`__. -If you want to use a Notebook to work through the examples shown in this documentation, -you'll need to replace calls to ``reactpy.run(App)`` with a line at the end of each cell -that constructs the ``App()`` in question. If that doesn't make sense, the introductory -notebook linked below will demonstrate how to do this: - -.. card:: - :link: https://mybinder.org/v2/gh/reactive-python/reactpy-jupyter/main?urlpath=lab/tree/notebooks/introduction.ipynb - - .. image:: _static/reactpy-in-jupyterlab.gif - :scale: 72% - :align: center - - -Section 1: Installing ReactPy ------------------------------ - -The next fastest option is to install ReactPy along with a supported server (like -``starlette``) with ``pip``: - -.. code-block:: bash - - pip install "reactpy[starlette]" - -To check that everything is working you can run the sample application: - -.. code-block:: bash - - python -c "import reactpy; reactpy.run(reactpy.sample.SampleApp)" - -.. note:: - - This launches a simple development server which is good enough for testing, but - probably not what you want to use in production. When deploying in production, - there's a number of different ways of :ref:`running ReactPy
`. - -You should then see a few log messages: - -.. code-block:: text - - 2022-03-27T11:58:59-0700 | WARNING | You are running a development server. Change this before deploying in production! - 2022-03-27T11:58:59-0700 | INFO | Running with 'Starlette' at http://127.0.0.1:8000 - -The second log message includes a URL indicating where you should go to view the app. -That will usually be http://127.0.0.1:8000. Once you go to that URL you should see -something like this: - -.. card:: - - .. reactpy-view:: _examples/sample_app - -If you get a ``RuntimeError`` similar to the following: - -.. code-block:: text - - Found none of the following builtin server implementations... - -Then be sure you run ``pip install "reactpy[starlette]"`` instead of just ``reactpy``. For -anything else, report your issue in ReactPy's :discussion-type:`discussion forum -`. - -.. card:: - :link: installing-reactpy - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Learn how ReactPy can be installed in a variety of different ways - with different web - servers and even in different frameworks. - - -Section 2: Running ReactPy --------------------------- - -Once you've :ref:`installed ReactPy `, you'll want to learn how to run an -application. Throughout most of the examples in this documentation, you'll see the -:func:`~reactpy.backend.utils.run` function used. While it's convenient tool for -development it shouldn't be used in production settings - it's slow, and could leak -secrets through debug log messages. - -.. reactpy:: _examples/hello_world - -.. card:: - :link: running-reactpy - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - See how ReactPy can be run with a variety of different production servers or be - added to existing applications. diff --git a/docs/source/guides/getting-started/installing-reactpy.rst b/docs/source/guides/getting-started/installing-reactpy.rst deleted file mode 100644 index 869dbcb02..000000000 --- a/docs/source/guides/getting-started/installing-reactpy.rst +++ /dev/null @@ -1,121 +0,0 @@ -Installing ReactPy -================== - -You will typically ``pip`` install ReactPy to alongside one of it's natively supported -backends. For example, if we want to run ReactPy using the `Starlette -`__ backend you would run - -.. code-block:: bash - - pip install "reactpy[starlette]" - -If you want to install a "pure" version of ReactPy **without a backend implementation** -you can do so without any installation extras. You might do this if you wanted to -:ref:`use a custom backend ` or if you wanted to manually pin -the dependencies for your chosen backend: - -.. code-block:: bash - - pip install reactpy - - -Native Backends ---------------- - -ReactPy includes built-in support for a variety backend implementations. To install the -required dependencies for each you should substitute ``starlette`` from the ``pip -install`` command above with one of the options below: - -- ``fastapi`` - https://fastapi.tiangolo.com -- ``flask`` - https://palletsprojects.com/p/flask/ -- ``sanic`` - https://sanicframework.org -- ``starlette`` - https://www.starlette.io/ -- ``tornado`` - https://www.tornadoweb.org/en/stable/ - -If you need to, you can install more than one option by separating them with commas: - -.. code-block:: bash - - pip install "reactpy[fastapi,flask,sanic,starlette,tornado]" - -Once this is complete you should be able to :ref:`run ReactPy ` with your -chosen implementation. - - -Other Backends --------------- - -While ReactPy can run in a variety of contexts, sometimes frameworks require extra work in -order to integrate with them. In these cases, the ReactPy team distributes bindings for -those frameworks as separate Python packages. For documentation on how to install and -run ReactPy in these supported frameworks, follow the links below: - -.. raw:: html - - - -.. role:: transparent-text-color - -.. We add transparent-text-color to the text so it's not visible, but it's still -.. searchable. - -.. grid:: 3 - - .. grid-item-card:: - :link: https://github.com/reactive-python/reactpy-django - :img-background: _static/logo-django.svg - :class-card: card-logo-image - - :transparent-text-color:`Django` - - .. grid-item-card:: - :link: https://github.com/reactive-python/reactpy-jupyter - :img-background: _static/logo-jupyter.svg - :class-card: card-logo-image - - :transparent-text-color:`Jupyter` - - .. grid-item-card:: - :link: https://github.com/reactive-python/reactpy-dash - :img-background: _static/logo-plotly.svg - :class-card: card-logo-image - - :transparent-text-color:`Plotly Dash` - - -For Development ---------------- - -If you want to contribute to the development of ReactPy or modify it, you'll want to -install a development version of ReactPy. This involves cloning the repository where ReactPy's -source is maintained, and setting up a :ref:`Contributor Guide`. From there you'll -be able to modifying ReactPy's source code and :ref:`run its tests ` to -ensure the modifications you've made are backwards compatible. If you want to add a new -feature to ReactPy you should write your own test that validates its behavior. - -If you have questions about how to modify ReactPy or help with its development, be sure to -:discussion:`start a discussion `. The ReactPy team are always -excited to welcome new contributions and contributors -of all kinds - -.. card:: - :link: /about/contributor-guide - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Learn more about how to contribute to the development of ReactPy. diff --git a/docs/source/guides/getting-started/running-reactpy.rst b/docs/source/guides/getting-started/running-reactpy.rst deleted file mode 100644 index 90a03cbc3..000000000 --- a/docs/source/guides/getting-started/running-reactpy.rst +++ /dev/null @@ -1,221 +0,0 @@ -Running ReactPy -=============== - -The simplest way to run ReactPy is with the :func:`~reactpy.backend.utils.run` function. This -is the method you'll see used throughout this documentation. However, this executes your -application using a development server which is great for testing, but probably not what -if you're :ref:`deploying in production `. Below are some -more robust and performant ways of running ReactPy with various supported servers. - - -Running ReactPy in Production ------------------------------ - -The first thing you'll need to do if you want to run ReactPy in production is choose a -backend implementation and follow its documentation on how to create and run an -application. This is the backend :ref:`you probably chose ` when -installing ReactPy. Then you'll need to configure that application with an ReactPy view. We -show the basics of how to set up, and then run, each supported backend below, but all -implementations will follow a pattern similar to the following: - -.. code-block:: - - from my_chosen_backend import Application - - from reactpy import component, html - from reactpy.backend.my_chosen_backend import configure - - - @component - def HelloWorld(): - return html.h1("Hello, world!") - - - app = Application() - configure(app, HelloWorld) - -You'll then run this ``app`` using an `ASGI `__ -or `WSGI `__ server from the command line. - - -Running with `FastAPI `__ -....................................................... - -.. reactpy:: _examples/run_fastapi - -Then assuming you put this in ``main.py``, you can run the ``app`` using the `Uvicorn -`__ ASGI server: - -.. code-block:: bash - - uvicorn main:app - - -Running with `Flask `__ -............................................................. - -.. reactpy:: _examples/run_flask - -Then assuming you put this in ``main.py``, you can run the ``app`` using the `Gunicorn -`__ WSGI server: - -.. code-block:: bash - - gunicorn main:app - - -Running with `Sanic `__ -................................................... - -.. reactpy:: _examples/run_sanic - -Then assuming you put this in ``main.py``, you can run the ``app`` using Sanic's builtin -server: - -.. code-block:: bash - - sanic main.app - - -Running with `Starlette `__ -...................................................... - -.. reactpy:: _examples/run_starlette - -Then assuming you put this in ``main.py``, you can run the application using the -`Uvicorn `__ ASGI server: - -.. code-block:: bash - - uvicorn main:app - - -Running with `Tornado `__ -................................................................ - -.. reactpy:: _examples/run_tornado - -Tornado is run using it's own builtin server rather than an external WSGI or ASGI -server. - - -Running ReactPy in Debug Mode ------------------------------ - -ReactPy provides a debug mode that is turned off by default. This can be enabled when you -run your application by setting the ``REACTPY_DEBUG`` environment variable. - -.. tab-set:: - - .. tab-item:: Unix Shell - - .. code-block:: - - export REACTPY_DEBUG=1 - python my_reactpy_app.py - - .. tab-item:: Command Prompt - - .. code-block:: text - - set REACTPY_DEBUG=1 - python my_reactpy_app.py - - .. tab-item:: PowerShell - - .. code-block:: powershell - - $env:REACTPY_DEBUG = "1" - python my_reactpy_app.py - -.. danger:: - - Leave debug mode off in production! - -Among other things, running in this mode: - -- Turns on debug log messages -- Adds checks to ensure the :ref:`VDOM` spec is adhered to -- Displays error messages that occur within your app - -Errors will be displayed where the uppermost component is located in the view: - -.. reactpy:: _examples/debug_error_example - - -Backend Configuration Options ------------------------------ - -ReactPy's various backend implementations come with ``Options`` that can be passed to their -respective ``configure()`` functions in the following way: - -.. code-block:: - - from reactpy.backend. import configure, Options - - configure(app, MyComponent, Options(...)) - -To learn more read about the options for your chosen backend ````: - -- :class:`reactpy.backend.fastapi.Options` -- :class:`reactpy.backend.flask.Options` -- :class:`reactpy.backend.sanic.Options` -- :class:`reactpy.backend.starlette.Options` -- :class:`reactpy.backend.tornado.Options` - - -Embed in an Existing Webpage ----------------------------- - -ReactPy provides a Javascript client called ``@reactpy/client`` that can be used to embed -ReactPy views within an existing applications. This is actually how the interactive -examples throughout this documentation have been created. You can try this out by -embedding one the examples from this documentation into your own webpage: - -.. tab-set:: - - .. tab-item:: HTML - - .. literalinclude:: _static/embed-doc-ex.html - :language: html - - .. tab-item:: ▶️ Result - - .. raw:: html - :file: _static/embed-doc-ex.html - -.. note:: - - For more information on how to use the client see the :ref:`Javascript API` - reference. Or if you need to, your can :ref:`write your own backend implementation - `. - -As mentioned though, this is connecting to the server that is hosting this -documentation. If you want to connect to a view from your own server, you'll need to -change the URL above to one you provide. One way to do this might be to add to an -existing application. Another would be to run ReactPy in an adjacent web server instance -that you coordinate with something like `NGINX `__. For the sake -of simplicity, we'll assume you do something similar to the following in an existing -Python app: - -.. tab-set:: - - .. tab-item:: main.py - - .. literalinclude:: _static/embed-reactpy-view/main.py - :language: python - - .. tab-item:: index.html - - .. literalinclude:: _static/embed-reactpy-view/index.html - :language: html - -After running ``python main.py``, you should be able to navigate to -``http://127.0.0.1:8000/index.html`` and see: - -.. card:: - :text-align: center - - .. image:: _static/embed-reactpy-view/screenshot.png - :width: 500px - diff --git a/docs/source/guides/managing-state/combining-contexts-and-reducers/index.rst b/docs/source/guides/managing-state/combining-contexts-and-reducers/index.rst deleted file mode 100644 index b9f274f0a..000000000 --- a/docs/source/guides/managing-state/combining-contexts-and-reducers/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -Combining Contexts and Reducers 🚧 -================================== - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/managing-state/deeply-sharing-state-with-contexts/index.rst b/docs/source/guides/managing-state/deeply-sharing-state-with-contexts/index.rst deleted file mode 100644 index 4a00caa48..000000000 --- a/docs/source/guides/managing-state/deeply-sharing-state-with-contexts/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -Deeply Sharing State with Contexts 🚧 -===================================== - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/managing-state/how-to-structure-state/index.rst b/docs/source/guides/managing-state/how-to-structure-state/index.rst deleted file mode 100644 index 5092370a5..000000000 --- a/docs/source/guides/managing-state/how-to-structure-state/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _Structuring Your State: - -How to Structure State 🚧 -========================= - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/managing-state/index.rst b/docs/source/guides/managing-state/index.rst deleted file mode 100644 index 0578bafdd..000000000 --- a/docs/source/guides/managing-state/index.rst +++ /dev/null @@ -1,127 +0,0 @@ -Managing State -============== - -.. toctree:: - :hidden: - - how-to-structure-state/index - sharing-component-state/index - when-and-how-to-reset-state/index - simplifying-updates-with-reducers/index - deeply-sharing-state-with-contexts/index - combining-contexts-and-reducers/index - -.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn - :color: info - :animate: fade-in - :open: - - .. grid:: 1 2 2 2 - - .. grid-item-card:: :octicon:`organization` How to Structure State - :link: how-to-structure-state/index - :link-type: doc - - Make it easy to reason about your application with strategies for organizing - state. - - .. grid-item-card:: :octicon:`link` Sharing Component State - :link: sharing-component-state/index - :link-type: doc - - Allow components to vary vary together, by lifting state into common - parents. - - .. grid-item-card:: :octicon:`light-bulb` When and How to Reset State - :link: when-and-how-to-reset-state/index - :link-type: doc - - Control if and how state is preserved by understanding it's relationship to - the "UI tree". - - .. grid-item-card:: :octicon:`plug` Simplifying Updates with Reducers - :link: simplifying-updates-with-reducers/index - :link-type: doc - - Consolidate state update logic outside your component in a single function, - called a “reducer". - - .. grid-item-card:: :octicon:`broadcast` Deeply Sharing State with Contexts - :link: deeply-sharing-state-with-contexts/index - :link-type: doc - - Instead of passing shared state down deep component trees, bring state into - "contexts" instead. - - .. grid-item-card:: :octicon:`rocket` Combining Contexts and Reducers - :link: combining-contexts-and-reducers/index - :link-type: doc - - You can combine reducers and context together to manage state of a complex - screen. - - -Section 1: How to Structure State ---------------------------------- - -.. note:: - - Under construction 🚧 - - -Section 2: Shared Component State ---------------------------------- - -Sometimes, you want the state of two components to always change together. To do it, -remove state from both of them, move it to their closest common parent, and then pass it -down to them via props. This is known as “lifting state up”, and it’s one of the most -common things you will do writing code with ReactPy. - -In the example below the search input and the list of elements below share the same -state, the state represents the food name. Note how the component ``Table`` gets called -at each change of state. The component is observing the state and reacting to state -changes automatically, just like it would do in React. - -.. reactpy:: sharing-component-state/_examples/synced_inputs - -.. card:: - :link: sharing-component-state/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Allow components to vary vary together, by lifting state into common parents. - - -Section 3: When and How to Reset State --------------------------------------- - -.. note:: - - Under construction 🚧 - - -Section 4: Simplifying Updates with Reducers --------------------------------------------- - -.. note:: - - Under construction 🚧 - - -Section 5: Deeply Sharing State with Contexts ---------------------------------------------- - -.. note:: - - Under construction 🚧 - - - -Section 6: Combining Contexts and Reducers ------------------------------------------- - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/data.json b/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/data.json deleted file mode 100644 index f977fe9a7..000000000 --- a/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/data.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "name": "Sushi", - "description": "Sushi is a traditional Japanese dish of prepared vinegared rice" - }, - { - "name": "Dal", - "description": "The most common way of preparing dal is in the form of a soup to which onions, tomatoes and various spices may be added" - }, - { - "name": "Pierogi", - "description": "Pierogi are filled dumplings made by wrapping unleavened dough around a savoury or sweet filling and cooking in boiling water" - }, - { - "name": "Shish Kebab", - "description": "Shish kebab is a popular meal of skewered and grilled cubes of meat" - }, - { - "name": "Dim sum", - "description": "Dim sum is a large range of small dishes that Cantonese people traditionally enjoy in restaurants for breakfast and lunch" - } -] diff --git a/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py b/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py deleted file mode 100644 index ca68aedcb..000000000 --- a/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py +++ /dev/null @@ -1,44 +0,0 @@ -import json -from pathlib import Path - -from reactpy import component, hooks, html, run - -HERE = Path(__file__) -DATA_PATH = HERE.parent / "data.json" -food_data = json.loads(DATA_PATH.read_text()) - - -@component -def FilterableList(): - value, set_value = hooks.use_state("") - return html.p(Search(value, set_value), html.hr(), Table(value, set_value)) - - -@component -def Search(value, set_value): - def handle_change(event): - set_value(event["target"]["value"]) - - return html.label( - "Search by Food Name: ", - html.input({"value": value, "on_change": handle_change}), - ) - - -@component -def Table(value, set_value): - rows = [] - for row in food_data: - name = html.td(row["name"]) - descr = html.td(row["description"]) - tr = html.tr(name, descr, value) - if not value: - rows.append(tr) - elif value.lower() in row["name"].lower(): - rows.append(tr) - headers = html.tr(html.td(html.b("name")), html.td(html.b("description"))) - table = html.table(html.thead(headers), html.tbody(rows)) - return table - - -run(FilterableList) diff --git a/docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py b/docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py deleted file mode 100644 index e8bcdf333..000000000 --- a/docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py +++ /dev/null @@ -1,23 +0,0 @@ -from reactpy import component, hooks, html, run - - -@component -def SyncedInputs(): - value, set_value = hooks.use_state("") - return html.p( - Input("First input", value, set_value), - Input("Second input", value, set_value), - ) - - -@component -def Input(label, value, set_value): - def handle_change(event): - set_value(event["target"]["value"]) - - return html.label( - label + " ", html.input({"value": value, "on_change": handle_change}) - ) - - -run(SyncedInputs) diff --git a/docs/source/guides/managing-state/sharing-component-state/index.rst b/docs/source/guides/managing-state/sharing-component-state/index.rst deleted file mode 100644 index 54b61335a..000000000 --- a/docs/source/guides/managing-state/sharing-component-state/index.rst +++ /dev/null @@ -1,38 +0,0 @@ -Sharing Component State -======================= - -.. note:: - - Parts of this document are still under construction 🚧 - -Sometimes, you want the state of two components to always change together. To do it, -remove state from both of them, move it to their closest common parent, and then pass it -down to them via props. This is known as “lifting state up”, and it’s one of the most -common things you will do writing code with ReactPy. - - -Synced Inputs -------------- - -In the code below the two input boxes are synchronized, this happens because they share -state. The state is shared via the parent component ``SyncedInputs``. Check the ``value`` -and ``set_value`` variables. - -.. reactpy:: _examples/synced_inputs - - -Filterable List ----------------- - -In the example below the search input and the list of elements below share the -same state, the state represents the food name. - -Note how the component ``Table`` gets called at each change of state. The -component is observing the state and reacting to state changes automatically, -just like it would do in React. - -.. reactpy:: _examples/filterable_list - -.. note:: - - Try typing a food name in the search bar. diff --git a/docs/source/guides/managing-state/simplifying-updates-with-reducers/index.rst b/docs/source/guides/managing-state/simplifying-updates-with-reducers/index.rst deleted file mode 100644 index 08fce5a69..000000000 --- a/docs/source/guides/managing-state/simplifying-updates-with-reducers/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -Simplifying Updates with Reducers 🚧 -==================================== - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/managing-state/when-and-how-to-reset-state/index.rst b/docs/source/guides/managing-state/when-and-how-to-reset-state/index.rst deleted file mode 100644 index 6a96f4b30..000000000 --- a/docs/source/guides/managing-state/when-and-how-to-reset-state/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _When to Reset State: - -When and How to Reset State 🚧 -============================== - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/understanding-reactpy/_static/idom-flow-diagram.svg b/docs/source/guides/understanding-reactpy/_static/idom-flow-diagram.svg deleted file mode 100644 index 9077913ca..000000000 --- a/docs/source/guides/understanding-reactpy/_static/idom-flow-diagram.svg +++ /dev/null @@ -1,383 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - layout - - - - - component - - server - - - - view - - client - - - - event - VDOM diff - VDOM - diff --git a/docs/source/guides/understanding-reactpy/_static/live-examples-in-docs.gif b/docs/source/guides/understanding-reactpy/_static/live-examples-in-docs.gif deleted file mode 100644 index 96a04d68b..000000000 Binary files a/docs/source/guides/understanding-reactpy/_static/live-examples-in-docs.gif and /dev/null differ diff --git a/docs/source/guides/understanding-reactpy/_static/mvc-flow-diagram.svg b/docs/source/guides/understanding-reactpy/_static/mvc-flow-diagram.svg deleted file mode 100644 index a1acbc2cb..000000000 --- a/docs/source/guides/understanding-reactpy/_static/mvc-flow-diagram.svg +++ /dev/null @@ -1,425 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - model - - - - - controller - - server - - - - - model - - - - - view - - client - - - - - event - sync - change - render - diff --git a/docs/source/guides/understanding-reactpy/_static/npm-download-trends.png b/docs/source/guides/understanding-reactpy/_static/npm-download-trends.png deleted file mode 100644 index cf5140b0d..000000000 Binary files a/docs/source/guides/understanding-reactpy/_static/npm-download-trends.png and /dev/null differ diff --git a/docs/source/guides/understanding-reactpy/index.rst b/docs/source/guides/understanding-reactpy/index.rst deleted file mode 100644 index 3e0b2ab72..000000000 --- a/docs/source/guides/understanding-reactpy/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -Understanding ReactPy -===================== - -.. toctree:: - :hidden: - - representing-html - what-are-components - the-rendering-pipeline - why-reactpy-needs-keys - the-rendering-process - layout-render-servers - writing-tests - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/understanding-reactpy/layout-render-servers.rst b/docs/source/guides/understanding-reactpy/layout-render-servers.rst deleted file mode 100644 index 9a7cceb54..000000000 --- a/docs/source/guides/understanding-reactpy/layout-render-servers.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _Layout Render Servers: - -Layout Render Servers 🚧 -======================== - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/understanding-reactpy/representing-html.rst b/docs/source/guides/understanding-reactpy/representing-html.rst deleted file mode 100644 index c2f32ebd9..000000000 --- a/docs/source/guides/understanding-reactpy/representing-html.rst +++ /dev/null @@ -1,76 +0,0 @@ -.. _Representing HTML: - -Representing HTML 🚧 -==================== - -.. note:: - - Under construction 🚧 - -We've already discussed how to construct HTML with ReactPy in a :ref:`previous section `, but we skimmed over the question of the data structure we use to represent -it. Let's reconsider the examples from before - on the top is some HTML and on the -bottom is the corresponding code to create it in ReactPy: - -.. code-block:: html - -
-

My Todo List

-
    -
  • Build a cool new app
  • -
  • Share it with the world!
  • -
-
- -.. testcode:: - - from reactpy import html - - layout = html.div( - html.h1("My Todo List"), - html.ul( - html.li("Build a cool new app"), - html.li("Share it with the world!"), - ) - ) - -Since we've captured our HTML into out the ``layout`` variable, we can inspect what it -contains. And, as it turns out, it holds a dictionary. Printing it produces the -following output: - -.. testsetup:: - - from pprint import pprint - print = lambda *args, **kwargs: pprint(*args, **kwargs, sort_dicts=False) - -.. testcode:: - - assert layout == { - 'tagName': 'div', - 'children': [ - { - 'tagName': 'h1', - 'children': ['My Todo List'] - }, - { - 'tagName': 'ul', - 'children': [ - {'tagName': 'li', 'children': ['Build a cool new app']}, - {'tagName': 'li', 'children': ['Share it with the world!']} - ] - } - ] - } - -This may look complicated, but let's take a moment to consider what's going on here. We -have a series of nested dictionaries that, in some way, represents the HTML structure -given above. If we look at their contents we should see a common form. Each has a -``tagName`` key which contains, as the name would suggest, the tag name of an HTML -element. Then within the ``children`` key is a list that either contains strings or -other dictionaries that represent HTML elements. - -What we're seeing here is called a "virtual document object model" or :ref:`VDOM`. This -is just a fancy way of saying we have a representation of the document object model or -`DOM -`__ -that is not the actual DOM. diff --git a/docs/source/guides/understanding-reactpy/the-rendering-pipeline.rst b/docs/source/guides/understanding-reactpy/the-rendering-pipeline.rst deleted file mode 100644 index cdde27f08..000000000 --- a/docs/source/guides/understanding-reactpy/the-rendering-pipeline.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _The Rendering Pipeline: - -The Rendering Pipeline 🚧 -========================= - -.. talk about layouts and dispatchers - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/understanding-reactpy/the-rendering-process.rst b/docs/source/guides/understanding-reactpy/the-rendering-process.rst deleted file mode 100644 index 00215a887..000000000 --- a/docs/source/guides/understanding-reactpy/the-rendering-process.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _The Rendering Process: - -The Rendering Process 🚧 -======================== - -.. refer to https://beta.reactjs.org/learn/render-and-commit - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/understanding-reactpy/what-are-components.rst b/docs/source/guides/understanding-reactpy/what-are-components.rst deleted file mode 100644 index 4c22dda13..000000000 --- a/docs/source/guides/understanding-reactpy/what-are-components.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _What Are Components: - -What Are Components? 🚧 -======================= - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/understanding-reactpy/why-reactpy-needs-keys.rst b/docs/source/guides/understanding-reactpy/why-reactpy-needs-keys.rst deleted file mode 100644 index e570b8f41..000000000 --- a/docs/source/guides/understanding-reactpy/why-reactpy-needs-keys.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _Why ReactPy Needs Keys: - -Why ReactPy Needs Keys 🚧 -========================= - -.. note:: - - Under construction 🚧 diff --git a/docs/source/guides/understanding-reactpy/writing-tests.rst b/docs/source/guides/understanding-reactpy/writing-tests.rst deleted file mode 100644 index ffac27df6..000000000 --- a/docs/source/guides/understanding-reactpy/writing-tests.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _Writing Tests: - -Writing Tests 🚧 -================ - -.. note:: - - Under construction 🚧 diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 8b21160f6..000000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,206 +0,0 @@ -.. card:: - - This documentation is still under construction 🚧. We welcome your `feedback - `__! - - -ReactPy -======= - -.. toctree:: - :hidden: - :caption: Guides - - guides/getting-started/index - guides/creating-interfaces/index - guides/adding-interactivity/index - guides/managing-state/index - guides/escape-hatches/index - guides/understanding-reactpy/index - -.. toctree:: - :hidden: - :caption: Reference - - reference/browser-events - reference/html-attributes - reference/hooks-api - _auto/apis - reference/javascript-api - reference/specifications - -.. toctree:: - :hidden: - :caption: About - - about/changelog - about/contributor-guide - about/credits-and-licenses - Source Code - Community - -ReactPy is a library for building user interfaces in Python without Javascript. ReactPy -interfaces are made from :ref:`components ` which look and behave -similarly to those found in `ReactJS `__. Designed with simplicity -in mind, ReactPy can be used by those without web development experience while also -being powerful enough to grow with your ambitions. - - -At a Glance ------------ - -To get a rough idea of how to write apps in ReactPy, take a look at the tiny `"hello world" -`__ application below: - -.. reactpy:: guides/getting-started/_examples/hello_world - -.. hint:: - - Try clicking the **🚀 result** tab to see what this displays! - -So what exactly does this code do? First, it imports a few tools from ``reactpy`` that will -get used to describe and execute an application. Then, we create an ``App`` function -which will define the content the application displays. Specifically, it displays a kind -of HTML element called an ``h1`` `section heading -`__. -Importantly though, a ``@component`` decorator has been applied to the ``App`` function -to turn it into a :ref:`component `. Finally, we :ref:`run -` a development web server by passing the ``App`` component to the -``run()`` function. - -.. note:: - - See :ref:`Running ReactPy in Production` to learn how to use a production-grade server - to run ReactPy. - - -Learning ReactPy ----------------- - -This documentation is broken up into chapters and sections that introduce you to -concepts step by step with detailed explanations and lots of examples. You should feel -free to dive into any content that seems interesting. While each chapter assumes -knowledge from those that came before, when you encounter a concept you're unfamiliar -with you should look for links that will help direct you to the place where it was -originally taught. - - -Chapter 1 - :ref:`Getting Started` ------------------------------------ - -If you want to follow along with examples in the sections that follow, you'll want to -start here so you can :ref:`install ReactPy `. This section also contains -more detailed information about how to :ref:`run ReactPy ` in different -contexts. For example, if you want to embed ReactPy into an existing application, or run -ReactPy within a Jupyter Notebook, this is where you can learn how to do those things. - -.. grid:: 1 2 2 2 - - .. grid-item:: - - .. image:: _static/install-and-run-reactpy.gif - - .. grid-item:: - - .. image:: guides/getting-started/_static/reactpy-in-jupyterlab.gif - -.. card:: - :link: guides/getting-started/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Install ReactPy and run it in a variety of different ways - with different web servers - and frameworks. You'll even embed ReactPy into an existing app. - - -Chapter 2 - :ref:`Creating Interfaces` --------------------------------------- - -ReactPy is a Python package for making user interfaces (UI). These interfaces are built -from small elements of functionality like buttons text and images. ReactPy allows you to -combine these elements into reusable :ref:`"components" `. In the -sections that follow you'll learn how these UI elements are created and organized into -components. Then, you'll use this knowledge to create interfaces from raw data: - -.. reactpy:: guides/creating-interfaces/rendering-data/_examples/todo_list_with_keys - -.. card:: - :link: guides/creating-interfaces/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Learn to construct user interfaces from basic HTML elements and reusable components. - - -Chapter 3 - :ref:`Adding Interactivity` ---------------------------------------- - -Components often need to change what’s on the screen as a result of an interaction. For -example, typing into the form should update the input field, clicking a “Comment” button -should bring up a text input field, clicking “Buy” should put a product in the shopping -cart. Components need to “remember” things like the current input value, the current -image, the shopping cart. In ReactPy, this kind of component-specific memory is created and -updated with a "hook" called ``use_state()`` that creates a **state variable** and -**state setter** respectively: - -.. reactpy:: guides/adding-interactivity/components-with-state/_examples/adding_state_variable - -In ReactPy, ``use_state``, as well as any other function whose name starts with ``use``, is -called a "hook". These are special functions that should only be called while ReactPy is -:ref:`rendering `. They let you "hook into" the different -capabilities of ReactPy's components of which ``use_state`` is just one (well get into the -other :ref:`later `). - -.. card:: - :link: guides/adding-interactivity/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Learn how user interfaces can be made to respond to user interaction in real-time. - - -Chapter 4 - :ref:`Managing State` ---------------------------------- - -.. card:: - :link: guides/managing-state/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Under construction 🚧 - - - -Chapter 5 - :ref:`Escape Hatches` ---------------------------------- - -.. card:: - :link: guides/escape-hatches/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Under construction 🚧 - - -Chapter 6 - :ref:`Understanding ReactPy` ----------------------------------------- - -.. card:: - :link: guides/escape-hatches/index - :link-type: doc - - :octicon:`book` Read More - ^^^^^^^^^^^^^^^^^^^^^^^^^ - - Under construction 🚧 - diff --git a/docs/source/reference/_examples/character_movement/main.py b/docs/source/reference/_examples/character_movement/main.py deleted file mode 100644 index 9545b0c0a..000000000 --- a/docs/source/reference/_examples/character_movement/main.py +++ /dev/null @@ -1,73 +0,0 @@ -from pathlib import Path -from typing import NamedTuple - -from reactpy import component, html, run, use_state -from reactpy.widgets import image - -HERE = Path(__file__) -CHARACTER_IMAGE = (HERE.parent / "static" / "bunny.png").read_bytes() - - -class Position(NamedTuple): - x: int - y: int - angle: int - - -def rotate(degrees): - return lambda old_position: Position( - old_position.x, - old_position.y, - old_position.angle + degrees, - ) - - -def translate(x=0, y=0): - return lambda old_position: Position( - old_position.x + x, - old_position.y + y, - old_position.angle, - ) - - -@component -def Scene(): - position, set_position = use_state(Position(100, 100, 0)) - - return html.div( - {"style": {"width": "225px"}}, - html.div( - { - "style": { - "width": "200px", - "height": "200px", - "background_color": "slategray", - } - }, - image( - "png", - CHARACTER_IMAGE, - { - "style": { - "position": "relative", - "left": f"{position.x}px", - "top": f"{position.y}.px", - "transform": f"rotate({position.angle}deg) scale(2, 2)", - } - }, - ), - ), - html.button( - {"on_click": lambda e: set_position(translate(x=-10))}, "Move Left" - ), - html.button( - {"on_click": lambda e: set_position(translate(x=10))}, "Move Right" - ), - html.button({"on_click": lambda e: set_position(translate(y=-10))}, "Move Up"), - html.button({"on_click": lambda e: set_position(translate(y=10))}, "Move Down"), - html.button({"on_click": lambda e: set_position(rotate(-30))}, "Rotate Left"), - html.button({"on_click": lambda e: set_position(rotate(30))}, "Rotate Right"), - ) - - -run(Scene) diff --git a/docs/source/reference/_examples/character_movement/static/bunny.png b/docs/source/reference/_examples/character_movement/static/bunny.png deleted file mode 100644 index ce1f989c5..000000000 Binary files a/docs/source/reference/_examples/character_movement/static/bunny.png and /dev/null differ diff --git a/docs/source/reference/_examples/click_count.py b/docs/source/reference/_examples/click_count.py deleted file mode 100644 index 3ee2c89c5..000000000 --- a/docs/source/reference/_examples/click_count.py +++ /dev/null @@ -1,13 +0,0 @@ -import reactpy - - -@reactpy.component -def ClickCount(): - count, set_count = reactpy.hooks.use_state(0) - - return reactpy.html.button( - {"on_click": lambda event: set_count(count + 1)}, [f"Click count: {count}"] - ) - - -reactpy.run(ClickCount) diff --git a/docs/source/reference/_examples/material_ui_switch.py b/docs/source/reference/_examples/material_ui_switch.py deleted file mode 100644 index 704ae3145..000000000 --- a/docs/source/reference/_examples/material_ui_switch.py +++ /dev/null @@ -1,22 +0,0 @@ -import reactpy - -mui = reactpy.web.module_from_template("react", "@material-ui/core@^5.0", fallback="⌛") -Switch = reactpy.web.export(mui, "Switch") - - -@reactpy.component -def DayNightSwitch(): - checked, set_checked = reactpy.hooks.use_state(False) - - return reactpy.html.div( - Switch( - { - "checked": checked, - "onChange": lambda event, checked: set_checked(checked), - } - ), - "🌞" if checked else "🌚", - ) - - -reactpy.run(DayNightSwitch) diff --git a/docs/source/reference/_examples/matplotlib_plot.py b/docs/source/reference/_examples/matplotlib_plot.py deleted file mode 100644 index a70f173ff..000000000 --- a/docs/source/reference/_examples/matplotlib_plot.py +++ /dev/null @@ -1,85 +0,0 @@ -from io import BytesIO - -import matplotlib.pyplot as plt - -import reactpy -from reactpy.widgets import image - - -@reactpy.component -def PolynomialPlot(): - coefficients, set_coefficients = reactpy.hooks.use_state([0]) - - x = list(linspace(-1, 1, 50)) - y = [polynomial(value, coefficients) for value in x] - - return reactpy.html.div( - plot(f"{len(coefficients)} Term Polynomial", x, y), - ExpandableNumberInputs(coefficients, set_coefficients), - ) - - -@reactpy.component -def ExpandableNumberInputs(values, set_values): - inputs = [] - for i in range(len(values)): - - def set_value_at_index(event, index=i): - new_value = float(event["target"]["value"] or 0) - set_values([*values[:index], new_value, *values[index + 1 :]]) - - inputs.append(poly_coef_input(i + 1, set_value_at_index)) - - def add_input(): - set_values([*values, 0]) - - def del_input(): - set_values(values[:-1]) - - return reactpy.html.div( - reactpy.html.div( - "add/remove term:", - reactpy.html.button({"on_click": lambda event: add_input()}, "+"), - reactpy.html.button({"on_click": lambda event: del_input()}, "-"), - ), - inputs, - ) - - -def plot(title, x, y): - fig, axes = plt.subplots() - axes.plot(x, y) - axes.set_title(title) - buffer = BytesIO() - fig.savefig(buffer, format="png") - plt.close(fig) - return image("png", buffer.getvalue()) - - -def poly_coef_input(index, callback): - return reactpy.html.div( - {"style": {"margin-top": "5px"}, "key": index}, - reactpy.html.label( - "C", - reactpy.html.sub(index), - " x X", - reactpy.html.sup(index), - ), - reactpy.html.input({"type": "number", "on_change": callback}), - ) - - -def polynomial(x, coefficients): - return sum(c * (x ** (i + 1)) for i, c in enumerate(coefficients)) - - -def linspace(start, stop, n): - if n == 1: - yield stop - return - h = (stop - start) / (n - 1) - for i in range(n): - yield start + h * i - - -reactpy.run(PolynomialPlot) diff --git a/docs/source/reference/_examples/network_graph.py b/docs/source/reference/_examples/network_graph.py deleted file mode 100644 index 79b1092f3..000000000 --- a/docs/source/reference/_examples/network_graph.py +++ /dev/null @@ -1,40 +0,0 @@ -import random - -import reactpy - -react_cytoscapejs = reactpy.web.module_from_template( - "react", - "react-cytoscapejs", - fallback="⌛", -) -Cytoscape = reactpy.web.export(react_cytoscapejs, "default") - - -@reactpy.component -def RandomNetworkGraph(): - return Cytoscape( - { - "style": {"width": "100%", "height": "200px"}, - "elements": random_network(20), - "layout": {"name": "cose"}, - } - ) - - -def random_network(number_of_nodes): - conns = [] - nodes = [{"data": {"id": 0, "label": 0}}] - - for src_node_id in range(1, number_of_nodes + 1): - tgt_node = random.choice(nodes) - src_node = {"data": {"id": src_node_id, "label": src_node_id}} - - new_conn = {"data": {"source": src_node_id, "target": tgt_node["data"]["id"]}} - - nodes.append(src_node) - conns.append(new_conn) - - return nodes + conns - - -reactpy.run(RandomNetworkGraph) diff --git a/docs/source/reference/_examples/pigeon_maps.py b/docs/source/reference/_examples/pigeon_maps.py deleted file mode 100644 index 1ddf04fdc..000000000 --- a/docs/source/reference/_examples/pigeon_maps.py +++ /dev/null @@ -1,46 +0,0 @@ -import reactpy - -pigeon_maps = reactpy.web.module_from_template("react", "pigeon-maps", fallback="⌛") -Map, Marker = reactpy.web.export(pigeon_maps, ["Map", "Marker"]) - - -@reactpy.component -def MapWithMarkers(): - marker_anchor, add_marker_anchor, remove_marker_anchor = use_set() - - markers = [ - Marker( - { - "anchor": anchor, - "onClick": lambda event, a=anchor: remove_marker_anchor(a), - }, - key=str(anchor), - ) - for anchor in marker_anchor - ] - - return Map( - { - "defaultCenter": (37.774, -122.419), - "defaultZoom": 12, - "height": "300px", - "metaWheelZoom": True, - "onClick": lambda event: add_marker_anchor(tuple(event["latLng"])), - }, - markers, - ) - - -def use_set(initial_value=None): - values, set_values = reactpy.hooks.use_state(initial_value or set()) - - def add_value(lat_lon): - set_values(values.union({lat_lon})) - - def remove_value(lat_lon): - set_values(values.difference({lat_lon})) - - return values, add_value, remove_value - - -reactpy.run(MapWithMarkers) diff --git a/docs/source/reference/_examples/simple_dashboard.py b/docs/source/reference/_examples/simple_dashboard.py deleted file mode 100644 index 0d39ab9af..000000000 --- a/docs/source/reference/_examples/simple_dashboard.py +++ /dev/null @@ -1,102 +0,0 @@ -import asyncio -import random -import time - -import reactpy -from reactpy.widgets import Input - -victory = reactpy.web.module_from_template( - "react", - "victory-line", - fallback="⌛", - # not usually required (see issue #461 for more info) - unmount_before_update=True, -) -VictoryLine = reactpy.web.export(victory, "VictoryLine") - - -@reactpy.component -def RandomWalk(): - mu = reactpy.hooks.use_ref(0) - sigma = reactpy.hooks.use_ref(1) - - return reactpy.html.div( - RandomWalkGraph(mu, sigma), - reactpy.html.style( - """ - .number-input-container {margin-bottom: 20px} - .number-input-container input {width: 48%;float: left} - .number-input-container input + input {margin-left: 4%} - """ - ), - NumberInput( - "Mean", - mu.current, - mu.set_current, - (-1, 1, 0.01), - ), - NumberInput( - "Standard Deviation", - sigma.current, - sigma.set_current, - (0, 1, 0.01), - ), - ) - - -@reactpy.component -def RandomWalkGraph(mu, sigma): - interval = use_interval(0.5) - data, set_data = reactpy.hooks.use_state([{"x": 0, "y": 0}] * 50) - - @reactpy.hooks.use_async_effect - async def animate(): - await interval - last_data_point = data[-1] - next_data_point = { - "x": last_data_point["x"] + 1, - "y": last_data_point["y"] + random.gauss(mu.current, sigma.current), - } - set_data([*data[1:], next_data_point]) - - return VictoryLine( - { - "data": data, - "style": { - "parent": {"width": "100%"}, - "data": {"stroke": "royalblue"}, - }, - } - ) - - -@reactpy.component -def NumberInput(label, value, set_value_callback, domain): - minimum, maximum, step = domain - attrs = {"min": minimum, "max": maximum, "step": step} - - value, set_value = reactpy.hooks.use_state(value) - - def update_value(value): - set_value(value) - set_value_callback(value) - - return reactpy.html.fieldset( - {"class_name": "number-input-container"}, - reactpy.html.legend({"style": {"font-size": "medium"}}, label), - Input(update_value, "number", value, attributes=attrs, cast=float), - Input(update_value, "range", value, attributes=attrs, cast=float), - ) - - -def use_interval(rate): - usage_time = reactpy.hooks.use_ref(time.time()) - - async def interval() -> None: - await asyncio.sleep(rate - (time.time() - usage_time.current)) - usage_time.current = time.time() - - return asyncio.ensure_future(interval()) - - -reactpy.run(RandomWalk) diff --git a/docs/source/reference/_examples/slideshow.py b/docs/source/reference/_examples/slideshow.py deleted file mode 100644 index b490b3feb..000000000 --- a/docs/source/reference/_examples/slideshow.py +++ /dev/null @@ -1,20 +0,0 @@ -import reactpy - - -@reactpy.component -def Slideshow(): - index, set_index = reactpy.hooks.use_state(0) - - def next_image(event): - set_index(index + 1) - - return reactpy.html.img( - { - "src": f"https://picsum.photos/id/{index}/800/300", - "style": {"cursor": "pointer"}, - "on_click": next_image, - } - ) - - -reactpy.run(Slideshow) diff --git a/docs/source/reference/_examples/snake_game.py b/docs/source/reference/_examples/snake_game.py deleted file mode 100644 index 5b5afc778..000000000 --- a/docs/source/reference/_examples/snake_game.py +++ /dev/null @@ -1,188 +0,0 @@ -import asyncio -import enum -import random -import time - -import reactpy - - -class GameState(enum.Enum): - init = 0 - lost = 1 - won = 2 - play = 3 - - -@reactpy.component -def GameView(): - game_state, set_game_state = reactpy.hooks.use_state(GameState.init) - - if game_state == GameState.play: - return GameLoop(grid_size=6, block_scale=50, set_game_state=set_game_state) - - start_button = reactpy.html.button( - {"on_click": lambda event: set_game_state(GameState.play)}, "Start" - ) - - if game_state == GameState.won: - menu = reactpy.html.div(reactpy.html.h3("You won!"), start_button) - elif game_state == GameState.lost: - menu = reactpy.html.div(reactpy.html.h3("You lost"), start_button) - else: - menu = reactpy.html.div(reactpy.html.h3("Click to play"), start_button) - - menu_style = reactpy.html.style( - """ - .snake-game-menu h3 { - margin-top: 0px !important; - } - """ - ) - - return reactpy.html.div({"class_name": "snake-game-menu"}, menu_style, menu) - - -class Direction(enum.Enum): - ArrowUp = (0, -1) - ArrowLeft = (-1, 0) - ArrowDown = (0, 1) - ArrowRight = (1, 0) - - -@reactpy.component -def GameLoop(grid_size, block_scale, set_game_state): - # we `use_ref` here to capture the latest direction press without any delay - direction = reactpy.hooks.use_ref(Direction.ArrowRight.value) - # capture the last direction of travel that was rendered - last_direction = direction.current - - snake, set_snake = reactpy.hooks.use_state( - [(grid_size // 2 - 1, grid_size // 2 - 1)] - ) - food, set_food = use_snake_food(grid_size, snake) - - grid = create_grid(grid_size, block_scale) - - @reactpy.event(prevent_default=True) - def on_direction_change(event): - if hasattr(Direction, event["key"]): - maybe_new_direction = Direction[event["key"]].value - direction_vector_sum = tuple( - map(sum, zip(last_direction, maybe_new_direction, strict=False)) - ) - if direction_vector_sum != (0, 0): - direction.current = maybe_new_direction - - grid_wrapper = reactpy.html.div({"on_key_down": on_direction_change}, grid) - - assign_grid_block_color(grid, food, "blue") - - for location in snake: - assign_grid_block_color(grid, location, "white") - - new_game_state = None - if snake[-1] in snake[:-1]: - assign_grid_block_color(grid, snake[-1], "red") - new_game_state = GameState.lost - elif len(snake) == grid_size**2: - assign_grid_block_color(grid, snake[-1], "yellow") - new_game_state = GameState.won - - interval = use_interval(0.5) - - @reactpy.hooks.use_async_effect - async def animate(): - if new_game_state is not None: - await asyncio.sleep(1) - set_game_state(new_game_state) - return - - await interval - - new_snake_head = ( - # grid wraps due to mod op here - (snake[-1][0] + direction.current[0]) % grid_size, - (snake[-1][1] + direction.current[1]) % grid_size, - ) - - if snake[-1] == food: - set_food() - new_snake = [*snake, new_snake_head] - else: - new_snake = [*snake[1:], new_snake_head] - - set_snake(new_snake) - - return grid_wrapper - - -def use_snake_food(grid_size, current_snake): - grid_points = {(x, y) for x in range(grid_size) for y in range(grid_size)} - points_not_in_snake = grid_points.difference(current_snake) - - food, _set_food = reactpy.hooks.use_state(current_snake[-1]) - - def set_food(): - _set_food(random.choice(list(points_not_in_snake))) - - return food, set_food - - -def use_interval(rate): - usage_time = reactpy.hooks.use_ref(time.time()) - - async def interval() -> None: - await asyncio.sleep(rate - (time.time() - usage_time.current)) - usage_time.current = time.time() - - return asyncio.ensure_future(interval()) - - -def create_grid(grid_size, block_scale): - return reactpy.html.div( - { - "style": { - "height": f"{block_scale * grid_size}px", - "width": f"{block_scale * grid_size}px", - "cursor": "pointer", - "display": "grid", - "grid-gap": 0, - "grid-template-columns": f"repeat({grid_size}, {block_scale}px)", - "grid-template-rows": f"repeat({grid_size}, {block_scale}px)", - }, - "tab_index": -1, - }, - [ - reactpy.html.div( - {"style": {"height": f"{block_scale}px"}, "key": i}, - [ - create_grid_block("black", block_scale, key=i) - for i in range(grid_size) - ], - ) - for i in range(grid_size) - ], - ) - - -def create_grid_block(color, block_scale, key): - return reactpy.html.div( - { - "style": { - "height": f"{block_scale}px", - "width": f"{block_scale}px", - "background_color": color, - "outline": "1px solid grey", - }, - "key": key, - } - ) - - -def assign_grid_block_color(grid, point, color): - x, y = point - block = grid["children"][x]["children"][y] - block["attributes"]["style"]["backgroundColor"] = color - - -reactpy.run(GameView) diff --git a/docs/source/reference/_examples/todo.py b/docs/source/reference/_examples/todo.py deleted file mode 100644 index 104ea59a9..000000000 --- a/docs/source/reference/_examples/todo.py +++ /dev/null @@ -1,35 +0,0 @@ -import reactpy - - -@reactpy.component -def Todo(): - items, set_items = reactpy.hooks.use_state([]) - - async def add_new_task(event): - if event["key"] == "Enter": - set_items([*items, event["target"]["value"]]) - - tasks = [] - - for index, text in enumerate(items): - - async def remove_task(event, index=index): - set_items(items[:index] + items[index + 1 :]) - - task_text = reactpy.html.td(reactpy.html.p(text)) - delete_button = reactpy.html.td( - {"on_click": remove_task}, reactpy.html.button(["x"]) - ) - tasks.append(reactpy.html.tr(task_text, delete_button)) - - task_input = reactpy.html.input({"on_key_down": add_new_task}) - task_table = reactpy.html.table(tasks) - - return reactpy.html.div( - reactpy.html.p("press enter to add a task:"), - task_input, - task_table, - ) - - -reactpy.run(Todo) diff --git a/docs/source/reference/_examples/use_reducer_counter.py b/docs/source/reference/_examples/use_reducer_counter.py deleted file mode 100644 index 6f9581dfd..000000000 --- a/docs/source/reference/_examples/use_reducer_counter.py +++ /dev/null @@ -1,27 +0,0 @@ -import reactpy - - -def reducer(count, action): - if action == "increment": - return count + 1 - elif action == "decrement": - return count - 1 - elif action == "reset": - return 0 - else: - msg = f"Unknown action '{action}'" - raise ValueError(msg) - - -@reactpy.component -def Counter(): - count, dispatch = reactpy.hooks.use_reducer(reducer, 0) - return reactpy.html.div( - f"Count: {count}", - reactpy.html.button({"on_click": lambda event: dispatch("reset")}, "Reset"), - reactpy.html.button({"on_click": lambda event: dispatch("increment")}, "+"), - reactpy.html.button({"on_click": lambda event: dispatch("decrement")}, "-"), - ) - - -reactpy.run(Counter) diff --git a/docs/source/reference/_examples/use_state_counter.py b/docs/source/reference/_examples/use_state_counter.py deleted file mode 100644 index b2d8c84a9..000000000 --- a/docs/source/reference/_examples/use_state_counter.py +++ /dev/null @@ -1,26 +0,0 @@ -import reactpy - - -def increment(last_count): - return last_count + 1 - - -def decrement(last_count): - return last_count - 1 - - -@reactpy.component -def Counter(): - initial_count = 0 - count, set_count = reactpy.hooks.use_state(initial_count) - return reactpy.html.div( - f"Count: {count}", - reactpy.html.button( - {"on_click": lambda event: set_count(initial_count)}, "Reset" - ), - reactpy.html.button({"on_click": lambda event: set_count(increment)}, "+"), - reactpy.html.button({"on_click": lambda event: set_count(decrement)}, "-"), - ) - - -reactpy.run(Counter) diff --git a/docs/source/reference/_examples/victory_chart.py b/docs/source/reference/_examples/victory_chart.py deleted file mode 100644 index ce37c522f..000000000 --- a/docs/source/reference/_examples/victory_chart.py +++ /dev/null @@ -1,7 +0,0 @@ -import reactpy - -victory = reactpy.web.module_from_template("react", "victory-bar", fallback="⌛") -VictoryBar = reactpy.web.export(victory, "VictoryBar") - -bar_style = {"parent": {"width": "500px"}, "data": {"fill": "royalblue"}} -reactpy.run(reactpy.component(lambda: VictoryBar({"style": bar_style}))) diff --git a/docs/source/reference/_static/vdom-json-schema.json b/docs/source/reference/_static/vdom-json-schema.json deleted file mode 100644 index b1005d2ed..000000000 --- a/docs/source/reference/_static/vdom-json-schema.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "$ref": "#/definitions/element", - "$schema": "http://json-schema.org/draft-07/schema", - "definitions": { - "element": { - "dependentSchemas": { - "error": { - "properties": { - "tagName": { - "maxLength": 0 - } - } - } - }, - "properties": { - "attributes": { - "type": "object" - }, - "children": { - "$ref": "#/definitions/elementChildren" - }, - "error": { - "type": "string" - }, - "eventHandlers": { - "$ref": "#/definitions/elementEventHandlers" - }, - "importSource": { - "$ref": "#/definitions/importSource" - }, - "key": { - "type": "string" - }, - "tagName": { - "type": "string" - } - }, - "required": ["tagName"], - "type": "object" - }, - "elementChildren": { - "items": { - "$ref": "#/definitions/elementOrString" - }, - "type": "array" - }, - "elementEventHandlers": { - "patternProperties": { - ".*": { - "$ref": "#/definitions/eventHander" - } - }, - "type": "object" - }, - "elementOrString": { - "if": { - "type": "object" - }, - "then": { - "$ref": "#/definitions/element" - }, - "type": ["object", "string"] - }, - "eventHandler": { - "properties": { - "preventDefault": { - "type": "boolean" - }, - "stopPropagation": { - "type": "boolean" - }, - "target": { - "type": "string" - } - }, - "required": ["target"], - "type": "object" - }, - "importSource": { - "properties": { - "fallback": { - "if": { - "not": { - "type": "null" - } - }, - "then": { - "$ref": "#/definitions/elementOrString" - }, - "type": ["object", "string", "null"] - }, - "source": { - "type": "string" - }, - "sourceType": { - "enum": ["URL", "NAME"] - }, - "unmountBeforeUpdate": { - "type": "boolean" - } - }, - "required": ["source"], - "type": "object" - } - } -} diff --git a/docs/source/reference/browser-events.rst b/docs/source/reference/browser-events.rst deleted file mode 100644 index 632be410a..000000000 --- a/docs/source/reference/browser-events.rst +++ /dev/null @@ -1,65 +0,0 @@ -.. _Browser Events: - -Browser Events 🚧 -================= - -The event types below are triggered by an event in the bubbling phase. To register an -event handler for the capture phase, append Capture to the event name; for example, -instead of using ``onClick``, you would use ``onClickCapture`` to handle the click event -in the capture phase. - -.. note:: - - Under construction 🚧 - - -Clipboard Events ----------------- - -Composition Events ------------------- - -Keyboard Events ---------------- - -Focus Events ------------- - -Form Events ------------ - -Generic Events --------------- - -Mouse Events ------------- - -Pointer Events --------------- - -Selection Events ----------------- - -Touch Events ------------- - -UI Events ---------- - -Wheel Events ------------- - -Media Events ------------- - -Image Events ------------- - -Animation Events ----------------- - -Transition Events ------------------ - -Other Events ------------- diff --git a/docs/source/reference/hooks-api.rst b/docs/source/reference/hooks-api.rst deleted file mode 100644 index ca8123e85..000000000 --- a/docs/source/reference/hooks-api.rst +++ /dev/null @@ -1,379 +0,0 @@ -========= -Hooks API -========= - -Hooks are functions that allow you to "hook into" the life cycle events and state of -Components. Their usage should always follow the :ref:`Rules of Hooks`. For most use -cases the :ref:`Basic Hooks` should be enough, however the remaining -:ref:`Supplementary Hooks` should fulfill less common scenarios. - - -Basic Hooks -=========== - - -Use State ---------- - -.. code-block:: - - state, set_state = use_state(initial_state) - -Returns a stateful value and a function to update it. - -During the first render the ``state`` will be identical to the ``initial_state`` passed -as the first argument. However in subsequent renders ``state`` will take on the value -passed to ``set_state``. - -.. code-block:: - - set_state(new_state) - -The ``set_state`` function accepts a ``new_state`` as its only argument and schedules a -re-render of the component where ``use_state`` was initially called. During these -subsequent re-renders the ``state`` returned by ``use_state`` will take on the value -of ``new_state``. - -.. note:: - - The identity of ``set_state`` is guaranteed to be preserved across renders. This - means it can safely be omitted from dependency lists in :ref:`Use Effect` or - :ref:`Use Callback`. - - -Functional Updates -.................. - -If the new state is computed from the previous state, you can pass a function which -accepts a single argument (the previous state) and returns the next state. Consider this -simply use case of a counter where we've pulled out logic for increment and -decremented the count: - -.. reactpy:: _examples/use_state_counter - -We use the functional form for the "+" and "-" buttons since the next ``count`` depends -on the previous value, while for the "Reset" button we simple assign the -``initial_count`` since it is independent of the prior ``count``. This is a trivial -example, but it demonstrates how complex state logic can be factored out into well -defined and potentially reusable functions. - - -Lazy Initial State -.................. - -In cases where it is costly to create the initial value for ``use_state``, you can pass -a constructor function that accepts no arguments instead - it will be called on the -first render of a component, but will be disregarded in all following renders: - -.. code-block:: - - state, set_state = use_state(lambda: some_expensive_computation(a, b, c)) - - -Skipping Updates -................ - -If you update a State Hook to the same value as the current state then the component which -owns that state will not be rendered again. We check ``if new_state is current_state`` -in order to determine whether there has been a change. Thus the following would not -result in a re-render: - -.. code-block:: - - state, set_state = use_state([]) - set_state(state) - - -Use Effect ----------- - -.. code-block:: - - use_effect(did_render) - -The ``use_effect`` hook accepts a function which may be imperative, or mutate state. The -function will be called immediately after the layout has fully updated. - -Asynchronous actions, mutations, subscriptions, and other `side effects`_ can cause -unexpected bugs if placed in the main body of a component's render function. Thus the -``use_effect`` hook provides a way to safely escape the purely functional world of -component render functions. - -.. note:: - - Normally in React the ``did_render`` function is called once an update has been - committed to the screen. Since no such action is performed by ReactPy, and the time - at which the update is displayed cannot be known we are unable to achieve parity - with this behavior. - - -Cleaning Up Effects -................... - -If the effect you wish to enact creates resources, you'll probably need to clean them -up. In such cases you may simply return a function that addresses this from the -``did_render`` function which created the resource. Consider the case of opening and -then closing a connection: - -.. code-block:: - - def establish_connection(): - connection = open_connection() - return lambda: close_connection(connection) - - use_effect(establish_connection) - -The clean-up function will be run before the component is unmounted or, before the next -effect is triggered when the component re-renders. You can -:ref:`conditionally fire events ` to avoid triggering them each -time a component renders. - - -Conditional Effects -................... - -By default, effects are triggered after every successful render to ensure that all state -referenced by the effect is up to date. However, when an effect function references -non-global variables, the effect will only if the value of that variable changes. For -example, imagine that we had an effect that connected to a ``url`` state variable: - -.. code-block:: - - url, set_url = use_state("https://example.com") - - def establish_connection(): - connection = open_connection(url) - return lambda: close_connection(connection) - - use_effect(establish_connection) - -Here, a new connection will be established whenever a new ``url`` is set. - - -Async Effects -............. - -A behavior unique to ReactPy's implementation of ``use_effect`` is that it natively -supports ``async`` functions: - -.. code-block:: - - async def non_blocking_effect(): - resource = await do_something_asynchronously() - return lambda: blocking_close(resource) - - use_effect(non_blocking_effect) - - -There are **three important subtleties** to note about using asynchronous effects: - -1. The cleanup function must be a normal synchronous function. - -2. Asynchronous effects which do not complete before the next effect is created - following a re-render will be cancelled. This means an - :class:`~asyncio.CancelledError` will be raised somewhere in the body of the effect. - -3. An asynchronous effect may occur any time after the update which added this effect - and before the next effect following a subsequent update. - - -Manual Effect Conditions -........................ - -In some cases, you may want to explicitly declare when an effect should be triggered. -You can do this by passing ``dependencies`` to ``use_effect``. Each of the following -values produce different effect behaviors: - -- ``use_effect(..., dependencies=None)`` - triggers and cleans up on every render. -- ``use_effect(..., dependencies=[])`` - only triggers on the first and cleans up after - the last render. -- ``use_effect(..., dependencies=[x, y])`` - triggers on the first render and on subsequent renders if - ``x`` or ``y`` have changed. - - -Use Context ------------ - -.. code-block:: - - value = use_context(MyContext) - -Accepts a context object (the value returned from -:func:`reactpy.core.hooks.create_context`) and returns the current context value for that -context. The current context value is determined by the ``value`` argument passed to the -nearest ``MyContext`` in the tree. - -When the nearest above the component updates, this Hook will -trigger a rerender with the latest context value passed to that MyContext provider. Even -if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen -starting at the component itself using useContext. - - -Supplementary Hooks -=================== - - -Use Reducer ------------ - -.. code-block:: - - state, dispatch_action = use_reducer(reducer, initial_state) - -An alternative and derivative of :ref:`Use State` the ``use_reducer`` hook, instead of -directly assigning a new state, allows you to specify an action which will transition -the previous state into the next state. This transition is defined by a reducer function -of the form ``(current_state, action) -> new_state``. The ``use_reducer`` hook then -returns the current state and a ``dispatch_action`` function that accepts an ``action`` -and causes a transition to the next state via the ``reducer``. - -``use_reducer`` is generally preferred to ``use_state`` if logic for transitioning from -one state to the next is especially complex or involves nested data structures. -``use_reducer`` can also be used to collect several ``use_state`` calls together - this -may be slightly more performant as well as being preferable since there is only one -``dispatch_action`` callback versus the many ``set_state`` callbacks. - -We can rework the :ref:`Functional Updates` counter example to use ``use_reducer``: - -.. reactpy:: _examples/use_reducer_counter - -.. note:: - - The identity of the ``dispatch_action`` function is guaranteed to be preserved - across re-renders throughout the lifetime of the component. This means it can safely - be omitted from dependency lists in :ref:`Use Effect` or :ref:`Use Callback`. - - -Use Callback ------------- - -.. code-block:: - - memoized_callback = use_callback(lambda: do_something(a, b)) - -A derivative of :ref:`Use Memo`, the ``use_callback`` hook returns a -`memoized `_ callback. This is useful when passing callbacks to child -components which check reference equality to prevent unnecessary renders. The -``memoized_callback`` will only change when any local variables is references do. - -.. note:: - - You may manually specify what values the callback depends on in the :ref:`same way - as effects ` using the ``dependencies`` parameter. - - -Use Memo --------- - -.. code-block:: - - memoized_value = use_memo(lambda: compute_something_expensive(a, b)) - -Returns a `memoized `_ value. By passing a constructor function accepting -no arguments and an array of dependencies for that constructor, the ``use_callback`` -hook will return the value computed by the constructor. The ``memoized_value`` will only -be recomputed if a local variable referenced by the constructor changes (e.g. ``a`` or -``b`` here). This optimizes performance because you don't need to -``compute_something_expensive`` on every render. - -Unlike ``use_effect`` the constructor function is called during each render (instead of -after) and should not incur side effects. - -.. warning:: - - Remember that you shouldn't optimize something unless you know it's a performance - bottleneck. Write your code without ``use_memo`` first and then add it to targeted - sections that need a speed-up. - -.. note:: - - You may manually specify what values the callback depends on in the :ref:`same way - as effects ` using the ``dependencies`` parameter. - - -Use Ref -------- - -.. code-block:: - - ref_container = use_ref(initial_value) - -Returns a mutable :class:`~reactpy.utils.Ref` object that has a single -:attr:`~reactpy.utils.Ref.current` attribute that at first contains the ``initial_state``. -The identity of the ``Ref`` object will be preserved for the lifetime of the component. - -A ``Ref`` is most useful if you need to incur side effects since updating its -``.current`` attribute doesn't trigger a re-render of the component. You'll often use this -hook alongside :ref:`Use Effect` or in response to component event handlers. - - -.. links -.. ===== - -.. _React Hooks: https://reactjs.org/docs/hooks-reference.html -.. _side effects: https://en.wikipedia.org/wiki/Side_effect_(computer_science) -.. _memoization: https://en.wikipedia.org/wiki/Memoization - - -Rules of Hooks -============== - -Hooks are just normal Python functions, but there's a bit of magic to them, and in order -for that magic to work you've got to follow two rules. Thankfully we supply a -:ref:`Flake8 Plugin` to help enforce them. - - -Only call outside flow controls -------------------------------- - -**Don't call hooks inside loops, conditions, or nested functions.** Instead you must -always call hooks at the top level of your functions. By adhering to this rule you -ensure that hooks are always called in the exact same order. This fact is what allows -ReactPy to preserve the state of hooks between multiple calls to ``useState`` and -``useEffect`` calls. - - -Only call in render functions ------------------------------ - -**Don't call hooks from regular Python functions.** Instead you should: - -- ✅ Call Hooks from a component's render function. - -- ✅ Call Hooks from another custom hook - -Following this rule ensures stateful logic for ReactPy component is always clearly -separated from the rest of your codebase. - - -Flake8 Plugin -------------- - -We provide a Flake8 plugin called `flake8-reactpy-hooks `_ that helps -to enforce the two rules described above. You can ``pip`` install it directly, or with -the ``lint`` extra for ReactPy: - -.. code-block:: bash - - pip install flake8-reactpy-hooks - -Once installed running, ``flake8`` on your code will start catching errors. For example: - -.. code-block:: bash - - flake8 my_reactpy_components.py - -Might produce something like the following output: - -.. code-block:: text - - ./my_reactpy_components:10:8 ROH102 hook 'use_effect' used inside if statement - ./my_reactpy_components:23:4 ROH102 hook 'use_state' used outside component or hook definition - -See the Flake8 docs for -`more info `__. - -.. links -.. ===== - -.. _Flake8 Linter Plugin: https://github.com/reactive-python/flake8-reactpy-hooks diff --git a/docs/source/reference/html-attributes.rst b/docs/source/reference/html-attributes.rst deleted file mode 100644 index 91813c355..000000000 --- a/docs/source/reference/html-attributes.rst +++ /dev/null @@ -1,197 +0,0 @@ -.. testcode:: - - from reactpy import html - - -HTML Attributes -=============== - -In ReactPy, HTML attributes are specified using snake_case instead of dash-separated -words. For example, ``tabindex`` and ``margin-left`` become ``tab_index`` and -``margin_left`` respectively. - - -Notable Attributes -------------------- - -Some attributes in ReactPy are renamed, have special meaning, or are used differently -than in HTML. - -``style`` -......... - -As mentioned above, instead of using a string to specify the ``style`` attribute, we use -a dictionary to describe the CSS properties we want to apply to an element. For example, -the following HTML: - -.. code-block:: html - -
-

My Todo List

-
    -
  • Build a cool new app
  • -
  • Share it with the world!
  • -
-
- -Would be written in ReactPy as: - -.. testcode:: - - html.div( - { - "style": { - "width": "50%", - "margin_left": "25%", - }, - }, - html.h1( - { - "style": { - "margin_top": "0px", - }, - }, - "My Todo List", - ), - html.ul( - html.li("Build a cool new app"), - html.li("Share it with the world!"), - ), - ) - -``class`` vs ``class_name`` -........................... - -In HTML, the ``class`` attribute is used to specify a CSS class for an element. In -ReactPy, this attribute is renamed to ``class_name`` to avoid conflicting with the -``class`` keyword in Python. For example, the following HTML: - -.. code-block:: html - -
-

My Todo List

-
    -
  • Build a cool new app
  • -
  • Share it with the world!
  • -
-
- -Would be written in ReactPy as: - -.. testcode:: - - html.div( - {"class_name": "container"}, - html.h1({"class_name": "title"}, "My Todo List"), - html.ul( - {"class_name": "list"}, - html.li({"class_name": "item"}, "Build a cool new app"), - html.li({"class_name": "item"}, "Share it with the world!"), - ), - ) - -``for`` vs ``html_for`` -....................... - -In HTML, the ``for`` attribute is used to specify the ``id`` of the element it's -associated with. In ReactPy, this attribute is renamed to ``html_for`` to avoid -conflicting with the ``for`` keyword in Python. For example, the following HTML: - -.. code-block:: html - -
- - -
- -Would be written in ReactPy as: - -.. testcode:: - - html.div( - html.label({"html_for": "todo"}, "Todo:"), - html.input({"id": "todo", "type": "text"}), - ) - -``dangerously_set_inner_HTML`` -.............................. - -This is used to set the ``innerHTML`` property of an element and should be provided a -dictionary with a single key ``__html`` whose value is the HTML to be set. It should be -used with **extreme caution** as it can lead to XSS attacks if the HTML inside isn't -trusted (for example if it comes from user input). - - -All Attributes --------------- - -`access_key `__ - A string. Specifies a keyboard shortcut for the element. Not generally recommended. - -`aria_* `__ - ARIA attributes let you specify the accessibility tree information for this element. - See ARIA attributes for a complete reference. In ReactPy, all ARIA attribute names are - exactly the same as in HTML. - -`auto_capitalize `__ - A string. Specifies whether and how the user input should be capitalized. - -`content_editable `__ - A boolean. If true, the browser lets the user edit the rendered element directly. This - is used to implement rich text input libraries like Lexical. ReactPy warns if you try - to pass children to an element with ``content_editable = True`` because ReactPy will - not be able to update its content after user edits. - -`data_* `__ - Data attributes let you attach some string data to the element, for example - data-fruit="banana". In ReactPy, they are not commonly used because you would usually - read data from props or state instead. - -`dir `__ - Either ``"ltr"`` or ``"rtl"``. Specifies the text direction of the element. - -`draggable `__ - A boolean. Specifies whether the element is draggable. Part of HTML Drag and Drop API. - -`enter_key_hint `__ - A string. Specifies which action to present for the enter key on virtual keyboards. - -`hidden