Repository: reactive-python/reactpy Branch: main Commit: 613b256abc1b Files: 339 Total size: 987.3 KB Directory structure: gitextract_qt3yaq80/ ├── .editorconfig ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ └── issue-form.yml │ ├── copilot-instructions.md │ ├── pull_request_template.md │ └── workflows/ │ ├── .hatch-run.yml │ ├── check.yml │ ├── codeql-analysis.yml │ └── publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── docs/ │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── docs_app/ │ │ ├── __init__.py │ │ ├── app.py │ │ ├── dev.py │ │ ├── examples.py │ │ └── prod.py │ ├── main.py │ ├── pyproject.toml │ └── source/ │ ├── _custom_js/ │ │ ├── README.md │ │ ├── bun.lockb │ │ ├── package.json │ │ ├── rollup.config.js │ │ └── src/ │ │ └── index.js │ ├── _exts/ │ │ ├── async_doctest.py │ │ ├── autogen_api_docs.py │ │ ├── build_custom_js.py │ │ ├── copy_vdom_json_schema.py │ │ ├── custom_autosectionlabel.py │ │ ├── patched_html_translator.py │ │ ├── reactpy_example.py │ │ └── reactpy_view.py │ ├── _static/ │ │ └── css/ │ │ ├── furo-theme-overrides.css │ │ ├── larger-api-margins.css │ │ ├── larger-headings.css │ │ ├── reactpy-view.css │ │ ├── sphinx-design-overrides.css │ │ └── widget-output-css-overrides.css │ ├── about/ │ │ ├── changelog.rst │ │ ├── contributor-guide.rst │ │ └── credits-and-licenses.rst │ ├── conf.py │ ├── guides/ │ │ ├── adding-interactivity/ │ │ │ ├── components-with-state/ │ │ │ │ ├── _examples/ │ │ │ │ │ ├── adding_state_variable/ │ │ │ │ │ │ ├── data.json │ │ │ │ │ │ └── main.py │ │ │ │ │ ├── isolated_state/ │ │ │ │ │ │ ├── data.json │ │ │ │ │ │ └── main.py │ │ │ │ │ ├── multiple_state_variables/ │ │ │ │ │ │ ├── data.json │ │ │ │ │ │ └── main.py │ │ │ │ │ └── when_variables_are_not_enough/ │ │ │ │ │ ├── data.json │ │ │ │ │ └── main.py │ │ │ │ └── index.rst │ │ │ ├── dangers-of-mutability/ │ │ │ │ ├── _examples/ │ │ │ │ │ ├── dict_remove.py │ │ │ │ │ ├── dict_update.py │ │ │ │ │ ├── list_insert.py │ │ │ │ │ ├── list_re_order.py │ │ │ │ │ ├── list_remove.py │ │ │ │ │ ├── list_replace.py │ │ │ │ │ ├── moving_dot.py │ │ │ │ │ ├── moving_dot_broken.py │ │ │ │ │ ├── set_remove.py │ │ │ │ │ └── set_update.py │ │ │ │ └── index.rst │ │ │ ├── index.rst │ │ │ ├── multiple-state-updates/ │ │ │ │ ├── _examples/ │ │ │ │ │ ├── delay_before_count_updater.py │ │ │ │ │ ├── delay_before_set_count.py │ │ │ │ │ ├── set_color_3_times.py │ │ │ │ │ └── set_state_function.py │ │ │ │ └── index.rst │ │ │ ├── responding-to-events/ │ │ │ │ ├── _examples/ │ │ │ │ │ ├── audio_player.py │ │ │ │ │ ├── button_async_handlers.py │ │ │ │ │ ├── button_does_nothing.py │ │ │ │ │ ├── button_handler_as_arg.py │ │ │ │ │ ├── button_prints_event.py │ │ │ │ │ ├── button_prints_message.py │ │ │ │ │ ├── prevent_default_event_actions.py │ │ │ │ │ └── stop_event_propagation.py │ │ │ │ └── index.rst │ │ │ └── state-as-a-snapshot/ │ │ │ ├── _examples/ │ │ │ │ ├── delayed_print_after_set.py │ │ │ │ ├── print_chat_message.py │ │ │ │ ├── print_count_after_set.py │ │ │ │ ├── send_message.py │ │ │ │ └── set_counter_3_times.py │ │ │ └── index.rst │ │ ├── creating-interfaces/ │ │ │ ├── html-with-reactpy/ │ │ │ │ └── index.rst │ │ │ ├── index.rst │ │ │ ├── rendering-data/ │ │ │ │ ├── _examples/ │ │ │ │ │ ├── sorted_and_filtered_todo_list.py │ │ │ │ │ ├── todo_from_list.py │ │ │ │ │ └── todo_list_with_keys.py │ │ │ │ └── index.rst │ │ │ └── your-first-components/ │ │ │ ├── _examples/ │ │ │ │ ├── bad_conditional_todo_list.py │ │ │ │ ├── good_conditional_todo_list.py │ │ │ │ ├── nested_photos.py │ │ │ │ ├── parametrized_photos.py │ │ │ │ ├── simple_photo.py │ │ │ │ ├── todo_list.py │ │ │ │ ├── wrap_in_div.py │ │ │ │ └── wrap_in_fragment.py │ │ │ └── index.rst │ │ ├── escape-hatches/ │ │ │ ├── _examples/ │ │ │ │ ├── material_ui_button_no_action.py │ │ │ │ ├── material_ui_button_on_click.py │ │ │ │ └── super_simple_chart/ │ │ │ │ ├── main.py │ │ │ │ └── super-simple-chart.js │ │ │ ├── distributing-javascript.rst │ │ │ ├── index.rst │ │ │ ├── javascript-components.rst │ │ │ ├── using-a-custom-backend.rst │ │ │ └── using-a-custom-client.rst │ │ ├── getting-started/ │ │ │ ├── _examples/ │ │ │ │ ├── debug_error_example.py │ │ │ │ ├── hello_world.py │ │ │ │ ├── run_fastapi.py │ │ │ │ ├── run_flask.py │ │ │ │ ├── run_sanic.py │ │ │ │ ├── run_starlette.py │ │ │ │ ├── run_tornado.py │ │ │ │ └── sample_app.py │ │ │ ├── _static/ │ │ │ │ ├── embed-doc-ex.html │ │ │ │ └── embed-reactpy-view/ │ │ │ │ ├── index.html │ │ │ │ └── main.py │ │ │ ├── index.rst │ │ │ ├── installing-reactpy.rst │ │ │ └── running-reactpy.rst │ │ ├── managing-state/ │ │ │ ├── combining-contexts-and-reducers/ │ │ │ │ └── index.rst │ │ │ ├── deeply-sharing-state-with-contexts/ │ │ │ │ └── index.rst │ │ │ ├── how-to-structure-state/ │ │ │ │ └── index.rst │ │ │ ├── index.rst │ │ │ ├── sharing-component-state/ │ │ │ │ ├── _examples/ │ │ │ │ │ ├── filterable_list/ │ │ │ │ │ │ ├── data.json │ │ │ │ │ │ └── main.py │ │ │ │ │ └── synced_inputs/ │ │ │ │ │ └── main.py │ │ │ │ └── index.rst │ │ │ ├── simplifying-updates-with-reducers/ │ │ │ │ └── index.rst │ │ │ └── when-and-how-to-reset-state/ │ │ │ └── index.rst │ │ └── understanding-reactpy/ │ │ ├── index.rst │ │ ├── layout-render-servers.rst │ │ ├── representing-html.rst │ │ ├── the-rendering-pipeline.rst │ │ ├── the-rendering-process.rst │ │ ├── what-are-components.rst │ │ ├── why-reactpy-needs-keys.rst │ │ └── writing-tests.rst │ ├── index.rst │ └── reference/ │ ├── _examples/ │ │ ├── character_movement/ │ │ │ └── main.py │ │ ├── click_count.py │ │ ├── material_ui_switch.py │ │ ├── matplotlib_plot.py │ │ ├── network_graph.py │ │ ├── pigeon_maps.py │ │ ├── simple_dashboard.py │ │ ├── slideshow.py │ │ ├── snake_game.py │ │ ├── todo.py │ │ ├── use_reducer_counter.py │ │ ├── use_state_counter.py │ │ └── victory_chart.py │ ├── _static/ │ │ └── vdom-json-schema.json │ ├── browser-events.rst │ ├── hooks-api.rst │ ├── html-attributes.rst │ ├── javascript-api.rst │ └── specifications.rst ├── pyproject.toml ├── src/ │ ├── build_scripts/ │ │ ├── build_js_app.py │ │ ├── build_js_client.py │ │ ├── build_js_event_to_object.py │ │ ├── clean_js_dir.py │ │ ├── copy_dir.py │ │ ├── delete_old_coverage.py │ │ └── install_playwright.py │ ├── js/ │ │ ├── .gitignore │ │ ├── bun.lockb │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── packages/ │ │ │ ├── @reactpy/ │ │ │ │ ├── app/ │ │ │ │ │ ├── bun.lockb │ │ │ │ │ ├── package.json │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── preact-dom.ts │ │ │ │ │ │ ├── preact-jsx-runtime.ts │ │ │ │ │ │ └── preact.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ └── client/ │ │ │ │ ├── README.md │ │ │ │ ├── bun.lockb │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── bind.tsx │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── components.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logger.ts │ │ │ │ │ ├── mount.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── vdom.tsx │ │ │ │ │ └── websocket.ts │ │ │ │ └── tsconfig.json │ │ │ └── event-to-object/ │ │ │ ├── README.md │ │ │ ├── bun.lockb │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.ts │ │ │ ├── tests/ │ │ │ │ ├── event-to-object.test.ts │ │ │ │ └── tooling/ │ │ │ │ ├── check.ts │ │ │ │ ├── mock.ts │ │ │ │ └── setup.js │ │ │ ├── tsconfig.json │ │ │ └── vitest.config.ts │ │ └── tsconfig.json │ └── reactpy/ │ ├── __init__.py │ ├── _console/ │ │ ├── __init__.py │ │ ├── ast_utils.py │ │ ├── cli.py │ │ ├── rewrite_keys.py │ │ └── rewrite_props.py │ ├── _html.py │ ├── _option.py │ ├── _warnings.py │ ├── config.py │ ├── core/ │ │ ├── __init__.py │ │ ├── _f_back.py │ │ ├── _life_cycle_hook.py │ │ ├── _thread_local.py │ │ ├── component.py │ │ ├── events.py │ │ ├── hooks.py │ │ ├── layout.py │ │ ├── serve.py │ │ └── vdom.py │ ├── executors/ │ │ ├── __init__.py │ │ ├── asgi/ │ │ │ ├── __init__.py │ │ │ ├── middleware.py │ │ │ ├── pyscript.py │ │ │ ├── standalone.py │ │ │ └── types.py │ │ ├── pyscript/ │ │ │ ├── __init__.py │ │ │ ├── component_template.py │ │ │ ├── components.py │ │ │ ├── layout_handler.py │ │ │ └── utils.py │ │ └── utils.py │ ├── logging.py │ ├── py.typed │ ├── reactjs/ │ │ ├── __init__.py │ │ ├── module.py │ │ ├── types.py │ │ └── utils.py │ ├── static/ │ │ └── pyscript-hide-debug.css │ ├── templatetags/ │ │ ├── __init__.py │ │ └── jinja.py │ ├── testing/ │ │ ├── __init__.py │ │ ├── backend.py │ │ ├── common.py │ │ ├── display.py │ │ └── logs.py │ ├── transforms.py │ ├── types.py │ ├── utils.py │ ├── web/ │ │ ├── __init__.py │ │ ├── module.py │ │ └── utils.py │ └── widgets.py └── tests/ ├── __init__.py ├── conftest.py ├── sample.py ├── templates/ │ ├── index.html │ ├── jinja_bad_kwargs.html │ └── pyscript.html ├── test_asgi/ │ ├── __init__.py │ ├── pyscript_components/ │ │ ├── load_first.py │ │ ├── load_second.py │ │ └── root.py │ ├── test_init.py │ ├── test_middleware.py │ ├── test_pyscript.py │ ├── test_standalone.py │ └── test_utils.py ├── test_client.py ├── test_config.py ├── test_console/ │ ├── __init__.py │ ├── test_rewrite_keys.py │ └── test_rewrite_props.py ├── test_core/ │ ├── __init__.py │ ├── test_component.py │ ├── test_events.py │ ├── test_hooks.py │ ├── test_layout.py │ ├── test_serve.py │ └── test_vdom.py ├── test_html.py ├── test_option.py ├── test_pyscript/ │ ├── __init__.py │ ├── pyscript_components/ │ │ ├── custom_root_name.py │ │ └── root.py │ ├── test_components.py │ └── test_utils.py ├── test_reactjs/ │ ├── __init__.py │ ├── js_fixtures/ │ │ ├── callable-prop.js │ │ ├── component-can-have-child.js │ │ ├── export-resolution/ │ │ │ ├── index.js │ │ │ ├── one.js │ │ │ └── two.js │ │ ├── exports-syntax.js │ │ ├── exports-two-components.js │ │ ├── generic-module.js │ │ ├── keys-properly-propagated.js │ │ ├── nest-custom-under-web.js │ │ ├── set-flag-when-unmount-is-called.js │ │ ├── simple-button.js │ │ └── subcomponent-notation.js │ ├── test_modules.py │ ├── test_modules_from_npm.py │ └── test_utils.py ├── test_sample.py ├── test_testing.py ├── test_utils.py ├── test_web/ │ ├── __init__.py │ ├── js_fixtures/ │ │ ├── callable-prop.js │ │ ├── component-can-have-child.js │ │ ├── export-resolution/ │ │ │ ├── index.js │ │ │ ├── one.js │ │ │ └── two.js │ │ ├── exports-syntax.js │ │ ├── exports-two-components.js │ │ ├── generic-module.js │ │ ├── keys-properly-propagated.js │ │ ├── set-flag-when-unmount-is-called.js │ │ ├── simple-button.js │ │ └── subcomponent-notation.js │ └── test_module.py ├── test_widgets.py └── tooling/ ├── __init__.py ├── aio.py ├── common.py ├── hooks.py ├── layout.py └── select.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 end_of_line = lf [*.py] indent_size = 4 max_line_length = 120 [*.md] indent_size = 4 [*.yml] indent_size = 4 [*.html] max_line_length = off [*.js] max_line_length = off [*.css] indent_size = 4 max_line_length = off # Tests can violate line width restrictions in the interest of clarity. [**/test_*.py] max_line_length = off ================================================ FILE: .github/CODEOWNERS ================================================ @maintainers ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [archmonger, rmorshea] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Start a Discussion url: https://github.com/reactive-python/reactpy/discussions about: Report issues, request features, ask questions, and share ideas ================================================ FILE: .github/ISSUE_TEMPLATE/issue-form.yml ================================================ name: Plan a Task description: Create a detailed plan of action (ONLY START AFTER DISCUSSION PLEASE 🙏). labels: ["flag-triage"] body: - type: textarea attributes: label: Current Situation description: Discuss how things currently are, why they require action, and any relevant prior discussion/context. validations: required: false - type: textarea attributes: label: Proposed Actions description: Describe what ought to be done, and why that will address the reasons for action mentioned above. validations: required: false ================================================ FILE: .github/copilot-instructions.md ================================================ # 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 three packages: event-to-object, @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/event-to-object/` -- Event serialization package - `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/` - Three main packages: event-to-object, @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 ================================================ FILE: .github/pull_request_template.md ================================================ ## Description ## Checklist Please update this checklist as you complete each item: - [ ] Tests have been developed for bug fixes or new functionality. - [ ] The changelog has been updated, if necessary. - [ ] Documentation has been updated, if necessary. - [ ] GitHub Issues closed by this PR have been linked. By submitting this pull request I agree that all contributions comply with this project's open source license(s). ================================================ FILE: .github/workflows/.hatch-run.yml ================================================ name: hatch-run on: workflow_call: inputs: job-name: required: true type: string run-cmd: required: true type: string runs-on: required: false type: string default: '["ubuntu-latest"]' python-version: required: false type: string default: '["3.x"]' secrets: node-auth-token: required: false pypi-username: required: false pypi-password: required: false jobs: hatch: name: ${{ format(inputs.job-name, matrix.python-version, matrix.runs-on) }} strategy: matrix: python-version: ${{ fromJson(inputs.python-version) }} runs-on: ${{ fromJson(inputs.runs-on) }} runs-on: ${{ matrix.runs-on }} steps: - uses: actions/checkout@v4 - if: runner.os == 'Windows' name: Cache Playwright Install uses: actions/cache@v5 with: path: C:\Users\runneradmin\AppData\Local\ms-playwright\ key: ${{ runner.os }}-playwright # FIXME: Temporarily added setup-node to fix lack of "Trusted Publishing" in Bun # Ref: https://github.com/oven-sh/bun/issues/15601 - uses: actions/setup-node@v6 with: node-version: 24 registry-url: https://registry.npmjs.org/ - uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Use Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: "pip" - name: Install Python Dependencies run: pip install hatch - name: Run Scripts env: NPM_CONFIG_TOKEN: ${{ secrets.node-auth-token }} HATCH_INDEX_USER: ${{ secrets.pypi-username }} HATCH_INDEX_AUTH: ${{ secrets.pypi-password }} run: ${{ inputs.run-cmd }} ================================================ FILE: .github/workflows/check.yml ================================================ name: check on: push: branches: - main pull_request: branches: - "*" schedule: - cron: "0 0 * * 0" jobs: test-python-coverage: uses: ./.github/workflows/.hatch-run.yml with: job-name: "python-{0}" # Retries needed because GitHub workers sometimes lag enough to crash parallel workers run-cmd: "hatch test --parallel --cover --retries 10" lint-python: uses: ./.github/workflows/.hatch-run.yml with: job-name: "python-{0}" run-cmd: "hatch fmt src/reactpy --check && hatch run python:type_check" test-python: uses: ./.github/workflows/.hatch-run.yml with: job-name: "python-{0} {1}" 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-javascript: uses: ./.github/workflows/.hatch-run.yml with: job-name: "{1}" run-cmd: "hatch run javascript:test" lint-javascript: uses: ./.github/workflows/.hatch-run.yml with: job-name: "{1}" run-cmd: "hatch run javascript:check" ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: codeql on: push: branches: [main] pull_request: # The branches below must be a subset of the branches above branches: [main] schedule: - cron: "43 3 * * 3" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["javascript", "python"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 ================================================ FILE: .github/workflows/publish.yml ================================================ name: publish on: release: types: [published] permissions: contents: read # Required to checkout the code id-token: write # Required to sign the NPM publishing statements jobs: publish-reactpy: if: startsWith(github.event.release.name, 'reactpy ') || startsWith(github.event.release.tag_name, 'reactpy-') uses: ./.github/workflows/.hatch-run.yml with: job-name: "Publish to PyPI" run-cmd: "hatch run javascript:build && hatch build --clean && hatch publish --yes" secrets: pypi-username: ${{ secrets.PYPI_USERNAME }} pypi-password: ${{ secrets.PYPI_PASSWORD }} publish-reactpy-client: if: startsWith(github.event.release.name, '@reactpy/client ') || startsWith(github.event.release.tag_name, '@reactpy/client-') uses: ./.github/workflows/.hatch-run.yml with: job-name: "Publish to NPM" run-cmd: "hatch run javascript:publish_client" publish-event-to-object: if: startsWith(github.event.release.name, 'event-to-object ') || startsWith(github.event.release.tag_name, 'event-to-object-') uses: ./.github/workflows/.hatch-run.yml with: job-name: "Publish to NPM" run-cmd: "hatch run javascript:publish_event_to_object" ================================================ FILE: .gitignore ================================================ # --- Build Artifacts --- src/reactpy/static/*.js* src/reactpy/static/morphdom/ src/reactpy/static/pyscript/ src/js/**/*.tgz src/js/**/LICENSE # --- Jupyter --- *.ipynb_checkpoints *Untitled*.ipynb # --- Jupyter Repo 2 Docker --- .local .ipython .cache .bash_history .python_history .jupyter # --- Python --- .hatch .venv* venv* MANIFEST build dist .eggs *.egg-info __pycache__/ *.py[cod] .tox .nox pip-wheel-metadata # --- PyEnv --- .python-version # -- Python Tests --- .coverage.* *.coverage *.pytest_cache *.mypy_cache # --- IDE --- .idea .vscode # --- JS --- node_modules ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: local hooks: - id: lint-py-fix name: Fix Python Lint entry: hatch run lint-py language: system args: [--fix] pass_filenames: false files: \.py$ - repo: local hooks: - id: lint-js-fix name: Fix JS Lint entry: hatch run lint-js --fix language: system pass_filenames: false files: \.(js|jsx|ts|tsx)$ - repo: local hooks: - id: lint-py-check name: Check Python Lint entry: hatch run lint-py language: system pass_filenames: false files: \.py$ - repo: local hooks: - id: lint-js-check name: Check JS Lint entry: hatch run lint-py language: system pass_filenames: false files: \.(js|jsx|ts|tsx)$ ================================================ FILE: .prettierrc ================================================ { "proseWrap": "never", "trailingComma": "all", "endOfLine": "auto" } ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - Added support for Python 3.12, 3.13, and 3.14. - Added type hints to `reactpy.html` attributes. - Added support for nested components in web modules - Added support for inline JavaScript as event handlers or other attributes that expect a callable via `reactpy.types.InlineJavaScript` - Event functions can now call `event.preventDefault()` and `event.stopPropagation()` methods directly on the event data object, rather than using the `@event` decorator. - Event data now supports accessing properties via dot notation (ex. `event.target.value`). - Added support for partial functions in EventHandler - Added `reactpy.types.Event` to provide type hints for the standard `data` function argument (for example `def on_click(event: Event): ...`). - Added `asgi` and `jinja` installation extras (for example `pip install reactpy[asgi, jinja]`). - Added `reactpy.executors.asgi.ReactPy` that can be used to run ReactPy in standalone mode via ASGI. - Added `reactpy.executors.asgi.ReactPyCsr` that can be used to run ReactPy in standalone mode via ASGI, but rendered entirely client-sided. - Added `reactpy.executors.asgi.ReactPyMiddleware` that can be used to utilize ReactPy within any ASGI compatible framework. - Added `reactpy.templatetags.ReactPyJinja` that can be used alongside `ReactPyMiddleware` to embed several ReactPy components into your existing application. This includes the following template tags: `{% component %}`, `{% pyscript_component %}`, and `{% pyscript_setup %}`. - Added `reactpy.pyscript_component` that can be used to embed ReactPy components into your existing application. - Added `reactpy.use_async_effect` hook. - Added `reactpy.Vdom` primitive interface for creating VDOM dictionaries. - Added `reactpy.reactjs.component_from_file` to import ReactJS components from a file. - Added `reactpy.reactjs.component_from_url` to import ReactJS components from a URL. - Added `reactpy.reactjs.component_from_string` to import ReactJS components from a string. - Added `reactpy.reactjs.component_from_npm` to import ReactJS components from NPM. - Added `reactpy.h` as a shorthand alias for `reactpy.html`. ### Changed - The `key` attribute is now stored within `attributes` in the VDOM spec. - Substitute client-side usage of `react` with `preact`. - Script elements no longer support behaving like effects. They now strictly behave like plain HTML scripts. - The `reactpy.html` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a `` element by calling `html.data_table()`. - Change `set_state` comparison method to check equality with `==` more consistently. - Add support for rendering `@component` children within `vdom_to_html`. - Renamed the `use_location` hook's `search` attribute to `query_string`. - Renamed the `use_location` hook's `pathname` attribute to `path`. - Renamed `reactpy.config.REACTPY_DEBUG_MODE` to `reactpy.config.REACTPY_DEBUG`. - ReactPy no longer auto-converts `snake_case` props to `camelCase`. It is now the responsibility of the user to ensure that props are in the correct format. - Rewrite the `event-to-object` package to be more robust at handling properties on events. - Custom JS components will now automatically assume you are using ReactJS in the absence of a `bind` function. - Refactor layout rendering logic to improve readability and maintainability. - The JavaScript package `@reactpy/client` now exports `React` and `ReactDOM`, which allows third-party components to re-use the same React instance as ReactPy. - `reactpy.html` will now automatically flatten lists recursively (ex. `reactpy.html(["child1", ["child2"]])`) - `reactpy.utils.reactpy_to_string` will now retain the user's original casing for `data-*` and `aria-*` attributes. - `reactpy.utils.string_to_reactpy` has been upgraded to handle more complex scenarios without causing ReactJS rendering errors. - `reactpy.core.vdom._CustomVdomDictConstructor` has been moved to `reactpy.types.CustomVdomConstructor`. - `reactpy.core.vdom._EllipsisRepr` has been moved to `reactpy.types.EllipsisRepr`. - `reactpy.types.VdomDictConstructor` has been renamed to `reactpy.types.VdomConstructor`. - `REACTPY_ASYNC_RENDERING` can now de-duplicate and cascade renders where necessary. - `REACTPY_ASYNC_RENDERING` is now defaulted to `True` for up to 40x performance improvements in environments with high concurrency. ### Deprecated - `reactpy.web.module_from_file` is deprecated. Use `reactpy.reactjs.component_from_file` instead. - `reactpy.web.module_from_url` is deprecated. Use `reactpy.reactjs.component_from_url` instead. - `reactpy.web.module_from_string` is deprecated. Use `reactpy.reactjs.component_from_string` instead. - `reactpy.web.export` is deprecated. Use `reactpy.reactjs.component_from_*` instead. - `reactpy.web.*` is deprecated. Use `reactpy.reactjs.*` instead. ### Removed - Removed support for Python 3.9 and 3.10. - Removed the ability to import `reactpy.html.*` elements directly. You must now call `html.*` to access the elements. - Removed backend specific installation extras (such as `pip install reactpy[starlette]`). - Removed support for async functions within `reactpy.use_effect` hook. Use `reactpy.use_async_effect` instead. - Removed deprecated function `module_from_template`. - Removed deprecated exception type `reactpy.core.serve.Stop`. - Removed deprecated component `reactpy.widgets.hotswap`. - Removed `reactpy.sample` module. - Removed `reactpy.svg` module. Contents previously within `reactpy.svg.*` can now be accessed via `reactpy.html.svg.*`. - Removed `reactpy.html._` function. Use `reactpy.html(...)` or `reactpy.html.fragment(...)` instead. - Removed `reactpy.run`. See the documentation for the new method to run ReactPy applications. - Removed `reactpy.backend.*`. See the documentation for the new method to run ReactPy applications. - Removed `reactpy.core.types` module. Use `reactpy.types` instead. - Removed `reactpy.utils.html_to_vdom`. Use `reactpy.utils.string_to_reactpy` instead. - Removed `reactpy.utils.vdom_to_html`. Use `reactpy.utils.reactpy_to_string` instead. - Removed `reactpy.vdom`. Use `reactpy.Vdom` instead. - Removed `reactpy.core.make_vdom_constructor`. Use `reactpy.Vdom` instead. - Removed `reactpy.core.custom_vdom_constructor`. Use `reactpy.Vdom` instead. - Removed `reactpy.Layout` top-level re-export. Use `reactpy.core.layout.Layout` instead. - Removed `reactpy.types.LayoutType`. Use `reactpy.types.BaseLayout` instead. - Removed `reactpy.types.ContextProviderType`. Use `reactpy.types.ContextProvider` instead. - Removed `reactpy.core.hooks._ContextProvider`. Use `reactpy.types.ContextProvider` instead. - Removed `reactpy.web.utils`. Use `reactpy.reactjs.utils` instead. ### Fixed - Fixed a bug where script elements would not render to the DOM as plain text. - Fixed a bug where the `key` property provided within server-side ReactPy code was failing to propagate to the front-end JavaScript components. - Fixed a bug where `RuntimeError("Hook stack is in an invalid state")` errors could be generated when using a webserver that reuses threads. - Allow for ReactPy and ReactJS components to be arbitrarily inserted onto the page with any possible hierarchy. ## [1.1.0] - 2024-11-24 ### Fixed - Fixed broken `module_from_template` due to a recent release of `requests`. - Fixed `module_from_template` not working when using Flask backend. - Fixed `UnicodeDecodeError` when using `reactpy.web.export`. - Fixed needless unmounting of JavaScript components during each ReactPy render. - Fixed missing `event["target"]["checked"]` on checkbox inputs. - Fixed missing static files on `sdist` Python distribution. ### Added - Allow concurrently rendering discrete component trees - enable this experimental feature by setting `REACTPY_ASYNC_RENDERING=true`. This improves the overall responsiveness of your app in situations where larger renders would otherwise block smaller renders from executing. ### Changed - Previously `None`, when present in an HTML element, would render as the string `"None"`. Now `None` will not render at all. This is now equivalent to how `None` is handled when returned from components. - Move hooks from `reactpy.backend.hooks` into `reactpy.core.hooks`. ### Deprecated - The `Stop` exception. Recent releases of `anyio` have made this exception difficult to use since it now raises an `ExceptionGroup`. This exception was primarily used for internal testing purposes and so is now deprecated. - Deprecate `reactpy.backend.hooks` since the hooks have been moved into `reactpy.core.hooks`. ## [1.0.2] - 2023-07-03 ### Fixed - Fix rendering bug when children change positions. ## [1.0.1] - 2023-06-16 ### Changed - Warn and attempt to fix missing mime types, which can result in `reactpy.run` not working as expected. - Rename `reactpy.backend.BackendImplementation` to `reactpy.backend.BackendType`. - Allow `reactpy.run` to fail in more predictable ways. ### Fixed - Better traceback for JSON serialization errors. - Explain that JS component attributes must be JSON. - Fix `reactpy.run` port assignment sometimes attaching to in-use ports on Windows. - Fix `reactpy.run` not recognizing `fastapi`. ## [1.0.0] - 2023-03-14 ### Changed - Reverts PR 841 as per the conclusion in discussion 916, but preserves the ability to declare attributes with snake_case. - Reverts PR 886 due to issue 896. - Revamped element constructor interface. Now instead of passing a dictionary of attributes to element constructors, attributes are declared using keyword arguments. For example, instead of writing: ### Deprecated - Declaration of keys via keyword arguments in standard elements. A script has been added to automatically convert old usages where possible. ### Removed - Accidental import of reactpy.testing. ### Fixed - Minor issues with camelCase rewrite CLI utility. - Minor type hint issue with `VdomDictConstructor`. - Stale event handlers after disconnect/reconnect cycle. - Fixed CLI not registered as entry point. - Unification of component and VDOM constructor interfaces. [Unreleased]: https://github.com/reactive-python/reactpy/compare/reactpy-v1.1.0...HEAD [1.1.0]: https://github.com/reactive-python/reactpy/compare/reactpy-v1.0.2...reactpy-v1.1.0 [1.0.2]: https://github.com/reactive-python/reactpy/compare/reactpy-v1.0.1...reactpy-v1.0.2 [1.0.1]: https://github.com/reactive-python/reactpy/compare/reactpy-v1.0.0...reactpy-v1.0.1 [1.0.0]: https://github.com/reactive-python/reactpy/compare/0.44.0...reactpy-v1.0.0 [0.44.0]: https://github.com/reactive-python/reactpy/compare/0.43.0...0.44.0 [0.43.0]: https://github.com/reactive-python/reactpy/compare/0.42.0...0.43.0 [0.42.0]: https://github.com/reactive-python/reactpy/compare/0.41.0...0.42.0 [0.41.0]: https://github.com/reactive-python/reactpy/compare/0.40.2...0.41.0 [0.40.2]: https://github.com/reactive-python/reactpy/compare/0.40.1...0.40.2 [0.40.1]: https://github.com/reactive-python/reactpy/compare/0.40.0...0.40.1 [0.40.0]: https://github.com/reactive-python/reactpy/compare/0.39.0...0.40.0 [0.39.0]: https://github.com/reactive-python/reactpy/compare/0.38.1...0.39.0 [0.38.1]: https://github.com/reactive-python/reactpy/compare/0.38.0...0.38.1 [0.38.0]: https://github.com/reactive-python/reactpy/compare/0.37.2...0.38.0 [0.37.2]: https://github.com/reactive-python/reactpy/compare/0.37.1...0.37.2 [0.37.1]: https://github.com/reactive-python/reactpy/compare/0.37.0...0.37.1 [0.37.0]: https://github.com/reactive-python/reactpy/compare/0.36.3...0.37.0 [0.36.3]: https://github.com/reactive-python/reactpy/compare/0.36.2...0.36.3 [0.36.2]: https://github.com/reactive-python/reactpy/compare/0.36.1...0.36.2 [0.36.1]: https://github.com/reactive-python/reactpy/compare/0.36.0...0.36.1 [0.36.0]: https://github.com/reactive-python/reactpy/compare/0.35.4...0.36.0 [0.35.4]: https://github.com/reactive-python/reactpy/compare/0.35.3...0.35.4 [0.35.3]: https://github.com/reactive-python/reactpy/compare/0.35.2...0.35.3 [0.35.2]: https://github.com/reactive-python/reactpy/compare/0.35.1...0.35.2 [0.35.1]: https://github.com/reactive-python/reactpy/compare/0.35.0...0.35.1 [0.35.0]: https://github.com/reactive-python/reactpy/compare/0.34.0...0.35.0 [0.34.0]: https://github.com/reactive-python/reactpy/compare/0.33.3...0.34.0 [0.33.3]: https://github.com/reactive-python/reactpy/compare/0.33.2...0.33.3 [0.33.2]: https://github.com/reactive-python/reactpy/compare/0.33.1...0.33.2 [0.33.1]: https://github.com/reactive-python/reactpy/compare/0.33.0...0.33.1 [0.33.0]: https://github.com/reactive-python/reactpy/compare/0.32.0...0.33.0 [0.32.0]: https://github.com/reactive-python/reactpy/compare/0.31.0...0.32.0 [0.31.0]: https://github.com/reactive-python/reactpy/compare/0.30.1...0.31.0 [0.30.1]: https://github.com/reactive-python/reactpy/compare/0.30.0...0.30.1 [0.30.0]: https://github.com/reactive-python/reactpy/compare/0.29.0...0.30.0 [0.29.0]: https://github.com/reactive-python/reactpy/compare/0.28.0...0.29.0 [0.28.0]: https://github.com/reactive-python/reactpy/compare/0.27.0...0.28.0 [0.27.0]: https://github.com/reactive-python/reactpy/compare/0.26.0...0.27.0 [0.26.0]: https://github.com/reactive-python/reactpy/compare/0.25.0...0.26.0 [0.25.0]: https://github.com/reactive-python/reactpy/compare/0.24.0...0.25.0 [0.24.0]: https://github.com/reactive-python/reactpy/compare/0.23.1...0.24.0 [0.23.1]: https://github.com/reactive-python/reactpy/compare/0.23.0...0.23.1 [0.23.0]: https://github.com/reactive-python/reactpy/releases/tag/0.23.0 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ryan.morshead@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) Reactive Python and affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ReactPy

[ReactPy](https://reactpy.dev/) is a library for building user interfaces in Python without Javascript. ReactPy interfaces are made from components that look and behave similar to those found in [ReactJS](https://reactjs.org/). 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.
Supported Backends
Built-in External
Flask, FastAPI, Sanic, Tornado Django, Jupyter, Plotly-Dash
# At a Glance To get a rough idea of how to write apps in ReactPy, take a look at this tiny _Hello World_ application. ```python from reactpy import component, html, run @component def hello_world(): return html.h1("Hello, World!") run(hello_world) ``` # Resources Follow the links below to find out more about this project. - [Try ReactPy (Jupyter Notebook)](https://mybinder.org/v2/gh/reactive-python/reactpy-jupyter/main?urlpath=lab/tree/notebooks/introduction.ipynb) - [Documentation](https://reactpy.dev/) - [GitHub Discussions](https://github.com/reactive-python/reactpy/discussions) - [Discord](https://discord.gg/uNb5P4hA9X) - [Contributor Guide](https://reactpy.dev/docs/about/contributor-guide.html) - [Code of Conduct](https://github.com/reactive-python/reactpy/blob/main/CODE_OF_CONDUCT.md) ================================================ FILE: docs/.gitignore ================================================ build source/_auto source/_static/custom.js source/vdom-json-schema.json ================================================ FILE: docs/Dockerfile ================================================ FROM python:3.11 WORKDIR /app/ RUN apt-get update # Create/Activate Python Venv # --------------------------- ENV VIRTUAL_ENV=/opt/venv RUN python3 -m venv $VIRTUAL_ENV ENV PATH="$VIRTUAL_ENV/bin:$PATH" # Install Python Build Dependencies # --------------------------------- RUN pip install --upgrade pip poetry hatch uv RUN curl -fsSL https://bun.sh/install | bash ENV PATH="/root/.bun/bin:$PATH" # Copy Files # ---------- COPY LICENSE ./ COPY README.md ./ COPY pyproject.toml ./ COPY src ./src COPY docs ./docs COPY branding ./branding # Install and Build Docs # ---------------------- WORKDIR /app/docs/ RUN poetry install -v RUN sphinx-build -v -W -b html source build # Define Entrypoint # ----------------- ENV PORT=5000 ENV REACTPY_DEBUG=1 ENV REACTPY_CHECK_VDOM_SPEC=0 CMD ["python", "main.py"] ================================================ FILE: docs/README.md ================================================ # ReactPy's Documentation ... ================================================ FILE: docs/docs_app/__init__.py ================================================ ================================================ FILE: docs/docs_app/app.py ================================================ from logging import getLogger from pathlib import Path from sanic import Sanic, response from docs_app.examples import get_normalized_example_name, load_examples from reactpy import component from reactpy.backend.sanic import Options, configure, use_request from reactpy.types import ComponentConstructor THIS_DIR = Path(__file__).parent DOCS_DIR = THIS_DIR.parent DOCS_BUILD_DIR = DOCS_DIR / "build" REACTPY_MODEL_SERVER_URL_PREFIX = "/_reactpy" logger = getLogger(__name__) REACTPY_MODEL_SERVER_URL_PREFIX = "/_reactpy" @component def Example(): raw_view_id = use_request().get_args().get("view_id") view_id = get_normalized_example_name(raw_view_id) return _get_examples()[view_id]() def _get_examples(): if not _EXAMPLES: _EXAMPLES.update(load_examples()) return _EXAMPLES def reload_examples(): _EXAMPLES.clear() _EXAMPLES.update(load_examples()) _EXAMPLES: dict[str, ComponentConstructor] = {} def make_app(name: str): app = Sanic(name) app.static("/docs", str(DOCS_BUILD_DIR)) @app.route("/") async def forward_to_index(_): return response.redirect("/docs/index.html") configure( app, Example, Options(url_prefix=REACTPY_MODEL_SERVER_URL_PREFIX), ) return app ================================================ FILE: docs/docs_app/dev.py ================================================ import asyncio import os import threading import time import webbrowser from sphinx_autobuild.cli import ( Server, _get_build_args, _get_ignore_handler, find_free_port, get_builder, get_parser, ) from docs_app.app import make_app, reload_examples from reactpy.backend.sanic import serve_development_app from reactpy.testing import clear_reactpy_web_modules_dir # these environment variable are used in custom Sphinx extensions os.environ["REACTPY_DOC_EXAMPLE_SERVER_HOST"] = "127.0.0.1:5555" os.environ["REACTPY_DOC_STATIC_SERVER_HOST"] = "" def wrap_builder(old_builder): # This is the bit that we're injecting to get the example components to reload too app = make_app("docs_dev_app") thread_started = threading.Event() def run_in_thread(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) server_started = asyncio.Event() async def set_thread_event_when_started(): await server_started.wait() thread_started.set() loop.run_until_complete( asyncio.gather( serve_development_app(app, "127.0.0.1", 5555, server_started), set_thread_event_when_started(), ) ) threading.Thread(target=run_in_thread, daemon=True).start() thread_started.wait() def new_builder(): clear_reactpy_web_modules_dir() reload_examples() old_builder() return new_builder def main(): # Mostly copied from https://github.com/executablebooks/sphinx-autobuild/blob/b54fb08afc5112bfcda1d844a700c5a20cd6ba5e/src/sphinx_autobuild/cli.py parser = get_parser() args = parser.parse_args() srcdir = os.path.realpath(args.sourcedir) outdir = os.path.realpath(args.outdir) if not os.path.exists(outdir): os.makedirs(outdir) server = Server() build_args, pre_build_commands = _get_build_args(args) builder = wrap_builder( get_builder( server.watcher, build_args, host=args.host, port=args.port, pre_build_commands=pre_build_commands, ) ) ignore_handler = _get_ignore_handler(args) server.watch(srcdir, builder, ignore=ignore_handler) for dirpath in args.additional_watched_dirs: real_dirpath = os.path.realpath(dirpath) server.watch(real_dirpath, builder, ignore=ignore_handler) server.watch(outdir, ignore=ignore_handler) if not args.no_initial_build: builder() # Find the free port portn = args.port or find_free_port() if args.openbrowser is True: def opener(): time.sleep(args.delay) webbrowser.open(f"http://{args.host}:{args.port}/index.html") threading.Thread(target=opener, daemon=True).start() server.serve(port=portn, host=args.host, root=outdir) ================================================ FILE: docs/docs_app/examples.py ================================================ from __future__ import annotations from collections.abc import Callable, Iterator from io import StringIO from pathlib import Path from traceback import format_exc import reactpy from reactpy.types import ComponentType HERE = Path(__file__) SOURCE_DIR = HERE.parent.parent / "source" CONF_FILE = SOURCE_DIR / "conf.py" RUN_ReactPy = reactpy.run def load_examples() -> Iterator[tuple[str, Callable[[], ComponentType]]]: for name in all_example_names(): yield name, load_one_example(name) def all_example_names() -> set[str]: names = set() for file in _iter_example_files(SOURCE_DIR): path = file.parent if file.name == "main.py" else file names.add("/".join(path.relative_to(SOURCE_DIR).with_suffix("").parts)) return names def load_one_example(file_or_name: Path | str) -> Callable[[], ComponentType]: return lambda: ( # we use a lambda to ensure each instance is fresh _load_one_example(file_or_name) ) def get_normalized_example_name( name: str, relative_to: str | Path | None = SOURCE_DIR ) -> str: return "/".join( _get_root_example_path_by_name(name, relative_to).relative_to(SOURCE_DIR).parts ) def get_main_example_file_by_name( name: str, relative_to: str | Path | None = SOURCE_DIR ) -> Path: path = _get_root_example_path_by_name(name, relative_to) if path.is_dir(): return path / "main.py" else: return path.with_suffix(".py") def get_example_files_by_name( name: str, relative_to: str | Path | None = SOURCE_DIR ) -> list[Path]: path = _get_root_example_path_by_name(name, relative_to) if path.is_dir(): return [p for p in path.glob("*") if not p.is_dir()] else: path = path.with_suffix(".py") return [path] if path.exists() else [] def _iter_example_files(root: Path) -> Iterator[Path]: for path in root.iterdir(): if path.is_dir(): if not path.name.startswith("_") or path.name == "_examples": yield from _iter_example_files(path) elif path.suffix == ".py" and path != CONF_FILE: yield path def _load_one_example(file_or_name: Path | str) -> ComponentType: if isinstance(file_or_name, str): file = get_main_example_file_by_name(file_or_name) else: file = file_or_name if not file.exists(): raise FileNotFoundError(str(file)) print_buffer = _PrintBuffer() def capture_print(*args, **kwargs): buffer = StringIO() print(*args, file=buffer, **kwargs) print_buffer.write(buffer.getvalue()) captured_component_constructor = None def capture_component(component_constructor): nonlocal captured_component_constructor captured_component_constructor = component_constructor reactpy.run = capture_component try: code = compile(file.read_text(), str(file), "exec") exec( code, { "print": capture_print, "__file__": str(file), "__name__": file.stem, }, ) except Exception: return _make_error_display(format_exc()) finally: reactpy.run = RUN_ReactPy if captured_component_constructor is None: return _make_example_did_not_run(str(file)) @reactpy.component def Wrapper(): return reactpy.html.div(captured_component_constructor(), PrintView()) @reactpy.component def PrintView(): text, set_text = reactpy.hooks.use_state(print_buffer.getvalue()) print_buffer.set_callback(set_text) return ( reactpy.html.pre({"class_name": "printout"}, text) if text else reactpy.html.div() ) return Wrapper() def _get_root_example_path_by_name(name: str, relative_to: str | Path | None) -> Path: if not name.startswith("/") and relative_to is not None: rel_path = Path(relative_to) rel_path = rel_path.parent if rel_path.is_file() else rel_path else: rel_path = SOURCE_DIR return rel_path.joinpath(*name.split("/")).resolve() class _PrintBuffer: def __init__(self, max_lines: int = 10): self._callback = None self._lines = () self._max_lines = max_lines def set_callback(self, function: Callable[[str], None]) -> None: self._callback = function def getvalue(self) -> str: return "".join(self._lines) def write(self, text: str) -> None: if len(self._lines) == self._max_lines: self._lines = (*self._lines[1:], text) else: self._lines += (text,) if self._callback is not None: self._callback(self.getvalue()) def _make_example_did_not_run(example_name): @reactpy.component def ExampleDidNotRun(): return reactpy.html.code(f"Example {example_name} did not run") return ExampleDidNotRun() def _make_error_display(message): @reactpy.component def ShowError(): return reactpy.html.pre(message) return ShowError() ================================================ FILE: docs/docs_app/prod.py ================================================ import os from docs_app.app import make_app app = make_app("docs_prod_app") def main() -> None: app.run( host="0.0.0.0", # noqa: S104 port=int(os.environ.get("PORT", "5000")), workers=int(os.environ.get("WEB_CONCURRENCY", "1")), debug=bool(int(os.environ.get("DEBUG", "0"))), ) ================================================ FILE: docs/main.py ================================================ import sys from docs_app import dev, prod if __name__ == "__main__": if len(sys.argv) == 1: prod.main() else: dev.main() ================================================ FILE: docs/pyproject.toml ================================================ [tool.poetry] name = "docs_app" version = "0.0.0" description = "docs" authors = ["rmorshea "] 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" ================================================ FILE: docs/source/_custom_js/README.md ================================================ # Custom Javascript for ReactPy's Docs Build the javascript with ``` bun run build ``` This will drop a javascript bundle into `../_static/custom.js` ================================================ FILE: docs/source/_custom_js/package.json ================================================ { "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" } } ================================================ FILE: docs/source/_custom_js/rollup.config.js ================================================ 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); }, }; ================================================ FILE: docs/source/_custom_js/src/index.js ================================================ 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); } ================================================ FILE: docs/source/_exts/async_doctest.py ================================================ 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) ================================================ FILE: docs/source/_exts/autogen_api_docs.py ================================================ 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 ================================================ FILE: docs/source/_exts/build_custom_js.py ================================================ 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 ================================================ FILE: docs/source/_exts/copy_vdom_json_schema.py ================================================ 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) ================================================ FILE: docs/source/_exts/custom_autosectionlabel.py ================================================ """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, } ================================================ FILE: docs/source/_exts/patched_html_translator.py ================================================ 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) ================================================ FILE: docs/source/_exts/reactpy_example.py ================================================ 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) ================================================ FILE: docs/source/_exts/reactpy_view.py ================================================ 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"""
""", format="html", ) ] def setup(app: Sphinx) -> None: app.add_directive("reactpy-view", IteractiveWidget) ================================================ FILE: docs/source/_static/css/furo-theme-overrides.css ================================================ .sidebar-container { width: 18em; } .sidebar-brand-text { display: none; } ================================================ FILE: docs/source/_static/css/larger-api-margins.css ================================================ :is(.data, .function, .class, .exception).py { margin-top: 3em; } :is(.attribute, .method).py { margin-top: 1.8em; } ================================================ FILE: docs/source/_static/css/larger-headings.css ================================================ h1, h2, h3, h4, h5, h6 { margin-top: 1.5em !important; font-weight: 900 !important; } ================================================ FILE: docs/source/_static/css/reactpy-view.css ================================================ .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; } ================================================ FILE: docs/source/_static/css/sphinx-design-overrides.css ================================================ .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; } ================================================ FILE: docs/source/_static/css/widget-output-css-overrides.css ================================================ .widget-container h1, .widget-container h2, .widget-container h3, .widget-container h4, .widget-container h5, .widget-container h6 { margin: 0 !important; } ================================================ FILE: docs/source/about/changelog.rst ================================================ .. THIS CHANGELOG HAS BEEN DEPRECATED. SEE TOP LEVEL CHANGELOG.md FILE INSTEAD. --- Changelog ========= .. note:: All notable changes to this project will be recorded in this document. The style of which is based on `Keep a Changelog `__. The versioning scheme for the project adheres to `Semantic Versioning `__. .. Using the following categories, list your changes in this order: .. [Added, Changed, Deprecated, Removed, Fixed, Security] .. Don't forget to remove deprecated code on each major release! Unreleased ---------- **Added** - :pull:`1113` - Added support for Python 3.12, 3.13, and 3.14. - :pull:`1281` - Added type hints to ``reactpy.html`` attributes. - :pull:`1285` - Added support for nested components in web modules - :pull:`1289` - Added support for inline JavaScript as event handlers or other attributes that expect a callable via ``reactpy.types.InlineJavaScript`` - :pull:`1308` - Event functions can now call ``event.preventDefault()`` and ``event.stopPropagation()`` methods directly on the event data object, rather than using the ``@event`` decorator. - :pull:`1308` - Event data now supports accessing properties via dot notation (ex. ``event.target.value``). - :pull:`1308` - Added ``reactpy.types.Event`` to provide type hints for the standard ``data`` function argument (for example ``def on_click(event: Event): ...``). - :pull:`1113` - Added ``asgi`` and ``jinja`` installation extras (for example ``pip install reactpy[asgi, jinja]``). - :pull:`1113` - Added ``reactpy.executors.asgi.ReactPy`` that can be used to run ReactPy in standalone mode via ASGI. - :pull:`1269` - Added ``reactpy.executors.asgi.ReactPyCsr`` that can be used to run ReactPy in standalone mode via ASGI, but rendered entirely client-sided. - :pull:`1113` - Added ``reactpy.executors.asgi.ReactPyMiddleware`` that can be used to utilize ReactPy within any ASGI compatible framework. - :pull:`1269` - Added ``reactpy.templatetags.ReactPyJinja`` that can be used alongside ``ReactPyMiddleware`` to embed several ReactPy components into your existing application. This includes the following template tags: ``{% component %}``, ``{% pyscript_component %}``, and ``{% pyscript_setup %}``. - :pull:`1269` - Added ``reactpy.pyscript_component`` that can be used to embed ReactPy components into your existing application. - :pull:`1264` - Added ``reactpy.use_async_effect`` hook. - :pull:`1281` - Added ``reactpy.Vdom`` primitive interface for creating VDOM dictionaries. - :pull:`1307` - Added ``reactpy.reactjs.component_from_file`` to import ReactJS components from a file. - :pull:`1307` - Added ``reactpy.reactjs.component_from_url`` to import ReactJS components from a URL. - :pull:`1307` - Added ``reactpy.reactjs.component_from_string`` to import ReactJS components from a string. - :pull:`1314` - Added ``reactpy.reactjs.component_from_npm`` to import ReactJS components from NPM. - :pull:`1314` - Added ``reactpy.h`` as a shorthand alias for ``reactpy.html``. **Changed** - :pull:`1314` - The ``key`` attribute is now stored within ``attributes`` in the VDOM spec. - :pull:`1251` - Substitute client-side usage of ``react`` with ``preact``. - :pull:`1239` - Script elements no longer support behaving like effects. They now strictly behave like plain HTML scripts. - :pull:`1255` - The ``reactpy.html`` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a ```` element by calling ``html.data_table()``. - :pull:`1256` - Change ``set_state`` comparison method to check equality with ``==`` more consistently. - :pull:`1257` - Add support for rendering ``@component`` children within ``vdom_to_html``. - :pull:`1113` - Renamed the ``use_location`` hook's ``search`` attribute to ``query_string``. - :pull:`1113` - Renamed the ``use_location`` hook's ``pathname`` attribute to ``path``. - :pull:`1113` - Renamed ``reactpy.config.REACTPY_DEBUG_MODE`` to ``reactpy.config.REACTPY_DEBUG``. - :pull:`1263` - ReactPy no longer auto-converts ``snake_case`` props to ``camelCase``. It is now the responsibility of the user to ensure that props are in the correct format. - :pull:`1196` - Rewrite the ``event-to-object`` package to be more robust at handling properties on events. - :pull:`1312` - Custom JS components will now automatically assume you are using ReactJS in the absence of a ``bind`` function. - :pull:`1312` - Refactor layout rendering logic to improve readability and maintainability. - :pull:`1113` - ``@reactpy/client`` now exports ``React`` and ``ReactDOM``. - :pull:`1281` - ``reactpy.html`` will now automatically flatten lists recursively (ex. ``reactpy.html(["child1", ["child2"]])``) - :pull:`1278` - ``reactpy.utils.reactpy_to_string`` will now retain the user's original casing for ``data-*`` and ``aria-*`` attributes. - :pull:`1278` - ``reactpy.utils.string_to_reactpy`` has been upgraded to handle more complex scenarios without causing ReactJS rendering errors. - :pull:`1281` - ``reactpy.core.vdom._CustomVdomDictConstructor`` has been moved to ``reactpy.types.CustomVdomConstructor``. - :pull:`1281` - ``reactpy.core.vdom._EllipsisRepr`` has been moved to ``reactpy.types.EllipsisRepr``. - :pull:`1281` - ``reactpy.types.VdomDictConstructor`` has been renamed to ``reactpy.types.VdomConstructor``. - :pull:`1312` - ``REACTPY_ASYNC_RENDERING`` can now de-duplicate and cascade renders where necessary. - :pull:`1312` - ``REACTPY_ASYNC_RENDERING`` is now defaulted to ``True`` for up to 40x performance improvements in environments with high concurrency. **Deprecated** -:pull:`1307` - ``reactpy.web.module_from_file`` is deprecated. Use ``reactpy.reactjs.component_from_file`` instead. -:pull:`1307` - ``reactpy.web.module_from_url`` is deprecated. Use ``reactpy.reactjs.component_from_url`` instead. -:pull:`1307` - ``reactpy.web.module_from_string`` is deprecated. Use ``reactpy.reactjs.component_from_string`` instead. -:pull:`1307` - ``reactpy.web.export`` is deprecated. Use ``reactpy.reactjs.component_from_*`` instead. -:pull:`1314` - ``reactpy.web.*`` is deprecated. Use ``reactpy.reactjs.*`` instead. **Removed** - :pull:`1113` - Removed support for Python 3.9 and 3.10. - :pull:`1255` - Removed the ability to import ``reactpy.html.*`` elements directly. You must now call ``html.*`` to access the elements. - :pull:`1113` - Removed backend specific installation extras (such as ``pip install reactpy[starlette]``). - :pull:`1264` - Removed support for async functions within ``reactpy.use_effect`` hook. Use ``reactpy.use_async_effect`` instead. - :pull:`1113` - Removed deprecated function ``module_from_template``. - :pull:`1311` - Removed deprecated exception type ``reactpy.core.serve.Stop``. - :pull:`1311` - Removed deprecated component ``reactpy.widgets.hotswap``. - :pull:`1255` - Removed ``reactpy.sample`` module. - :pull:`1255` - Removed ``reactpy.svg`` module. Contents previously within ``reactpy.svg.*`` can now be accessed via ``reactpy.html.svg.*``. - :pull:`1255` - Removed ``reactpy.html._`` function. Use ``reactpy.html(...)`` or ``reactpy.html.fragment(...)`` instead. - :pull:`1113` - Removed ``reactpy.run``. See the documentation for the new method to run ReactPy applications. - :pull:`1113` - Removed ``reactpy.backend.*``. See the documentation for the new method to run ReactPy applications. - :pull:`1113` - Removed ``reactpy.core.types`` module. Use ``reactpy.types`` instead. - :pull:`1278` - Removed ``reactpy.utils.html_to_vdom``. Use ``reactpy.utils.string_to_reactpy`` instead. - :pull:`1278` - Removed ``reactpy.utils.vdom_to_html``. Use ``reactpy.utils.reactpy_to_string`` instead. - :pull:`1281` - Removed ``reactpy.vdom``. Use ``reactpy.Vdom`` instead. - :pull:`1281` - Removed ``reactpy.core.make_vdom_constructor``. Use ``reactpy.Vdom`` instead. - :pull:`1281` - Removed ``reactpy.core.custom_vdom_constructor``. Use ``reactpy.Vdom`` instead. - :pull:`1311` - Removed ``reactpy.Layout`` top-level re-export. Use ``reactpy.core.layout.Layout`` instead. - :pull:`1312` - Removed ``reactpy.types.LayoutType``. Use ``reactpy.types.BaseLayout`` instead. - :pull:`1312` - Removed ``reactpy.types.ContextProviderType``. Use ``reactpy.types.ContextProvider`` instead. - :pull:`1312` - Removed ``reactpy.core.hooks._ContextProvider``. Use ``reactpy.types.ContextProvider`` instead. - :pull:`1314` - Removed ``reactpy.web.utils``. Use ``reactpy.reactjs.utils`` instead. **Fixed** - :pull:`1239` - Fixed a bug where script elements would not render to the DOM as plain text. - :pull:`1271` - Fixed a bug where the ``key`` property provided within server-side ReactPy code was failing to propagate to the front-end JavaScript components. - :pull:`1254` - Fixed a bug where ``RuntimeError("Hook stack is in an invalid state")`` errors could be generated when using a webserver that reuses threads. - :pull:`1314` - Allow for ReactPy and ReactJS components to be arbitrarily inserted onto the page with any possible hierarchy. v1.1.0 ------ :octicon:`milestone` *released on 2024-11-24* **Fixed** - :pull:`1118` - ``module_from_template`` is broken with a recent release of ``requests`` - :pull:`1131` - ``module_from_template`` did not work when using Flask backend - :pull:`1200` - Fixed ``UnicodeDecodeError`` when using ``reactpy.web.export`` - :pull:`1224` - Fixed needless unmounting of JavaScript components during each ReactPy render. - :pull:`1126` - Fixed missing ``event["target"]["checked"]`` on checkbox inputs - :pull:`1191` - Fixed missing static files on `sdist` Python distribution **Added** - :pull:`1165` - Allow concurrently rendering discrete component trees - enable this experimental feature by setting ``REACTPY_ASYNC_RENDERING=true``. This improves the overall responsiveness of your app in situations where larger renders would otherwise block smaller renders from executing. **Changed** - :pull:`1171` - Previously ``None``, when present in an HTML element, would render as the string ``"None"``. Now ``None`` will not render at all. This is now equivalent to how ``None`` is handled when returned from components. - :pull:`1210` - Move hooks from ``reactpy.backend.hooks`` into ``reactpy.core.hooks``. **Deprecated** - :pull:`1171` - The ``Stop`` exception. Recent releases of ``anyio`` have made this exception difficult to use since it now raises an ``ExceptionGroup``. This exception was primarily used for internal testing purposes and so is now deprecated. - :pull:`1210` - Deprecate ``reactpy.backend.hooks`` since the hooks have been moved into ``reactpy.core.hooks``. v1.0.2 ------ :octicon:`milestone` *released on 2023-07-03* **Fixed** - :issue:`1086` - fix rendering bug when children change positions (via :pull:`1085`) v1.0.1 ------ :octicon:`milestone` *released on 2023-06-16* **Changed** - :pull:`1050` - Warn and attempt to fix missing mime types, which can result in ``reactpy.run`` not working as expected. - :pull:`1051` - Rename ``reactpy.backend.BackendImplementation`` to ``reactpy.backend.BackendType`` - :pull:`1051` - Allow ``reactpy.run`` to fail in more predictable ways **Fixed** - :issue:`930` - better traceback for JSON serialization errors (via :pull:`1008`) - :issue:`437` - explain that JS component attributes must be JSON (via :pull:`1008`) - :pull:`1051` - Fix ``reactpy.run`` port assignment sometimes attaching to in-use ports on Windows - :pull:`1051` - Fix ``reactpy.run`` not recognizing ``fastapi`` v1.0.0 ------ :octicon:`milestone` *released on 2023-03-14* No changes. v1.0.0-a6 --------- :octicon:`milestone` *released on 2023-02-23* **Fixed** - :pull:`936` - remaining issues from :pull:`934` v1.0.0-a5 --------- :octicon:`milestone` *released on 2023-02-21* **Fixed** - :pull:`934` - minor issues with camelCase rewrite CLI utility v1.0.0-a4 --------- :octicon:`milestone` *released on 2023-02-21* **Changed** - :pull:`919` - Reverts :pull:`841` as per the conclusion in :discussion:`916`. but preserves the ability to declare attributes with snake_case. **Deprecated** - :pull:`919` - Declaration of keys via keyword arguments in standard elements. A script has been added to automatically convert old usages where possible. v1.0.0-a3 --------- :octicon:`milestone` *released on 2023-02-02* **Fixed** - :pull:`908` - minor type hint issue with ``VdomDictConstructor`` **Removed** - :pull:`907` - accidental import of reactpy.testing v1.0.0-a2 --------- :octicon:`milestone` *released on 2023-01-31* **Reverted** - :pull:`901` - reverts :pull:`886` due to :issue:`896` **Fixed** - :issue:`896` - Stale event handlers after disconnect/reconnect cycle - :issue:`898` - Fixed CLI not registered as entry point v1.0.0-a1 --------- :octicon:`milestone` *released on 2023-01-28* **Changed** - :pull:`841` - Revamped element constructor interface. Now instead of passing a dictionary of attributes to element constructors, attributes are declared using keyword arguments. For example, instead of writing: .. code-block:: html.div({"className": "some-class"}, "some", "text") You now should write: .. code-block:: html.div("some", "text", class_name="some-class") .. note:: All attributes are written using ``snake_case``. In conjunction, with these changes, ReactPy now supplies a command line utility that makes a "best effort" attempt to automatically convert code to the new API. Usage of this utility is as follows: .. code-block:: bash reactpy update-html-usages [PATHS] Where ``[PATHS]`` is any number of directories or files that should be rewritten. .. warning:: After running this utility, code comments and formatting may have been altered. It's recommended that you run a code formatting tool like `Black `__ and manually review and replace any comments that may have been moved. **Fixed** - :issue:`755` - unification of component and VDOM constructor interfaces. See above. v0.44.0 ------- :octicon:`milestone` *released on 2023-01-27* **Deprecated** - :pull:`876` - ``reactpy.widgets.hotswap``. The function has no clear uses outside of some internal applications. For this reason it has been deprecated. **Removed** - :pull:`886` - Ability to access element value from events via `event['value']` key. Instead element value should be accessed via `event['target']['value']`. Originally deprecated in :ref:`v0.34.0`. - :pull:`886` - old misspelled option ``reactpy.config.REACTPY_WED_MODULES_DIR``. Originally deprecated in :ref:`v0.36.1`. v0.43.0 ------- :octicon:`milestone` *released on 2023-01-09* **Deprecated** - :pull:`870` - ``ComponentType.()``. This method was implemented based on reading the React/Preact source code. As it turns out though it seems like it's mostly a vestige from the fact that both these libraries still support class-based components. The ability for components to not render also caused several bugs. **Fixed** - :issue:`846` - Nested context does no update value if outer context should not render. - :issue:`847` - Detached model state on render of context consumer if unmounted and context value does not change. v0.42.0 ------- :octicon:`milestone` *released on 2022-12-02* **Added** - :pull:`835` - Ability to customize the ```` element of ReactPy's built-in client. - :pull:`835` - ``vdom_to_html`` utility function. - :pull:`843` - Ability to subscribe to changes that are made to mutable options. - :pull:`832` - ``del_html_head_body_transform`` to remove ````, ````, and ```` while preserving children. - :pull:`699` - Support for form element serialization **Fixed** - :issue:`582` - ``REACTPY_DEBUG_MODE`` is now mutable and can be changed at runtime - :pull:`832` - Fix ``html_to_vdom`` improperly removing ````, ````, and ```` nodes. **Removed** - :pull:`832` - Removed ``reactpy.html.body`` as it is currently unusable due to technological limitations, and thus not needed. - :pull:`840` - remove ``REACTPY_FEATURE_INDEX_AS_DEFAULT_KEY`` option - :pull:`835` - ``serve_static_files`` option from backend configuration **Deprecated** - :commit:`8f3785b` - Deprecated ``module_from_template`` v0.41.0 ------- :octicon:`milestone` *released on 2022-11-01* **Changed** - :pull:`823` - The hooks ``use_location`` and ``use_scope`` are no longer implementation specific and are now available as top-level imports. Instead of each backend defining these hooks, backends establish a ``ConnectionContext`` with this information. - :pull:`824` - ReactPy's built-in backend server now expose the following routes: - ``/_reactpy/assets/`` - ``/_reactpy/stream/`` - ``/_reactpy/modules/`` - ``//`` This should allow the browser to cache static resources. Even if your ``url_prefix`` is ``/_reactpy``, your app should still work as expected. Though if you're using ``reactpy-router``, ReactPy's server routes will always take priority. - :pull:`824` - Backend implementations now strip any URL prefix in the pathname for ``use_location``. - :pull:`827` - ``use_state`` now returns a named tuple with ``value`` and ``set_value`` fields. This is convenient for adding type annotations if the initial state value is not the same as the values you might pass to the state setter. Where previously you might have to do something like: .. code-block:: value: int | None = None value, set_value = use_state(value) Now you can annotate your state using the ``State`` class: .. code-block:: state: State[int | None] = use_state(None) # access value and setter state.value state.set_value # can still destructure if you need to value, set_value = state **Added** - :pull:`823` - There is a new ``use_connection`` hook which returns a ``Connection`` object. This ``Connection`` object contains a ``location`` and ``scope``, along with a ``carrier`` which is unique to each backend implementation. v0.40.2 ------- :octicon:`milestone` *released on 2022-09-13* **Changed** - :pull:`809` - Avoid the use of JSON patch for diffing models. v0.40.1 ------- :octicon:`milestone` *released on 2022-09-11* **Fixed** - :issue:`806` - Child models after a component fail to render v0.40.0 (yanked) ---------------- :octicon:`milestone` *released on 2022-08-13* **Fixed** - :issue:`777` - Fix edge cases where ``html_to_vdom`` can fail to convert HTML - :issue:`789` - Conditionally rendered components cannot use contexts - :issue:`773` - Use strict equality check for text, numeric, and binary types in hooks - :issue:`801` - Accidental mutation of old model causes invalid JSON Patch **Changed** - :pull:`123` - set default timeout on playwright page for testing - :pull:`787` - Track contexts in hooks as state - :pull:`787` - remove non-standard ``name`` argument from ``create_context`` **Added** - :pull:`123` - ``asgiref`` as a dependency - :pull:`795` - ``lxml`` as a dependency v0.39.0 ------- :octicon:`milestone` *released on 2022-06-20* **Fixed** - :pull:`763` - ``No module named 'reactpy.server'`` from ``reactpy.run`` - :pull:`749` - Setting appropriate MIME type for web modules in `sanic` server implementation **Changed** - :pull:`763` - renamed various: - ``reactpy.testing.server -> reactpy.testing.backend`` - ``ServerFixture -> BackendFixture`` - ``DisplayFixture.server -> DisplayFixture.backend`` - :pull:`765` - ``exports_default`` parameter is removed from ``module_from_template``. **Added** - :pull:`765` - ability to specify versions with module templates (e.g. ``module_from_template("react@^17.0.0", ...)``). v0.38.1 ------- :octicon:`milestone` *released on 2022-04-15* **Fixed** - `reactive-python/reactpy-jupyter#22 `__ - a missing file extension was causing a problem with WebPack. v0.38.0 ------- :octicon:`milestone` *released on 2022-04-15* No changes. v0.38.0-a4 ---------- :octicon:`milestone` *released on 2022-04-15* **Added** - :pull:`733` - ``use_debug_value`` hook **Changed** - :pull:`733` - renamed ``assert_reactpy_logged`` testing util to ``assert_reactpy_did_log`` v0.38.0-a3 ---------- :octicon:`milestone` *released on 2022-04-15* **Changed** - :pull:`730` - Layout context management is not async v0.38.0-a2 ---------- :octicon:`milestone` *released on 2022-04-14* **Added** - :pull:`721` - Implement ``use_location()`` hook. Navigating to any route below the root of the application will be reflected in the ``location.pathname``. This operates in concert with how ReactPy's configured routes have changed. This will ultimately work towards resolving :issue:`569`. **Changed** - :pull:`721` - The routes ReactPy configures on apps have changed .. code-block:: text prefix/_api/modules/* web modules prefix/_api/stream websocket endpoint prefix/* client react app This means that ReactPy's client app is available at any route below the configured ``url_prefix`` besides ``prefix/_api``. The ``_api`` route will likely remain a route which is reserved by ReactPy. The route navigated to below the ``prefix`` will be shown in ``use_location``. - :pull:`721` - ReactPy's client now uses Preact instead of React - :pull:`726` - Renamed ``reactpy.server`` to ``reactpy.backend``. Other references to "server implementations" have been renamed to "backend implementations" throughout the documentation and code. **Removed** - :pull:`721` - ``redirect_root`` server option v0.38.0-a1 ---------- :octicon:`milestone` *released on 2022-03-27* **Changed** - :pull:`703` - How ReactPy integrates with servers. ``reactpy.run`` no longer accepts an app instance to discourage use outside of testing. ReactPy's server implementations now provide ``configure()`` functions instead. ``reactpy.testing`` has been completely reworked in order to support async web drivers - :pull:`703` - ``PerClientStateServer`` has been functionally replaced by ``configure`` **Added** - :issue:`669` - Access to underlying server requests via contexts **Removed** - :issue:`669` - Removed ``reactpy.widgets.multiview`` since basic routing view ``use_scope`` is now possible as well as all ``SharedClientStateServer`` implementations. **Fixed** - :issue:`591` - ReactPy's test suite no longer uses sync web drivers - :issue:`678` - Updated Sanic requirement to ``>=21`` - :issue:`657` - How we advertise ``reactpy.run`` v0.37.2 ------- :octicon:`milestone` *released on 2022-03-27* **Changed** - :pull:`701` - The name of ``proto`` modules to ``types`` and added a top level ``reactpy.types`` module **Fixed** - :pull:`716` - A typo caused ReactPy to use the insecure ``ws`` web-socket protocol on pages loaded with ``https`` instead of the secure ``wss`` protocol v0.37.1 ------- :octicon:`milestone` *released on 2022-03-05* No changes. v0.37.1-a2 ---------- :octicon:`milestone` *released on 2022-03-02* **Fixed:** - :issue:`684` - Revert :pull:`694` and by making ``value`` uncontrolled client-side v0.37.1-a1 ---------- :octicon:`milestone` *released on 2022-02-28* **Fixed:** - :issue:`684` - ``onChange`` event for inputs missing key strokes v0.37.0 ------- :octicon:`milestone` *released on 2022-02-27* **Added:** - :issue:`682` - Support for keys in HTML fragments - :pull:`585` - Use Context Hook **Fixed:** - :issue:`690` - React warning about set state in unmounted component - :pull:`688` - Missing reset of schedule_render_later flag ---- Releases below do not use the "Keep a Changelog" style guidelines. ---- v0.36.3 ------- :octicon:`milestone` *released on 2022-02-18* Misc bug fixes along with a minor improvement that allows components to return ``None`` to render nothing. **Closed Issues** - All child states wiped upon any child key change - :issue:`652` - Allow NoneType returns within components - :issue:`538` **Merged Pull Requests** - fix #652 - :pull:`672` - Fix 663 - :pull:`667` v0.36.2 ------- :octicon:`milestone` *released on 2022-02-02* Hot fix for newly introduced ``DeprecatedOption``: - :commit:`c146dfb264cbc3d2256a62efdfe9ccf62c795b01` v0.36.1 ------- :octicon:`milestone` *released on 2022-02-02* Includes bug fixes and renames the configuration option ``REACTPY_WED_MODULES_DIR`` to ``REACTPY_WEB_MODULES_DIR`` with a corresponding deprecation warning. **Closed Issues** - Fix Key Error When Cleaning Up Event Handlers - :issue:`640` - Update Script Tag Behavior - :issue:`628` **Merged Pull Requests** - mark old state as None if unmounting - :pull:`641` - rename REACTPY_WED_MODULES_DIR to REACTPY_WEB_MODULES_DIR - :pull:`638` v0.36.0 ------- :octicon:`milestone` *released on 2022-01-30* This release includes an important fix for errors produced after :pull:`623` was merged. In addition there is not a new ``http.script`` element which can behave similarly to a standard HTML `` ================================================ FILE: docs/source/guides/getting-started/_static/embed-reactpy-view/index.html ================================================ Example App

This is an Example App

Just below is an embedded ReactPy view...

================================================ FILE: docs/source/guides/getting-started/_static/embed-reactpy-view/main.py ================================================ 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) ================================================ FILE: docs/source/guides/getting-started/index.rst ================================================ 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. ================================================ FILE: docs/source/guides/getting-started/installing-reactpy.rst ================================================ 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. ================================================ FILE: docs/source/guides/getting-started/running-reactpy.rst ================================================ 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 ================================================ FILE: docs/source/guides/managing-state/combining-contexts-and-reducers/index.rst ================================================ Combining Contexts and Reducers 🚧 ================================== .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/managing-state/deeply-sharing-state-with-contexts/index.rst ================================================ Deeply Sharing State with Contexts 🚧 ===================================== .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/managing-state/how-to-structure-state/index.rst ================================================ .. _Structuring Your State: How to Structure State 🚧 ========================= .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/managing-state/index.rst ================================================ 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 🚧 ================================================ FILE: docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/data.json ================================================ [ { "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" } ] ================================================ FILE: docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py ================================================ 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) ================================================ FILE: docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py ================================================ 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) ================================================ FILE: docs/source/guides/managing-state/sharing-component-state/index.rst ================================================ 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. ================================================ FILE: docs/source/guides/managing-state/simplifying-updates-with-reducers/index.rst ================================================ Simplifying Updates with Reducers 🚧 ==================================== .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/managing-state/when-and-how-to-reset-state/index.rst ================================================ .. _When to Reset State: When and How to Reset State 🚧 ============================== .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/understanding-reactpy/index.rst ================================================ 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 🚧 ================================================ FILE: docs/source/guides/understanding-reactpy/layout-render-servers.rst ================================================ .. _Layout Render Servers: Layout Render Servers 🚧 ======================== .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/understanding-reactpy/representing-html.rst ================================================ .. _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. ================================================ FILE: docs/source/guides/understanding-reactpy/the-rendering-pipeline.rst ================================================ .. _The Rendering Pipeline: The Rendering Pipeline 🚧 ========================= .. talk about layouts and dispatchers .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/understanding-reactpy/the-rendering-process.rst ================================================ .. _The Rendering Process: The Rendering Process 🚧 ======================== .. refer to https://beta.reactjs.org/learn/render-and-commit .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/understanding-reactpy/what-are-components.rst ================================================ .. _What Are Components: What Are Components? 🚧 ======================= .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/understanding-reactpy/why-reactpy-needs-keys.rst ================================================ .. _Why ReactPy Needs Keys: Why ReactPy Needs Keys 🚧 ========================= .. note:: Under construction 🚧 ================================================ FILE: docs/source/guides/understanding-reactpy/writing-tests.rst ================================================ .. _Writing Tests: Writing Tests 🚧 ================ .. note:: Under construction 🚧 ================================================ FILE: docs/source/index.rst ================================================ .. 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 🚧 ================================================ FILE: docs/source/reference/_examples/character_movement/main.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/click_count.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/material_ui_switch.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/matplotlib_plot.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/network_graph.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/pigeon_maps.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/simple_dashboard.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/slideshow.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/snake_game.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/todo.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/use_reducer_counter.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/use_state_counter.py ================================================ 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) ================================================ FILE: docs/source/reference/_examples/victory_chart.py ================================================ 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}))) ================================================ FILE: docs/source/reference/_static/vdom-json-schema.json ================================================ { "$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" } } } ================================================ FILE: docs/source/reference/browser-events.rst ================================================ .. _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 ------------ ================================================ FILE: docs/source/reference/hooks-api.rst ================================================ ========= 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 ================================================ FILE: docs/source/reference/html-attributes.rst ================================================ .. 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