├── .editorconfig
├── .github
├── CODEOWNERS
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── issue-form.yml
├── pull_request_template.md
└── workflows
│ ├── .hatch-run.yml
│ ├── check.yml
│ ├── codeql-analysis.yml
│ └── publish.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── branding
├── ico
│ └── reactpy-logo.ico
├── png
│ ├── reactpy-logo-landscape-padded.png
│ ├── reactpy-logo-landscape.png
│ └── reactpy-logo-padded.png
└── svg
│ ├── reactpy-logo-landscape-padded.svg
│ ├── reactpy-logo-landscape.svg
│ ├── reactpy-logo-square-padded.svg
│ └── reactpy-logo-square.svg
├── docs
├── .gitignore
├── Dockerfile
├── README.md
├── docs_app
│ ├── __init__.py
│ ├── app.py
│ ├── dev.py
│ ├── examples.py
│ └── prod.py
├── main.py
├── poetry.lock
├── 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
│ └── install-and-run-reactpy.gif
│ ├── 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
│ │ │ ├── _static
│ │ │ ├── direct-state-change.png
│ │ │ └── reactpy-state-change.png
│ │ │ └── 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
│ │ │ │ └── screenshot.png
│ │ │ ├── logo-django.svg
│ │ │ ├── logo-jupyter.svg
│ │ │ ├── logo-plotly.svg
│ │ │ ├── reactpy-in-jupyterlab.gif
│ │ │ └── shared-client-state-server-slider.gif
│ │ ├── 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
│ │ ├── _static
│ │ ├── idom-flow-diagram.svg
│ │ ├── live-examples-in-docs.gif
│ │ ├── mvc-flow-diagram.svg
│ │ └── npm-download-trends.png
│ │ ├── 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
│ │ └── static
│ │ │ └── bunny.png
│ ├── 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
│ ├── clean_js_dir.py
│ └── copy_dir.py
├── js
│ ├── .gitignore
│ ├── README.md
│ ├── bun.lockb
│ ├── eslint.config.mjs
│ ├── package.json
│ ├── packages
│ │ ├── @reactpy
│ │ │ ├── app
│ │ │ │ ├── bun.lockb
│ │ │ │ ├── eslint.config.mjs
│ │ │ │ ├── package.json
│ │ │ │ ├── src
│ │ │ │ │ └── index.ts
│ │ │ │ └── tsconfig.json
│ │ │ └── client
│ │ │ │ ├── README.md
│ │ │ │ ├── bun.lockb
│ │ │ │ ├── package.json
│ │ │ │ ├── src
│ │ │ │ ├── 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
│ │ │ ├── events.ts
│ │ │ └── index.ts
│ │ │ ├── tests
│ │ │ ├── event-to-object.test.ts
│ │ │ └── tooling
│ │ │ │ ├── check.ts
│ │ │ │ ├── mock.ts
│ │ │ │ └── setup.js
│ │ │ └── tsconfig.json
│ └── 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
│ └── utils.py
│ ├── logging.py
│ ├── py.typed
│ ├── pyscript
│ ├── __init__.py
│ ├── component_template.py
│ ├── components.py
│ ├── layout_handler.py
│ └── utils.py
│ ├── static
│ └── pyscript-hide-debug.css
│ ├── templatetags
│ ├── __init__.py
│ └── jinja.py
│ ├── testing
│ ├── __init__.py
│ ├── backend.py
│ ├── common.py
│ ├── display.py
│ ├── logs.py
│ └── utils.py
│ ├── transforms.py
│ ├── types.py
│ ├── utils.py
│ ├── web
│ ├── __init__.py
│ ├── module.py
│ ├── templates
│ │ └── react.js
│ └── 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_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_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
│ ├── keys-properly-propagated.js
│ ├── set-flag-when-unmount-is-called.js
│ ├── simple-button.js
│ └── subcomponent-notation.js
├── test_module.py
└── test_utils.py
├── test_widgets.py
└── tooling
├── __init__.py
├── aio.py
├── common.py
├── hooks.py
├── layout.py
└── select.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 | charset = utf-8
11 | end_of_line = lf
12 |
13 | [*.py]
14 | indent_size = 4
15 | max_line_length = 120
16 |
17 | [*.md]
18 | indent_size = 4
19 |
20 | [*.yml]
21 | indent_size = 4
22 |
23 | [*.html]
24 | max_line_length = off
25 |
26 | [*.js]
27 | max_line_length = off
28 |
29 | [*.css]
30 | indent_size = 4
31 | max_line_length = off
32 |
33 | # Tests can violate line width restrictions in the interest of clarity.
34 | [**/test_*.py]
35 | max_line_length = off
36 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | @maintainers
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [archmonger, rmorshea]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Start a Discussion
4 | url: https://github.com/reactive-python/reactpy/discussions
5 | about: Report issues, request features, ask questions, and share ideas
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue-form.yml:
--------------------------------------------------------------------------------
1 | name: Plan a Task
2 | description: Create a detailed plan of action (ONLY START AFTER DISCUSSION PLEASE 🙏).
3 | labels: ["flag-triage"]
4 | body:
5 | - type: textarea
6 | attributes:
7 | label: Current Situation
8 | description: Discuss how things currently are, why they require action, and any relevant prior discussion/context.
9 | validations:
10 | required: false
11 | - type: textarea
12 | attributes:
13 | label: Proposed Actions
14 | description: Describe what ought to be done, and why that will address the reasons for action mentioned above.
15 | validations:
16 | required: false
17 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 |
5 | ## Checklist
6 |
7 | Please update this checklist as you complete each item:
8 |
9 | - [ ] Tests have been developed for bug fixes or new functionality.
10 | - [ ] The changelog has been updated, if necessary.
11 | - [ ] Documentation has been updated, if necessary.
12 | - [ ] GitHub Issues closed by this PR have been linked.
13 |
14 | By submitting this pull request I agree that all contributions comply with this project's open source license(s).
15 |
--------------------------------------------------------------------------------
/.github/workflows/.hatch-run.yml:
--------------------------------------------------------------------------------
1 | name: hatch-run
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | job-name:
7 | required: true
8 | type: string
9 | run-cmd:
10 | required: true
11 | type: string
12 | runs-on:
13 | required: false
14 | type: string
15 | default: '["ubuntu-latest"]'
16 | python-version:
17 | required: false
18 | type: string
19 | default: '["3.x"]'
20 | secrets:
21 | node-auth-token:
22 | required: false
23 | pypi-username:
24 | required: false
25 | pypi-password:
26 | required: false
27 |
28 | jobs:
29 | hatch:
30 | name: ${{ format(inputs.job-name, matrix.python-version, matrix.runs-on) }}
31 | strategy:
32 | matrix:
33 | python-version: ${{ fromJson(inputs.python-version) }}
34 | runs-on: ${{ fromJson(inputs.runs-on) }}
35 | runs-on: ${{ matrix.runs-on }}
36 | steps:
37 | - uses: actions/checkout@v4
38 | - uses: oven-sh/setup-bun@v2
39 | with:
40 | bun-version: latest
41 | - name: Use Python ${{ matrix.python-version }}
42 | uses: actions/setup-python@v5
43 | with:
44 | python-version: ${{ matrix.python-version }}
45 | - name: Install Python Dependencies
46 | run: pip install --upgrade hatch uv
47 | - name: Run Scripts
48 | env:
49 | NPM_CONFIG_TOKEN: ${{ secrets.node-auth-token }}
50 | HATCH_INDEX_USER: ${{ secrets.pypi-username }}
51 | HATCH_INDEX_AUTH: ${{ secrets.pypi-password }}
52 | run: ${{ inputs.run-cmd }}
53 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: check
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - "*"
10 | schedule:
11 | - cron: "0 0 * * 0"
12 |
13 | jobs:
14 | test-python-coverage:
15 | uses: ./.github/workflows/.hatch-run.yml
16 | with:
17 | job-name: "python-{0}"
18 | run-cmd: "hatch test --cover"
19 | lint-python:
20 | uses: ./.github/workflows/.hatch-run.yml
21 | with:
22 | job-name: "python-{0}"
23 | run-cmd: "hatch fmt src/reactpy --check && hatch run python:type_check"
24 | test-python:
25 | uses: ./.github/workflows/.hatch-run.yml
26 | with:
27 | job-name: "python-{0} {1}"
28 | run-cmd: "hatch test"
29 | runs-on: '["ubuntu-latest", "macos-latest", "windows-latest"]'
30 | python-version: '["3.10", "3.11", "3.12", "3.13"]'
31 | test-documentation:
32 | # Temporarily disabled while we transition from Sphinx to MkDocs
33 | # https://github.com/reactive-python/reactpy/pull/1052
34 | if: 0
35 | uses: ./.github/workflows/.hatch-run.yml
36 | with:
37 | job-name: "python-{0}"
38 | run-cmd: "hatch run docs:check"
39 | python-version: '["3.11"]'
40 | test-javascript:
41 | # Temporarily disabled while we rewrite the "event_to_object" package
42 | # https://github.com/reactive-python/reactpy/issues/1196
43 | if: 0
44 | uses: ./.github/workflows/.hatch-run.yml
45 | with:
46 | job-name: "{1}"
47 | run-cmd: "hatch run javascript:test"
48 | lint-javascript:
49 | uses: ./.github/workflows/.hatch-run.yml
50 | with:
51 | job-name: "{1}"
52 | run-cmd: "hatch run javascript:check"
53 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: codeql
13 |
14 | on:
15 | push:
16 | branches: [main]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [main]
20 | schedule:
21 | - cron: "43 3 * * 3"
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: ["javascript", "python"]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # 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
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | publish-reactpy:
9 | if: startsWith(github.event.release.name, 'reactpy ')
10 | uses: ./.github/workflows/.hatch-run.yml
11 | with:
12 | job-name: "Publish to PyPI"
13 | run-cmd: "hatch build --clean && hatch publish --yes"
14 | secrets:
15 | pypi-username: ${{ secrets.PYPI_USERNAME }}
16 | pypi-password: ${{ secrets.PYPI_PASSWORD }}
17 |
18 | publish-reactpy-client:
19 | if: startsWith(github.event.release.name, '@reactpy/client ')
20 | uses: ./.github/workflows/.hatch-run.yml
21 | with:
22 | job-name: "Publish to NPM"
23 | run-cmd: "hatch run javascript:publish_reactpy_client"
24 | secrets:
25 | node-auth-token: ${{ secrets.NODE_AUTH_TOKEN }}
26 |
27 | publish-event-to-object:
28 | if: startsWith(github.event.release.name, 'event-to-object ')
29 | uses: ./.github/workflows/.hatch-run.yml
30 | with:
31 | job-name: "Publish to NPM"
32 | run-cmd: "hatch run javascript:publish_event_to_object"
33 | secrets:
34 | node-auth-token: ${{ secrets.NODE_AUTH_TOKEN }}
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # --- Build Artifacts ---
2 | src/reactpy/static/index.js*
3 | src/reactpy/static/morphdom/
4 | src/reactpy/static/pyscript/
5 |
6 | # --- Jupyter ---
7 | *.ipynb_checkpoints
8 | *Untitled*.ipynb
9 |
10 | # --- Jupyter Repo 2 Docker ---
11 | .local
12 | .ipython
13 | .cache
14 | .bash_history
15 | .python_history
16 | .jupyter
17 |
18 | # --- Python ---
19 | .hatch
20 | .venv*
21 | venv*
22 | MANIFEST
23 | build
24 | dist
25 | .eggs
26 | *.egg-info
27 | __pycache__/
28 | *.py[cod]
29 | .tox
30 | .nox
31 | pip-wheel-metadata
32 |
33 | # --- PyEnv ---
34 | .python-version
35 |
36 | # -- Python Tests ---
37 | .coverage.*
38 | *.coverage
39 | *.pytest_cache
40 | *.mypy_cache
41 |
42 | # --- IDE ---
43 | .idea
44 | .vscode
45 |
46 | # --- JS ---
47 | node_modules
48 |
49 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: local
3 | hooks:
4 | - id: lint-py-fix
5 | name: Fix Python Lint
6 | entry: hatch run lint-py
7 | language: system
8 | args: [--fix]
9 | pass_filenames: false
10 | files: \.py$
11 | - repo: local
12 | hooks:
13 | - id: lint-js-fix
14 | name: Fix JS Lint
15 | entry: hatch run lint-js --fix
16 | language: system
17 | pass_filenames: false
18 | files: \.(js|jsx|ts|tsx)$
19 | - repo: local
20 | hooks:
21 | - id: lint-py-check
22 | name: Check Python Lint
23 | entry: hatch run lint-py
24 | language: system
25 | pass_filenames: false
26 | files: \.py$
27 | - repo: local
28 | hooks:
29 | - id: lint-js-check
30 | name: Check JS Lint
31 | entry: hatch run lint-py
32 | language: system
33 | pass_filenames: false
34 | files: \.(js|jsx|ts|tsx)$
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Reactive Python and affiliates.
4 |
5 | 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:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | 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.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReactPy
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | [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.
22 |
23 |
47 |
48 | # At a Glance
49 |
50 | To get a rough idea of how to write apps in ReactPy, take a look at this tiny _Hello World_ application.
51 |
52 | ```python
53 | from reactpy import component, html, run
54 |
55 | @component
56 | def hello_world():
57 | return html.h1("Hello, World!")
58 |
59 | run(hello_world)
60 | ```
61 |
62 | # Resources
63 |
64 | Follow the links below to find out more about this project.
65 |
66 | - [Try ReactPy (Jupyter Notebook)](https://mybinder.org/v2/gh/reactive-python/reactpy-jupyter/main?urlpath=lab/tree/notebooks/introduction.ipynb)
67 | - [Documentation](https://reactpy.dev/)
68 | - [GitHub Discussions](https://github.com/reactive-python/reactpy/discussions)
69 | - [Discord](https://discord.gg/uNb5P4hA9X)
70 | - [Contributor Guide](https://reactpy.dev/docs/about/contributor-guide.html)
71 | - [Code of Conduct](https://github.com/reactive-python/reactpy/blob/main/CODE_OF_CONDUCT.md)
72 |
--------------------------------------------------------------------------------
/branding/ico/reactpy-logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/branding/ico/reactpy-logo.ico
--------------------------------------------------------------------------------
/branding/png/reactpy-logo-landscape-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/branding/png/reactpy-logo-landscape-padded.png
--------------------------------------------------------------------------------
/branding/png/reactpy-logo-landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/branding/png/reactpy-logo-landscape.png
--------------------------------------------------------------------------------
/branding/png/reactpy-logo-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/branding/png/reactpy-logo-padded.png
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | source/_auto
3 | source/_static/custom.js
4 | source/vdom-json-schema.json
5 |
--------------------------------------------------------------------------------
/docs/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11
2 | WORKDIR /app/
3 |
4 | RUN apt-get update
5 |
6 | # Create/Activate Python Venv
7 | # ---------------------------
8 | ENV VIRTUAL_ENV=/opt/venv
9 | RUN python3 -m venv $VIRTUAL_ENV
10 | ENV PATH="$VIRTUAL_ENV/bin:$PATH"
11 |
12 | # Install Python Build Dependencies
13 | # ---------------------------------
14 | RUN pip install --upgrade pip poetry hatch uv
15 | RUN curl -fsSL https://bun.sh/install | bash
16 | ENV PATH="/root/.bun/bin:$PATH"
17 |
18 | # Copy Files
19 | # ----------
20 | COPY LICENSE ./
21 | COPY README.md ./
22 | COPY pyproject.toml ./
23 | COPY src ./src
24 | COPY docs ./docs
25 | COPY branding ./branding
26 |
27 | # Install and Build Docs
28 | # ----------------------
29 | WORKDIR /app/docs/
30 | RUN poetry install -v
31 | RUN sphinx-build -v -W -b html source build
32 |
33 | # Define Entrypoint
34 | # -----------------
35 | ENV PORT=5000
36 | ENV REACTPY_DEBUG=1
37 | ENV REACTPY_CHECK_VDOM_SPEC=0
38 | CMD ["python", "main.py"]
39 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # ReactPy's Documentation
2 |
3 | ...
4 |
--------------------------------------------------------------------------------
/docs/docs_app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/docs_app/__init__.py
--------------------------------------------------------------------------------
/docs/docs_app/app.py:
--------------------------------------------------------------------------------
1 | from logging import getLogger
2 | from pathlib import Path
3 |
4 | from sanic import Sanic, response
5 |
6 | from docs_app.examples import get_normalized_example_name, load_examples
7 | from reactpy import component
8 | from reactpy.backend.sanic import Options, configure, use_request
9 | from reactpy.types import ComponentConstructor
10 |
11 | THIS_DIR = Path(__file__).parent
12 | DOCS_DIR = THIS_DIR.parent
13 | DOCS_BUILD_DIR = DOCS_DIR / "build"
14 |
15 | REACTPY_MODEL_SERVER_URL_PREFIX = "/_reactpy"
16 |
17 | logger = getLogger(__name__)
18 |
19 |
20 | REACTPY_MODEL_SERVER_URL_PREFIX = "/_reactpy"
21 |
22 |
23 | @component
24 | def Example():
25 | raw_view_id = use_request().get_args().get("view_id")
26 | view_id = get_normalized_example_name(raw_view_id)
27 | return _get_examples()[view_id]()
28 |
29 |
30 | def _get_examples():
31 | if not _EXAMPLES:
32 | _EXAMPLES.update(load_examples())
33 | return _EXAMPLES
34 |
35 |
36 | def reload_examples():
37 | _EXAMPLES.clear()
38 | _EXAMPLES.update(load_examples())
39 |
40 |
41 | _EXAMPLES: dict[str, ComponentConstructor] = {}
42 |
43 |
44 | def make_app(name: str):
45 | app = Sanic(name)
46 |
47 | app.static("/docs", str(DOCS_BUILD_DIR))
48 |
49 | @app.route("/")
50 | async def forward_to_index(_):
51 | return response.redirect("/docs/index.html")
52 |
53 | configure(
54 | app,
55 | Example,
56 | Options(url_prefix=REACTPY_MODEL_SERVER_URL_PREFIX),
57 | )
58 |
59 | return app
60 |
--------------------------------------------------------------------------------
/docs/docs_app/dev.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | import threading
4 | import time
5 | import webbrowser
6 |
7 | from sphinx_autobuild.cli import (
8 | Server,
9 | _get_build_args,
10 | _get_ignore_handler,
11 | find_free_port,
12 | get_builder,
13 | get_parser,
14 | )
15 |
16 | from docs_app.app import make_app, reload_examples
17 | from reactpy.backend.sanic import serve_development_app
18 | from reactpy.testing import clear_reactpy_web_modules_dir
19 |
20 | # these environment variable are used in custom Sphinx extensions
21 | os.environ["REACTPY_DOC_EXAMPLE_SERVER_HOST"] = "127.0.0.1:5555"
22 | os.environ["REACTPY_DOC_STATIC_SERVER_HOST"] = ""
23 |
24 |
25 | def wrap_builder(old_builder):
26 | # This is the bit that we're injecting to get the example components to reload too
27 |
28 | app = make_app("docs_dev_app")
29 |
30 | thread_started = threading.Event()
31 |
32 | def run_in_thread():
33 | loop = asyncio.new_event_loop()
34 | asyncio.set_event_loop(loop)
35 |
36 | server_started = asyncio.Event()
37 |
38 | async def set_thread_event_when_started():
39 | await server_started.wait()
40 | thread_started.set()
41 |
42 | loop.run_until_complete(
43 | asyncio.gather(
44 | serve_development_app(app, "127.0.0.1", 5555, server_started),
45 | set_thread_event_when_started(),
46 | )
47 | )
48 |
49 | threading.Thread(target=run_in_thread, daemon=True).start()
50 |
51 | thread_started.wait()
52 |
53 | def new_builder():
54 | clear_reactpy_web_modules_dir()
55 | reload_examples()
56 | old_builder()
57 |
58 | return new_builder
59 |
60 |
61 | def main():
62 | # Mostly copied from https://github.com/executablebooks/sphinx-autobuild/blob/b54fb08afc5112bfcda1d844a700c5a20cd6ba5e/src/sphinx_autobuild/cli.py
63 | parser = get_parser()
64 | args = parser.parse_args()
65 |
66 | srcdir = os.path.realpath(args.sourcedir)
67 | outdir = os.path.realpath(args.outdir)
68 | if not os.path.exists(outdir):
69 | os.makedirs(outdir)
70 |
71 | server = Server()
72 |
73 | build_args, pre_build_commands = _get_build_args(args)
74 | builder = wrap_builder(
75 | get_builder(
76 | server.watcher,
77 | build_args,
78 | host=args.host,
79 | port=args.port,
80 | pre_build_commands=pre_build_commands,
81 | )
82 | )
83 |
84 | ignore_handler = _get_ignore_handler(args)
85 | server.watch(srcdir, builder, ignore=ignore_handler)
86 | for dirpath in args.additional_watched_dirs:
87 | real_dirpath = os.path.realpath(dirpath)
88 | server.watch(real_dirpath, builder, ignore=ignore_handler)
89 | server.watch(outdir, ignore=ignore_handler)
90 |
91 | if not args.no_initial_build:
92 | builder()
93 |
94 | # Find the free port
95 | portn = args.port or find_free_port()
96 | if args.openbrowser is True:
97 |
98 | def opener():
99 | time.sleep(args.delay)
100 | webbrowser.open(f"http://{args.host}:{args.port}/index.html")
101 |
102 | threading.Thread(target=opener, daemon=True).start()
103 |
104 | server.serve(port=portn, host=args.host, root=outdir)
105 |
--------------------------------------------------------------------------------
/docs/docs_app/prod.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from docs_app.app import make_app
4 |
5 | app = make_app("docs_prod_app")
6 |
7 |
8 | def main() -> None:
9 | app.run(
10 | host="0.0.0.0", # noqa: S104
11 | port=int(os.environ.get("PORT", 5000)),
12 | workers=int(os.environ.get("WEB_CONCURRENCY", 1)),
13 | debug=bool(int(os.environ.get("DEBUG", "0"))),
14 | )
15 |
--------------------------------------------------------------------------------
/docs/main.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from docs_app import dev, prod
4 |
5 | if __name__ == "__main__":
6 | if len(sys.argv) == 1:
7 | prod.main()
8 | else:
9 | dev.main()
10 |
--------------------------------------------------------------------------------
/docs/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "docs_app"
3 | version = "0.0.0"
4 | description = "docs"
5 | authors = ["rmorshea "]
6 | readme = "README.md"
7 |
8 | [tool.poetry.dependencies]
9 | python = "^3.9"
10 | furo = "2022.04.07"
11 | reactpy = { path = "..", extras = ["all"], develop = false }
12 | sphinx = "*"
13 | sphinx-autodoc-typehints = "*"
14 | sphinx-copybutton = "*"
15 | sphinx-autobuild = "*"
16 | sphinx-reredirects = "*"
17 | sphinx-design = "*"
18 | sphinx-resolve-py-references = "*"
19 | sphinxext-opengraph = "*"
20 |
21 | [build-system]
22 | requires = ["poetry-core"]
23 | build-backend = "poetry.core.masonry.api"
24 |
--------------------------------------------------------------------------------
/docs/source/_custom_js/README.md:
--------------------------------------------------------------------------------
1 | # Custom Javascript for ReactPy's Docs
2 |
3 | Build the javascript with
4 |
5 | ```
6 | bun run build
7 | ```
8 |
9 | This will drop a javascript bundle into `../_static/custom.js`
10 |
--------------------------------------------------------------------------------
/docs/source/_custom_js/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/_custom_js/bun.lockb
--------------------------------------------------------------------------------
/docs/source/_custom_js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactpy-docs-example-loader",
3 | "version": "1.0.0",
4 | "description": "simple javascript client for ReactPy's documentation",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "rollup --config",
8 | "format": "prettier --ignore-path .gitignore --write ."
9 | },
10 | "devDependencies": {
11 | "@rollup/plugin-commonjs": "^21.0.1",
12 | "@rollup/plugin-node-resolve": "^13.1.1",
13 | "@rollup/plugin-replace": "^3.0.0",
14 | "prettier": "^2.2.1",
15 | "rollup": "^2.35.1"
16 | },
17 | "dependencies": {
18 | "@reactpy/client": "file:../../../src/js/packages/@reactpy/client"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/docs/source/_custom_js/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from "@rollup/plugin-node-resolve";
2 | import commonjs from "@rollup/plugin-commonjs";
3 | import replace from "@rollup/plugin-replace";
4 |
5 | export default {
6 | input: "src/index.js",
7 | output: {
8 | file: "../_static/custom.js",
9 | format: "esm",
10 | },
11 | plugins: [
12 | resolve(),
13 | commonjs(),
14 | replace({
15 | "process.env.NODE_ENV": JSON.stringify("production"),
16 | preventAssignment: true,
17 | }),
18 | ],
19 | onwarn: function (warning) {
20 | if (warning.code === "THIS_IS_UNDEFINED") {
21 | // skip warning where `this` is undefined at the top level of a module
22 | return;
23 | }
24 | console.warn(warning.message);
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/docs/source/_custom_js/src/index.js:
--------------------------------------------------------------------------------
1 | import { SimpleReactPyClient, mount } from "@reactpy/client";
2 |
3 | let didMountDebug = false;
4 |
5 | export function mountWidgetExample(
6 | mountID,
7 | viewID,
8 | reactpyServerHost,
9 | useActivateButton,
10 | ) {
11 | let reactpyHost, reactpyPort;
12 | if (reactpyServerHost) {
13 | [reactpyHost, reactpyPort] = reactpyServerHost.split(":", 2);
14 | } else {
15 | reactpyHost = window.location.hostname;
16 | reactpyPort = window.location.port;
17 | }
18 |
19 | const client = new SimpleReactPyClient({
20 | serverLocation: {
21 | url: `${window.location.protocol}//${reactpyHost}:${reactpyPort}`,
22 | route: "/",
23 | query: `?view_id=${viewID}`,
24 | },
25 | });
26 |
27 | const mountEl = document.getElementById(mountID);
28 | let isMounted = false;
29 | triggerIfInViewport(mountEl, () => {
30 | if (!isMounted) {
31 | activateView(mountEl, client, useActivateButton);
32 | isMounted = true;
33 | }
34 | });
35 | }
36 |
37 | function activateView(mountEl, client, useActivateButton) {
38 | if (!useActivateButton) {
39 | mount(mountEl, client);
40 | return;
41 | }
42 |
43 | const enableWidgetButton = document.createElement("button");
44 | enableWidgetButton.appendChild(document.createTextNode("Activate"));
45 | enableWidgetButton.setAttribute("class", "enable-widget-button");
46 |
47 | enableWidgetButton.addEventListener("click", () =>
48 | fadeOutElementThenCallback(enableWidgetButton, () => {
49 | {
50 | mountEl.removeChild(enableWidgetButton);
51 | mountEl.setAttribute("class", "interactive widget-container");
52 | mountWithLayoutServer(mountEl, serverInfo);
53 | }
54 | }),
55 | );
56 |
57 | function fadeOutElementThenCallback(element, callback) {
58 | {
59 | var op = 1; // initial opacity
60 | var timer = setInterval(function () {
61 | {
62 | if (op < 0.001) {
63 | {
64 | clearInterval(timer);
65 | element.style.display = "none";
66 | callback();
67 | }
68 | }
69 | element.style.opacity = op;
70 | element.style.filter = "alpha(opacity=" + op * 100 + ")";
71 | op -= op * 0.5;
72 | }
73 | }, 50);
74 | }
75 | }
76 |
77 | mountEl.appendChild(enableWidgetButton);
78 | }
79 |
80 | function triggerIfInViewport(element, callback) {
81 | const observer = new window.IntersectionObserver(
82 | ([entry]) => {
83 | if (entry.isIntersecting) {
84 | callback();
85 | }
86 | },
87 | {
88 | root: null,
89 | threshold: 0.1, // set offset 0.1 means trigger if at least 10% of element in viewport
90 | },
91 | );
92 |
93 | observer.observe(element);
94 | }
95 |
--------------------------------------------------------------------------------
/docs/source/_exts/async_doctest.py:
--------------------------------------------------------------------------------
1 | from doctest import DocTest, DocTestRunner
2 | from textwrap import indent
3 | from typing import Any
4 |
5 | from sphinx.application import Sphinx
6 | from sphinx.ext.doctest import DocTestBuilder
7 | from sphinx.ext.doctest import setup as doctest_setup
8 |
9 | test_template = """
10 | import asyncio as __test_template_asyncio
11 |
12 | async def __test_template__main():
13 |
14 | {test}
15 |
16 | globals().update(locals())
17 |
18 | __test_template_asyncio.run(__test_template__main())
19 | """
20 |
21 |
22 | class TestRunnerWrapper:
23 | def __init__(self, runner: DocTestRunner):
24 | self._runner = runner
25 |
26 | def __getattr__(self, name: str) -> Any:
27 | return getattr(self._runner, name)
28 |
29 | def run(self, test: DocTest, *args: Any, **kwargs: Any) -> Any:
30 | for ex in test.examples:
31 | ex.source = test_template.format(test=indent(ex.source, " ").strip())
32 | return self._runner.run(test, *args, **kwargs)
33 |
34 |
35 | class AsyncDoctestBuilder(DocTestBuilder):
36 | @property
37 | def test_runner(self) -> DocTestRunner:
38 | return self._test_runner
39 |
40 | @test_runner.setter
41 | def test_runner(self, value: DocTestRunner) -> None:
42 | self._test_runner = TestRunnerWrapper(value)
43 |
44 |
45 | def setup(app: Sphinx) -> None:
46 | doctest_setup(app)
47 | app.add_builder(AsyncDoctestBuilder, override=True)
48 |
--------------------------------------------------------------------------------
/docs/source/_exts/build_custom_js.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | from pathlib import Path
3 |
4 | from sphinx.application import Sphinx
5 |
6 | SOURCE_DIR = Path(__file__).parent.parent
7 | CUSTOM_JS_DIR = SOURCE_DIR / "_custom_js"
8 |
9 |
10 | def setup(app: Sphinx) -> None:
11 | subprocess.run("bun install", cwd=CUSTOM_JS_DIR, shell=True) # noqa S607
12 | subprocess.run("bun run build", cwd=CUSTOM_JS_DIR, shell=True) # noqa S607
13 |
--------------------------------------------------------------------------------
/docs/source/_exts/copy_vdom_json_schema.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | from sphinx.application import Sphinx
5 |
6 | from reactpy.core.vdom import VDOM_JSON_SCHEMA
7 |
8 |
9 | def setup(app: Sphinx) -> None:
10 | schema_file = Path(__file__).parent.parent / "vdom-json-schema.json"
11 | current_schema = json.dumps(VDOM_JSON_SCHEMA, indent=2, sort_keys=True)
12 |
13 | # We need to make this check because the autoreload system for the docs checks
14 | # to see if the file has changed to determine whether to re-build. Thus we should
15 | # only write to the file if its contents will be different.
16 | if not schema_file.exists() or schema_file.read_text() != current_schema:
17 | schema_file.write_text(current_schema)
18 |
--------------------------------------------------------------------------------
/docs/source/_exts/custom_autosectionlabel.py:
--------------------------------------------------------------------------------
1 | """Mostly copied from sphinx.ext.autosectionlabel
2 |
3 | See Sphinx BSD license:
4 | https://github.com/sphinx-doc/sphinx/blob/f9968594206e538f13fa1c27c065027f10d4ea27/LICENSE
5 | """
6 |
7 | from __future__ import annotations
8 |
9 | from fnmatch import fnmatch
10 | from typing import Any, cast
11 |
12 | from docutils import nodes
13 | from docutils.nodes import Node
14 | from sphinx.application import Sphinx
15 | from sphinx.domains.std import StandardDomain
16 | from sphinx.locale import __
17 | from sphinx.util import logging
18 | from sphinx.util.nodes import clean_astext
19 |
20 | logger = logging.getLogger(__name__)
21 |
22 |
23 | def get_node_depth(node: Node) -> int:
24 | i = 0
25 | cur_node = node
26 | while cur_node.parent != node.document:
27 | cur_node = cur_node.parent
28 | i += 1
29 | return i
30 |
31 |
32 | def register_sections_as_label(app: Sphinx, document: Node) -> None:
33 | docname = app.env.docname
34 |
35 | for pattern in app.config.autosectionlabel_skip_docs:
36 | if fnmatch(docname, pattern):
37 | return None
38 |
39 | domain = cast(StandardDomain, app.env.get_domain("std"))
40 | for node in document.traverse(nodes.section):
41 | if (
42 | app.config.autosectionlabel_maxdepth
43 | and get_node_depth(node) >= app.config.autosectionlabel_maxdepth
44 | ):
45 | continue
46 | labelid = node["ids"][0]
47 |
48 | title = cast(nodes.title, node[0])
49 | ref_name = getattr(title, "rawsource", title.astext())
50 | if app.config.autosectionlabel_prefix_document:
51 | name = nodes.fully_normalize_name(docname + ":" + ref_name)
52 | else:
53 | name = nodes.fully_normalize_name(ref_name)
54 | sectname = clean_astext(title)
55 |
56 | if name in domain.labels:
57 | logger.warning(
58 | __("duplicate label %s, other instance in %s"),
59 | name,
60 | app.env.doc2path(domain.labels[name][0]),
61 | location=node,
62 | type="autosectionlabel",
63 | subtype=docname,
64 | )
65 |
66 | domain.anonlabels[name] = docname, labelid
67 | domain.labels[name] = docname, labelid, sectname
68 |
69 |
70 | def setup(app: Sphinx) -> dict[str, Any]:
71 | app.add_config_value("autosectionlabel_prefix_document", False, "env")
72 | app.add_config_value("autosectionlabel_maxdepth", None, "env")
73 | app.add_config_value("autosectionlabel_skip_docs", [], "env")
74 | app.connect("doctree-read", register_sections_as_label)
75 |
76 | return {
77 | "version": "builtin",
78 | "parallel_read_safe": True,
79 | "parallel_write_safe": True,
80 | }
81 |
--------------------------------------------------------------------------------
/docs/source/_exts/patched_html_translator.py:
--------------------------------------------------------------------------------
1 | from sphinx.util.docutils import is_html5_writer_available
2 | from sphinx.writers.html import HTMLTranslator
3 | from sphinx.writers.html5 import HTML5Translator
4 |
5 |
6 | class PatchedHTMLTranslator(
7 | HTML5Translator if is_html5_writer_available() else HTMLTranslator
8 | ):
9 | def starttag(self, node, tagname, *args, **attrs):
10 | if (
11 | tagname == "a"
12 | and "target" not in attrs
13 | and (
14 | "external" in attrs.get("class", "")
15 | or "external" in attrs.get("classes", [])
16 | )
17 | ):
18 | attrs["target"] = "_blank"
19 | attrs["ref"] = "noopener noreferrer"
20 | return super().starttag(node, tagname, *args, **attrs)
21 |
22 |
23 | def setup(app):
24 | app.set_translator("html", PatchedHTMLTranslator)
25 |
--------------------------------------------------------------------------------
/docs/source/_exts/reactpy_view.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Any, ClassVar
3 |
4 | from docs_app.examples import get_normalized_example_name
5 | from docutils.nodes import raw
6 | from docutils.parsers.rst import directives
7 | from sphinx.application import Sphinx
8 | from sphinx.util.docutils import SphinxDirective
9 |
10 | _REACTPY_EXAMPLE_HOST = os.environ.get("REACTPY_DOC_EXAMPLE_SERVER_HOST", "")
11 | _REACTPY_STATIC_HOST = os.environ.get("REACTPY_DOC_STATIC_SERVER_HOST", "/docs").rstrip(
12 | "/"
13 | )
14 |
15 |
16 | class IteractiveWidget(SphinxDirective):
17 | has_content = False
18 | required_arguments = 1
19 | _next_id = 0
20 |
21 | option_spec: ClassVar[dict[str, Any]] = {
22 | "activate-button": directives.flag,
23 | "margin": float,
24 | }
25 |
26 | def run(self):
27 | IteractiveWidget._next_id += 1
28 | container_id = f"reactpy-widget-{IteractiveWidget._next_id}"
29 | view_id = get_normalized_example_name(
30 | self.arguments[0],
31 | # only used if example name starts with "/"
32 | self.get_source_info()[0],
33 | )
34 | return [
35 | raw(
36 | "",
37 | f"""
38 |
54 | """,
55 | format="html",
56 | )
57 | ]
58 |
59 |
60 | def setup(app: Sphinx) -> None:
61 | app.add_directive("reactpy-view", IteractiveWidget)
62 |
--------------------------------------------------------------------------------
/docs/source/_static/css/furo-theme-overrides.css:
--------------------------------------------------------------------------------
1 | .sidebar-container {
2 | width: 18em;
3 | }
4 | .sidebar-brand-text {
5 | display: none;
6 | }
7 |
--------------------------------------------------------------------------------
/docs/source/_static/css/larger-api-margins.css:
--------------------------------------------------------------------------------
1 | :is(.data, .function, .class, .exception).py {
2 | margin-top: 3em;
3 | }
4 |
5 | :is(.attribute, .method).py {
6 | margin-top: 1.8em;
7 | }
8 |
--------------------------------------------------------------------------------
/docs/source/_static/css/larger-headings.css:
--------------------------------------------------------------------------------
1 | h1,
2 | h2,
3 | h3,
4 | h4,
5 | h5,
6 | h6 {
7 | margin-top: 1.5em !important;
8 | font-weight: 900 !important;
9 | }
10 |
--------------------------------------------------------------------------------
/docs/source/_static/css/reactpy-view.css:
--------------------------------------------------------------------------------
1 | .interactive {
2 | -webkit-transition: 0.1s ease-out;
3 | -moz-transition: 0.1s ease-out;
4 | -o-transition: 0.1s ease-out;
5 | transition: 0.1s ease-out;
6 | }
7 | .widget-container {
8 | padding: 15px;
9 | overflow: auto;
10 | background-color: var(--color-code-background);
11 | min-height: 75px;
12 | }
13 |
14 | .widget-container .printout {
15 | margin-top: 20px;
16 | border-top: solid 2px var(--color-foreground-border);
17 | padding-top: 20px;
18 | }
19 |
20 | .widget-container > div {
21 | width: 100%;
22 | }
23 |
24 | .enable-widget-button {
25 | padding: 10px;
26 | color: #ffffff !important;
27 | text-transform: uppercase;
28 | text-decoration: none;
29 | background: #526cfe;
30 | border: 2px solid #526cfe !important;
31 | transition: all 0.1s ease 0s;
32 | box-shadow: 0 5px 10px var(--color-foreground-border);
33 | }
34 | .enable-widget-button:hover {
35 | color: #526cfe !important;
36 | background: #ffffff;
37 | transition: all 0.1s ease 0s;
38 | }
39 | .enable-widget-button:focus {
40 | outline: 0 !important;
41 | transform: scale(0.98);
42 | transition: all 0.1s ease 0s;
43 | }
44 |
--------------------------------------------------------------------------------
/docs/source/_static/css/sphinx-design-overrides.css:
--------------------------------------------------------------------------------
1 | .sd-card-body {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: stretch;
5 | }
6 |
7 | .sd-tab-content .highlight pre {
8 | max-height: 700px;
9 | overflow: auto;
10 | }
11 |
12 | .sd-card-title .sd-badge {
13 | font-size: 1em;
14 | }
15 |
--------------------------------------------------------------------------------
/docs/source/_static/css/widget-output-css-overrides.css:
--------------------------------------------------------------------------------
1 | .widget-container h1,
2 | .widget-container h2,
3 | .widget-container h3,
4 | .widget-container h4,
5 | .widget-container h5,
6 | .widget-container h6 {
7 | margin: 0 !important;
8 | }
9 |
--------------------------------------------------------------------------------
/docs/source/_static/install-and-run-reactpy.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/_static/install-and-run-reactpy.gif
--------------------------------------------------------------------------------
/docs/source/about/credits-and-licenses.rst:
--------------------------------------------------------------------------------
1 | Credits and Licenses
2 | ====================
3 |
4 | Much of this documentation, including its layout and content, was created with heavy
5 | influence from https://reactjs.org which uses the `Creative Commons Attribution 4.0
6 | International
7 | `__
8 | license. While many things have been transformed, we paraphrase and, in some places,
9 | copy language or examples where ReactPy's behavior mirrors that of React's.
10 |
11 |
12 | Source Code License
13 | -------------------
14 |
15 | .. literalinclude:: ../../../LICENSE
16 | :language: text
17 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/components-with-state/_examples/adding_state_variable/main.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | from reactpy import component, hooks, html, run
5 |
6 | HERE = Path(__file__)
7 | DATA_PATH = HERE.parent / "data.json"
8 | sculpture_data = json.loads(DATA_PATH.read_text())
9 |
10 |
11 | @component
12 | def Gallery():
13 | index, set_index = hooks.use_state(0)
14 |
15 | def handle_click(event):
16 | set_index(index + 1)
17 |
18 | bounded_index = index % len(sculpture_data)
19 | sculpture = sculpture_data[bounded_index]
20 | alt = sculpture["alt"]
21 | artist = sculpture["artist"]
22 | description = sculpture["description"]
23 | name = sculpture["name"]
24 | url = sculpture["url"]
25 |
26 | return html.div(
27 | html.button({"on_click": handle_click}, "Next"),
28 | html.h2(name, " by ", artist),
29 | html.p(f"({bounded_index + 1} of {len(sculpture_data)})"),
30 | html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
31 | html.p(description),
32 | )
33 |
34 |
35 | run(Gallery)
36 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/components-with-state/_examples/isolated_state/main.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | from reactpy import component, hooks, html, run
5 |
6 | HERE = Path(__file__)
7 | DATA_PATH = HERE.parent / "data.json"
8 | sculpture_data = json.loads(DATA_PATH.read_text())
9 |
10 |
11 | @component
12 | def Gallery():
13 | index, set_index = hooks.use_state(0)
14 | show_more, set_show_more = hooks.use_state(False)
15 |
16 | def handle_next_click(event):
17 | set_index(index + 1)
18 |
19 | def handle_more_click(event):
20 | set_show_more(not show_more)
21 |
22 | bounded_index = index % len(sculpture_data)
23 | sculpture = sculpture_data[bounded_index]
24 | alt = sculpture["alt"]
25 | artist = sculpture["artist"]
26 | description = sculpture["description"]
27 | name = sculpture["name"]
28 | url = sculpture["url"]
29 |
30 | return html.div(
31 | html.button({"on_click": handle_next_click}, "Next"),
32 | html.h2(name, " by ", artist),
33 | html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
34 | html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
35 | html.div(
36 | html.button(
37 | {"on_click": handle_more_click},
38 | f"{('Show' if show_more else 'Hide')} details",
39 | ),
40 | (html.p(description) if show_more else ""),
41 | ),
42 | )
43 |
44 |
45 | @component
46 | def App():
47 | return html.div(
48 | html.section({"style": {"width": "50%", "float": "left"}}, Gallery()),
49 | html.section({"style": {"width": "50%", "float": "left"}}, Gallery()),
50 | )
51 |
52 |
53 | run(App)
54 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/components-with-state/_examples/multiple_state_variables/main.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | from reactpy import component, hooks, html, run
5 |
6 | HERE = Path(__file__)
7 | DATA_PATH = HERE.parent / "data.json"
8 | sculpture_data = json.loads(DATA_PATH.read_text())
9 |
10 |
11 | @component
12 | def Gallery():
13 | index, set_index = hooks.use_state(0)
14 | show_more, set_show_more = hooks.use_state(False)
15 |
16 | def handle_next_click(event):
17 | set_index(index + 1)
18 |
19 | def handle_more_click(event):
20 | set_show_more(not show_more)
21 |
22 | bounded_index = index % len(sculpture_data)
23 | sculpture = sculpture_data[bounded_index]
24 | alt = sculpture["alt"]
25 | artist = sculpture["artist"]
26 | description = sculpture["description"]
27 | name = sculpture["name"]
28 | url = sculpture["url"]
29 |
30 | return html.div(
31 | html.button({"on_click": handle_next_click}, "Next"),
32 | html.h2(name, " by ", artist),
33 | html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
34 | html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
35 | html.div(
36 | html.button(
37 | {"on_click": handle_more_click},
38 | f"{('Show' if show_more else 'Hide')} details",
39 | ),
40 | (html.p(description) if show_more else ""),
41 | ),
42 | )
43 |
44 |
45 | run(Gallery)
46 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/components-with-state/_examples/when_variables_are_not_enough/main.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 | # errors F841,F823 for `index = index + 1` inside the closure
3 |
4 | # :lines: 7-
5 | # :linenos:
6 |
7 | import json
8 | from pathlib import Path
9 |
10 | from reactpy import component, html, run
11 |
12 |
13 | HERE = Path(__file__)
14 | DATA_PATH = HERE.parent / "data.json"
15 | sculpture_data = json.loads(DATA_PATH.read_text())
16 |
17 |
18 | @component
19 | def Gallery():
20 | index = 0
21 |
22 | def handle_click(event):
23 | index = index + 1
24 |
25 | bounded_index = index % len(sculpture_data)
26 | sculpture = sculpture_data[bounded_index]
27 | alt = sculpture["alt"]
28 | artist = sculpture["artist"]
29 | description = sculpture["description"]
30 | name = sculpture["name"]
31 | url = sculpture["url"]
32 |
33 | return html.div(
34 | html.button({"on_click": handle_click}, "Next"),
35 | html.h2(name, " by ", artist),
36 | html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
37 | html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
38 | html.p(description),
39 | )
40 |
41 |
42 | run(Gallery)
43 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_remove.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def Definitions():
6 | term_to_add, set_term_to_add = use_state(None)
7 | definition_to_add, set_definition_to_add = use_state(None)
8 | all_terms, set_all_terms = use_state({})
9 |
10 | def handle_term_to_add_change(event):
11 | set_term_to_add(event["target"]["value"])
12 |
13 | def handle_definition_to_add_change(event):
14 | set_definition_to_add(event["target"]["value"])
15 |
16 | def handle_add_click(event):
17 | if term_to_add and definition_to_add:
18 | set_all_terms({**all_terms, term_to_add: definition_to_add})
19 | set_term_to_add(None)
20 | set_definition_to_add(None)
21 |
22 | def make_delete_click_handler(term_to_delete):
23 | def handle_click(event):
24 | set_all_terms({t: d for t, d in all_terms.items() if t != term_to_delete})
25 |
26 | return handle_click
27 |
28 | return html.div(
29 | html.button({"on_click": handle_add_click}, "add term"),
30 | html.label(
31 | "Term: ",
32 | html.input({"value": term_to_add, "on_change": handle_term_to_add_change}),
33 | ),
34 | html.label(
35 | "Definition: ",
36 | html.input(
37 | {
38 | "value": definition_to_add,
39 | "on_change": handle_definition_to_add_change,
40 | }
41 | ),
42 | ),
43 | html.hr(),
44 | [
45 | html.div(
46 | {"key": term},
47 | html.button(
48 | {"on_click": make_delete_click_handler(term)}, "delete term"
49 | ),
50 | html.dt(term),
51 | html.dd(definition),
52 | )
53 | for term, definition in all_terms.items()
54 | ],
55 | )
56 |
57 |
58 | run(Definitions)
59 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/dict_update.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def Form():
6 | person, set_person = use_state(
7 | {
8 | "first_name": "Barbara",
9 | "last_name": "Hepworth",
10 | "email": "bhepworth@sculpture.com",
11 | }
12 | )
13 |
14 | def handle_first_name_change(event):
15 | set_person({**person, "first_name": event["target"]["value"]})
16 |
17 | def handle_last_name_change(event):
18 | set_person({**person, "last_name": event["target"]["value"]})
19 |
20 | def handle_email_change(event):
21 | set_person({**person, "email": event["target"]["value"]})
22 |
23 | return html.div(
24 | html.label(
25 | "First name: ",
26 | html.input(
27 | {"value": person["first_name"], "on_change": handle_first_name_change}
28 | ),
29 | ),
30 | html.label(
31 | "Last name: ",
32 | html.input(
33 | {"value": person["last_name"], "on_change": handle_last_name_change}
34 | ),
35 | ),
36 | html.label(
37 | "Email: ",
38 | html.input({"value": person["email"], "on_change": handle_email_change}),
39 | ),
40 | html.p(f"{person['first_name']} {person['last_name']} {person['email']}"),
41 | )
42 |
43 |
44 | run(Form)
45 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_insert.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def ArtistList():
6 | artist_to_add, set_artist_to_add = use_state("")
7 | artists, set_artists = use_state([])
8 |
9 | def handle_change(event):
10 | set_artist_to_add(event["target"]["value"])
11 |
12 | def handle_click(event):
13 | if artist_to_add and artist_to_add not in artists:
14 | set_artists([*artists, artist_to_add])
15 | set_artist_to_add("")
16 |
17 | return html.div(
18 | html.h1("Inspiring sculptors:"),
19 | html.input({"value": artist_to_add, "on_change": handle_change}),
20 | html.button({"on_click": handle_click}, "add"),
21 | html.ul([html.li({"key": name}, name) for name in artists]),
22 | )
23 |
24 |
25 | run(ArtistList)
26 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_re_order.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def ArtistList():
6 | artists, set_artists = use_state(
7 | ["Marta Colvin Andrade", "Lamidi Olonade Fakeye", "Louise Nevelson"]
8 | )
9 |
10 | def handle_sort_click(event):
11 | set_artists(sorted(artists))
12 |
13 | def handle_reverse_click(event):
14 | set_artists(list(reversed(artists)))
15 |
16 | return html.div(
17 | html.h1("Inspiring sculptors:"),
18 | html.button({"on_click": handle_sort_click}, "sort"),
19 | html.button({"on_click": handle_reverse_click}, "reverse"),
20 | html.ul([html.li({"key": name}, name) for name in artists]),
21 | )
22 |
23 |
24 | run(ArtistList)
25 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_remove.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def ArtistList():
6 | artist_to_add, set_artist_to_add = use_state("")
7 | artists, set_artists = use_state(
8 | ["Marta Colvin Andrade", "Lamidi Olonade Fakeye", "Louise Nevelson"]
9 | )
10 |
11 | def handle_change(event):
12 | set_artist_to_add(event["target"]["value"])
13 |
14 | def handle_add_click(event):
15 | if artist_to_add not in artists:
16 | set_artists([*artists, artist_to_add])
17 | set_artist_to_add("")
18 |
19 | def make_handle_delete_click(index):
20 | def handle_click(event):
21 | set_artists(artists[:index] + artists[index + 1 :])
22 |
23 | return handle_click
24 |
25 | return html.div(
26 | html.h1("Inspiring sculptors:"),
27 | html.input({"value": artist_to_add, "on_change": handle_change}),
28 | html.button({"on_click": handle_add_click}, "add"),
29 | html.ul(
30 | [
31 | html.li(
32 | {"key": name},
33 | name,
34 | html.button(
35 | {"on_click": make_handle_delete_click(index)}, "delete"
36 | ),
37 | )
38 | for index, name in enumerate(artists)
39 | ]
40 | ),
41 | )
42 |
43 |
44 | run(ArtistList)
45 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/list_replace.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def CounterList():
6 | counters, set_counters = use_state([0, 0, 0])
7 |
8 | def make_increment_click_handler(index):
9 | def handle_click(event):
10 | new_value = counters[index] + 1
11 | set_counters(counters[:index] + [new_value] + counters[index + 1 :])
12 |
13 | return handle_click
14 |
15 | return html.ul(
16 | [
17 | html.li(
18 | {"key": index},
19 | count,
20 | html.button({"on_click": make_increment_click_handler(index)}, "+1"),
21 | )
22 | for index, count in enumerate(counters)
23 | ]
24 | )
25 |
26 |
27 | run(CounterList)
28 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def MovingDot():
6 | position, set_position = use_state({"x": 0, "y": 0})
7 |
8 | async def handle_pointer_move(event):
9 | outer_div_info = event["currentTarget"]
10 | outer_div_bounds = outer_div_info["boundingClientRect"]
11 | set_position(
12 | {
13 | "x": event["clientX"] - outer_div_bounds["x"],
14 | "y": event["clientY"] - outer_div_bounds["y"],
15 | }
16 | )
17 |
18 | return html.div(
19 | {
20 | "on_pointer_move": handle_pointer_move,
21 | "style": {
22 | "position": "relative",
23 | "height": "200px",
24 | "width": "100%",
25 | "background_color": "white",
26 | },
27 | },
28 | html.div(
29 | {
30 | "style": {
31 | "position": "absolute",
32 | "background_color": "red",
33 | "border_radius": "50%",
34 | "width": "20px",
35 | "height": "20px",
36 | "left": "-10px",
37 | "top": "-10px",
38 | "transform": f"translate({position['x']}px, {position['y']}px)",
39 | }
40 | }
41 | ),
42 | )
43 |
44 |
45 | run(MovingDot)
46 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/moving_dot_broken.py:
--------------------------------------------------------------------------------
1 | # :linenos:
2 |
3 | from reactpy import component, html, run, use_state
4 |
5 |
6 | @component
7 | def MovingDot():
8 | position, _ = use_state({"x": 0, "y": 0})
9 |
10 | def handle_pointer_move(event):
11 | outer_div_info = event["currentTarget"]
12 | outer_div_bounds = outer_div_info["boundingClientRect"]
13 | position["x"] = event["clientX"] - outer_div_bounds["x"]
14 | position["y"] = event["clientY"] - outer_div_bounds["y"]
15 |
16 | return html.div(
17 | {
18 | "on_pointer_move": handle_pointer_move,
19 | "style": {
20 | "position": "relative",
21 | "height": "200px",
22 | "width": "100%",
23 | "background_color": "white",
24 | },
25 | },
26 | html.div(
27 | {
28 | "style": {
29 | "position": "absolute",
30 | "background_color": "red",
31 | "border_radius": "50%",
32 | "width": "20px",
33 | "height": "20px",
34 | "left": "-10px",
35 | "top": "-10px",
36 | "transform": f"translate({position['x']}px, {position['y']}px)",
37 | }
38 | }
39 | ),
40 | )
41 |
42 |
43 | run(MovingDot)
44 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_remove.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def Grid():
6 | line_size = 5
7 | selected_indices, set_selected_indices = use_state({1, 2, 4})
8 |
9 | def make_handle_click(index):
10 | def handle_click(event):
11 | if index in selected_indices:
12 | set_selected_indices(selected_indices - {index})
13 | else:
14 | set_selected_indices(selected_indices | {index})
15 |
16 | return handle_click
17 |
18 | return html.div(
19 | {"style": {"display": "flex", "flex-direction": "row"}},
20 | [
21 | html.div(
22 | {
23 | "on_click": make_handle_click(index),
24 | "style": {
25 | "height": "30px",
26 | "width": "30px",
27 | "background_color": (
28 | "black" if index in selected_indices else "white"
29 | ),
30 | "outline": "1px solid grey",
31 | "cursor": "pointer",
32 | },
33 | "key": index,
34 | }
35 | )
36 | for index in range(line_size)
37 | ],
38 | )
39 |
40 |
41 | run(Grid)
42 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/dangers-of-mutability/_examples/set_update.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def Grid():
6 | line_size = 5
7 | selected_indices, set_selected_indices = use_state(set())
8 |
9 | def make_handle_click(index):
10 | def handle_click(event):
11 | set_selected_indices(selected_indices | {index})
12 |
13 | return handle_click
14 |
15 | return html.div(
16 | {"style": {"display": "flex", "flex-direction": "row"}},
17 | [
18 | html.div(
19 | {
20 | "on_click": make_handle_click(index),
21 | "style": {
22 | "height": "30px",
23 | "width": "30px",
24 | "background_color": (
25 | "black" if index in selected_indices else "white"
26 | ),
27 | "outline": "1px solid grey",
28 | "cursor": "pointer",
29 | },
30 | "key": index,
31 | }
32 | )
33 | for index in range(line_size)
34 | ],
35 | )
36 |
37 |
38 | run(Grid)
39 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_count_updater.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from reactpy import component, html, run, use_state
4 |
5 |
6 | @component
7 | def Counter():
8 | number, set_number = use_state(0)
9 |
10 | async def handle_click(event):
11 | await asyncio.sleep(3)
12 | set_number(lambda old_number: old_number + 1)
13 |
14 | return html.div(
15 | html.h1(number),
16 | html.button({"on_click": handle_click}, "Increment"),
17 | )
18 |
19 |
20 | run(Counter)
21 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/delay_before_set_count.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from reactpy import component, html, run, use_state
4 |
5 |
6 | @component
7 | def Counter():
8 | number, set_number = use_state(0)
9 |
10 | async def handle_click(event):
11 | await asyncio.sleep(3)
12 | set_number(number + 1)
13 |
14 | return html.div(
15 | html.h1(number),
16 | html.button({"on_click": handle_click}, "Increment"),
17 | )
18 |
19 |
20 | run(Counter)
21 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_color_3_times.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def ColorButton():
6 | color, set_color = use_state("gray")
7 |
8 | def handle_click(event):
9 | set_color("orange")
10 | set_color("pink")
11 | set_color("blue")
12 |
13 | def handle_reset(event):
14 | set_color("gray")
15 |
16 | return html.div(
17 | html.button(
18 | {"on_click": handle_click, "style": {"background_color": color}},
19 | "Set Color",
20 | ),
21 | html.button(
22 | {"on_click": handle_reset, "style": {"background_color": color}}, "Reset"
23 | ),
24 | )
25 |
26 |
27 | run(ColorButton)
28 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/multiple-state-updates/_examples/set_state_function.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | def increment(old_number):
5 | new_number = old_number + 1
6 | return new_number
7 |
8 |
9 | @component
10 | def Counter():
11 | number, set_number = use_state(0)
12 |
13 | def handle_click(event):
14 | set_number(increment)
15 | set_number(increment)
16 | set_number(increment)
17 |
18 | return html.div(
19 | html.h1(number),
20 | html.button({"on_click": handle_click}, "Increment"),
21 | )
22 |
23 |
24 | run(Counter)
25 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/responding-to-events/_examples/audio_player.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import reactpy
4 |
5 |
6 | @reactpy.component
7 | def PlayDinosaurSound():
8 | event, set_event = reactpy.hooks.use_state(None)
9 | return reactpy.html.div(
10 | reactpy.html.audio(
11 | {
12 | "controls": True,
13 | "on_time_update": lambda e: set_event(e),
14 | "src": "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
15 | }
16 | ),
17 | reactpy.html.pre(json.dumps(event, indent=2)),
18 | )
19 |
20 |
21 | reactpy.run(PlayDinosaurSound)
22 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_async_handlers.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from reactpy import component, html, run
4 |
5 |
6 | @component
7 | def ButtonWithDelay(message, delay):
8 | async def handle_event(event):
9 | await asyncio.sleep(delay)
10 | print(message)
11 |
12 | return html.button({"on_click": handle_event}, message)
13 |
14 |
15 | @component
16 | def App():
17 | return html.div(
18 | ButtonWithDelay("print 3 seconds later", delay=3),
19 | ButtonWithDelay("print immediately", delay=0),
20 | )
21 |
22 |
23 | run(App)
24 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_does_nothing.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def Button():
6 | return html.button("I don't do anything yet")
7 |
8 |
9 | run(Button)
10 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_handler_as_arg.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def Button(display_text, on_click):
6 | return html.button({"on_click": on_click}, display_text)
7 |
8 |
9 | @component
10 | def PlayButton(movie_name):
11 | def handle_click(event):
12 | print(f"Playing {movie_name}")
13 |
14 | return Button(f"Play {movie_name}", on_click=handle_click)
15 |
16 |
17 | @component
18 | def FastForwardButton():
19 | def handle_click(event):
20 | print("Skipping ahead")
21 |
22 | return Button("Fast forward", on_click=handle_click)
23 |
24 |
25 | @component
26 | def App():
27 | return html.div(
28 | PlayButton("Buena Vista Social Club"),
29 | FastForwardButton(),
30 | )
31 |
32 |
33 | run(App)
34 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_event.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def Button():
6 | def handle_event(event):
7 | print(event)
8 |
9 | return html.button({"on_click": handle_event}, "Click me!")
10 |
11 |
12 | run(Button)
13 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/responding-to-events/_examples/button_prints_message.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def PrintButton(display_text, message_text):
6 | def handle_event(event):
7 | print(message_text)
8 |
9 | return html.button({"on_click": handle_event}, display_text)
10 |
11 |
12 | @component
13 | def App():
14 | return html.div(
15 | PrintButton("Play", "Playing"),
16 | PrintButton("Pause", "Paused"),
17 | )
18 |
19 |
20 | run(App)
21 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/responding-to-events/_examples/prevent_default_event_actions.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, event, html, run
2 |
3 |
4 | @component
5 | def DoNotChangePages():
6 | return html.div(
7 | html.p("Normally clicking this link would take you to a new page"),
8 | html.a(
9 | {
10 | "on_click": event(lambda event: None, prevent_default=True),
11 | "href": "https://google.com",
12 | },
13 | "https://google.com",
14 | ),
15 | )
16 |
17 |
18 | run(DoNotChangePages)
19 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/responding-to-events/_examples/stop_event_propagation.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, event, hooks, html, run
2 |
3 |
4 | @component
5 | def DivInDiv():
6 | stop_propagatation, set_stop_propagatation = hooks.use_state(True)
7 | inner_count, set_inner_count = hooks.use_state(0)
8 | outer_count, set_outer_count = hooks.use_state(0)
9 |
10 | div_in_div = html.div(
11 | {
12 | "on_click": lambda event: set_outer_count(outer_count + 1),
13 | "style": {"height": "100px", "width": "100px", "background_color": "red"},
14 | },
15 | html.div(
16 | {
17 | "on_click": event(
18 | lambda event: set_inner_count(inner_count + 1),
19 | stop_propagation=stop_propagatation,
20 | ),
21 | "style": {
22 | "height": "50px",
23 | "width": "50px",
24 | "background_color": "blue",
25 | },
26 | }
27 | ),
28 | )
29 |
30 | return html.div(
31 | html.button(
32 | {"on_click": lambda event: set_stop_propagatation(not stop_propagatation)},
33 | "Toggle Propagation",
34 | ),
35 | html.pre(f"Will propagate: {not stop_propagatation}"),
36 | html.pre(f"Inner click count: {inner_count}"),
37 | html.pre(f"Outer click count: {outer_count}"),
38 | div_in_div,
39 | )
40 |
41 |
42 | run(DivInDiv)
43 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/delayed_print_after_set.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from reactpy import component, html, run, use_state
4 |
5 |
6 | @component
7 | def Counter():
8 | number, set_number = use_state(0)
9 |
10 | async def handle_click(event):
11 | set_number(number + 5)
12 | print("about to print...")
13 | await asyncio.sleep(3)
14 | print(number)
15 |
16 | return html.div(
17 | html.h1(number),
18 | html.button({"on_click": handle_click}, "Increment"),
19 | )
20 |
21 |
22 | run(Counter)
23 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_chat_message.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from reactpy import component, event, html, run, use_state
4 |
5 |
6 | @component
7 | def App():
8 | recipient, set_recipient = use_state("Alice")
9 | message, set_message = use_state("")
10 |
11 | @event(prevent_default=True)
12 | async def handle_submit(event):
13 | set_message("")
14 | print("About to send message...")
15 | await asyncio.sleep(5)
16 | print(f"Sent '{message}' to {recipient}")
17 |
18 | return html.form(
19 | {"on_submit": handle_submit, "style": {"display": "inline-grid"}},
20 | html.label(
21 | {},
22 | "To: ",
23 | html.select(
24 | {
25 | "value": recipient,
26 | "on_change": lambda event: set_recipient(event["target"]["value"]),
27 | },
28 | html.option({"value": "Alice"}, "Alice"),
29 | html.option({"value": "Bob"}, "Bob"),
30 | ),
31 | ),
32 | html.input(
33 | {
34 | "type": "text",
35 | "placeholder": "Your message...",
36 | "value": message,
37 | "on_change": lambda event: set_message(event["target"]["value"]),
38 | }
39 | ),
40 | html.button({"type": "submit"}, "Send"),
41 | )
42 |
43 |
44 | run(App)
45 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/print_count_after_set.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def Counter():
6 | number, set_number = use_state(0)
7 |
8 | def handle_click(event):
9 | set_number(number + 5)
10 | print(number)
11 |
12 | return html.div(
13 | html.h1(number),
14 | html.button({"on_click": handle_click}, "Increment"),
15 | )
16 |
17 |
18 | run(Counter)
19 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/send_message.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, event, html, run, use_state
2 |
3 |
4 | @component
5 | def App():
6 | is_sent, set_is_sent = use_state(False)
7 | message, set_message = use_state("")
8 |
9 | if is_sent:
10 | return html.div(
11 | html.h1("Message sent!"),
12 | html.button(
13 | {"on_click": lambda event: set_is_sent(False)}, "Send new message?"
14 | ),
15 | )
16 |
17 | @event(prevent_default=True)
18 | def handle_submit(event):
19 | set_message("")
20 | set_is_sent(True)
21 |
22 | return html.form(
23 | {"on_submit": handle_submit, "style": {"display": "inline-grid"}},
24 | html.textarea(
25 | {
26 | "placeholder": "Your message here...",
27 | "value": message,
28 | "on_change": lambda event: set_message(event["target"]["value"]),
29 | }
30 | ),
31 | html.button({"type": "submit"}, "Send"),
32 | )
33 |
34 |
35 | run(App)
36 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/state-as-a-snapshot/_examples/set_counter_3_times.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run, use_state
2 |
3 |
4 | @component
5 | def Counter():
6 | number, set_number = use_state(0)
7 |
8 | def handle_click(event):
9 | set_number(number + 1)
10 | set_number(number + 1)
11 | set_number(number + 1)
12 |
13 | return html.div(
14 | html.h1(number),
15 | html.button({"on_click": handle_click}, "Increment"),
16 | )
17 |
18 |
19 | run(Counter)
20 |
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/direct-state-change.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/direct-state-change.png
--------------------------------------------------------------------------------
/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/reactpy-state-change.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/guides/adding-interactivity/state-as-a-snapshot/_static/reactpy-state-change.png
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/rendering-data/_examples/sorted_and_filtered_todo_list.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def DataList(items, filter_by_priority=None, sort_by_priority=False):
6 | if filter_by_priority is not None:
7 | items = [i for i in items if i["priority"] <= filter_by_priority]
8 | if sort_by_priority:
9 | items = sorted(items, key=lambda i: i["priority"])
10 | list_item_elements = [html.li(i["text"]) for i in items]
11 | return html.ul(list_item_elements)
12 |
13 |
14 | @component
15 | def TodoList():
16 | tasks = [
17 | {"text": "Make breakfast", "priority": 0},
18 | {"text": "Feed the dog", "priority": 0},
19 | {"text": "Do laundry", "priority": 2},
20 | {"text": "Go on a run", "priority": 1},
21 | {"text": "Clean the house", "priority": 2},
22 | {"text": "Go to the grocery store", "priority": 2},
23 | {"text": "Do some coding", "priority": 1},
24 | {"text": "Read a book", "priority": 1},
25 | ]
26 | return html.section(
27 | html.h1("My Todo List"),
28 | DataList(tasks, filter_by_priority=1, sort_by_priority=True),
29 | )
30 |
31 |
32 | run(TodoList)
33 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/rendering-data/_examples/todo_from_list.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def DataList(items):
6 | list_item_elements = [html.li(text) for text in items]
7 | return html.ul(list_item_elements)
8 |
9 |
10 | @component
11 | def TodoList():
12 | tasks = [
13 | "Make breakfast (important)",
14 | "Feed the dog (important)",
15 | "Do laundry",
16 | "Go on a run (important)",
17 | "Clean the house",
18 | "Go to the grocery store",
19 | "Do some coding",
20 | "Read a book (important)",
21 | ]
22 | return html.section(
23 | html.h1("My Todo List"),
24 | DataList(tasks),
25 | )
26 |
27 |
28 | run(TodoList)
29 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/rendering-data/_examples/todo_list_with_keys.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def DataList(items, filter_by_priority=None, sort_by_priority=False):
6 | if filter_by_priority is not None:
7 | items = [i for i in items if i["priority"] <= filter_by_priority]
8 | if sort_by_priority:
9 | items = sorted(items, key=lambda i: i["priority"])
10 | list_item_elements = [html.li({"key": i["id"]}, i["text"]) for i in items]
11 | return html.ul(list_item_elements)
12 |
13 |
14 | @component
15 | def TodoList():
16 | tasks = [
17 | {"id": 0, "text": "Make breakfast", "priority": 0},
18 | {"id": 1, "text": "Feed the dog", "priority": 0},
19 | {"id": 2, "text": "Do laundry", "priority": 2},
20 | {"id": 3, "text": "Go on a run", "priority": 1},
21 | {"id": 4, "text": "Clean the house", "priority": 2},
22 | {"id": 5, "text": "Go to the grocery store", "priority": 2},
23 | {"id": 6, "text": "Do some coding", "priority": 1},
24 | {"id": 7, "text": "Read a book", "priority": 1},
25 | ]
26 | return html.section(
27 | html.h1("My Todo List"),
28 | DataList(tasks, filter_by_priority=1, sort_by_priority=True),
29 | )
30 |
31 |
32 | run(TodoList)
33 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/your-first-components/_examples/bad_conditional_todo_list.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def Item(name, done):
6 | if done:
7 | return html.li(name, " ✔")
8 | else:
9 | return html.li(name)
10 |
11 |
12 | @component
13 | def TodoList():
14 | return html.section(
15 | html.h1("My Todo List"),
16 | html.ul(
17 | Item("Find a cool problem to solve", done=True),
18 | Item("Build an app to solve it", done=True),
19 | Item("Share that app with the world!", done=False),
20 | ),
21 | )
22 |
23 |
24 | run(TodoList)
25 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/your-first-components/_examples/good_conditional_todo_list.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def Item(name, done):
6 | return html.li(name, " ✔" if done else "")
7 |
8 |
9 | @component
10 | def TodoList():
11 | return html.section(
12 | html.h1("My Todo List"),
13 | html.ul(
14 | Item("Find a cool problem to solve", done=True),
15 | Item("Build an app to solve it", done=True),
16 | Item("Share that app with the world!", done=False),
17 | ),
18 | )
19 |
20 |
21 | run(TodoList)
22 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/your-first-components/_examples/nested_photos.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def Photo():
6 | return html.img(
7 | {
8 | "src": "https://picsum.photos/id/274/500/300",
9 | "style": {"width": "30%"},
10 | "alt": "Ray Charles",
11 | }
12 | )
13 |
14 |
15 | @component
16 | def Gallery():
17 | return html.section(
18 | html.h1("Famous Musicians"),
19 | Photo(),
20 | Photo(),
21 | Photo(),
22 | )
23 |
24 |
25 | run(Gallery)
26 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/your-first-components/_examples/parametrized_photos.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def Photo(alt_text, image_id):
6 | return html.img(
7 | {
8 | "src": f"https://picsum.photos/id/{image_id}/500/200",
9 | "style": {"width": "50%"},
10 | "alt": alt_text,
11 | }
12 | )
13 |
14 |
15 | @component
16 | def Gallery():
17 | return html.section(
18 | html.h1("Photo Gallery"),
19 | Photo("Landscape", image_id=830),
20 | Photo("City", image_id=274),
21 | Photo("Puppy", image_id=237),
22 | )
23 |
24 |
25 | run(Gallery)
26 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/your-first-components/_examples/simple_photo.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def Photo():
6 | return html.img(
7 | {
8 | "src": "https://picsum.photos/id/237/500/300",
9 | "style": {"width": "50%"},
10 | "alt": "Puppy",
11 | }
12 | )
13 |
14 |
15 | run(Photo)
16 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/your-first-components/_examples/todo_list.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def Item(name, done):
6 | return html.li(name)
7 |
8 |
9 | @component
10 | def TodoList():
11 | return html.section(
12 | html.h1("My Todo List"),
13 | html.ul(
14 | Item("Find a cool problem to solve", done=True),
15 | Item("Build an app to solve it", done=True),
16 | Item("Share that app with the world!", done=False),
17 | ),
18 | )
19 |
20 |
21 | run(TodoList)
22 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_div.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def MyTodoList():
6 | return html.div(
7 | html.h1("My Todo List"),
8 | html.img({"src": "https://picsum.photos/id/0/500/300"}),
9 | html.ul(html.li("The first thing I need to do is...")),
10 | )
11 |
12 |
13 | run(MyTodoList)
14 |
--------------------------------------------------------------------------------
/docs/source/guides/creating-interfaces/your-first-components/_examples/wrap_in_fragment.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def MyTodoList():
6 | return html._(
7 | html.h1("My Todo List"),
8 | html.img({"src": "https://picsum.photos/id/0/500/200"}),
9 | html.ul(html.li("The first thing I need to do is...")),
10 | )
11 |
12 |
13 | run(MyTodoList)
14 |
--------------------------------------------------------------------------------
/docs/source/guides/escape-hatches/_examples/material_ui_button_no_action.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, run, web
2 |
3 | mui = web.module_from_template(
4 | "react@^17.0.0",
5 | "@material-ui/core@4.12.4",
6 | fallback="⌛",
7 | )
8 | Button = web.export(mui, "Button")
9 |
10 |
11 | @component
12 | def HelloWorld():
13 | return Button({"color": "primary", "variant": "contained"}, "Hello World!")
14 |
15 |
16 | run(HelloWorld)
17 |
--------------------------------------------------------------------------------
/docs/source/guides/escape-hatches/_examples/material_ui_button_on_click.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import reactpy
4 |
5 | mui = reactpy.web.module_from_template(
6 | "react@^17.0.0",
7 | "@material-ui/core@4.12.4",
8 | fallback="⌛",
9 | )
10 | Button = reactpy.web.export(mui, "Button")
11 |
12 |
13 | @reactpy.component
14 | def ViewButtonEvents():
15 | event, set_event = reactpy.hooks.use_state(None)
16 |
17 | return reactpy.html.div(
18 | Button(
19 | {
20 | "color": "primary",
21 | "variant": "contained",
22 | "onClick": lambda event: set_event(event),
23 | },
24 | "Click Me!",
25 | ),
26 | reactpy.html.pre(json.dumps(event, indent=2)),
27 | )
28 |
29 |
30 | reactpy.run(ViewButtonEvents)
31 |
--------------------------------------------------------------------------------
/docs/source/guides/escape-hatches/_examples/super_simple_chart/main.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from reactpy import component, run, web
4 |
5 | file = Path(__file__).parent / "super-simple-chart.js"
6 | ssc = web.module_from_file("super-simple-chart", file, fallback="⌛")
7 | SuperSimpleChart = web.export(ssc, "SuperSimpleChart")
8 |
9 |
10 | @component
11 | def App():
12 | return SuperSimpleChart(
13 | {
14 | "data": [
15 | {"x": 1, "y": 2},
16 | {"x": 2, "y": 4},
17 | {"x": 3, "y": 7},
18 | {"x": 4, "y": 3},
19 | {"x": 5, "y": 5},
20 | {"x": 6, "y": 9},
21 | {"x": 7, "y": 6},
22 | ],
23 | "height": 300,
24 | "width": 500,
25 | "color": "royalblue",
26 | "lineWidth": 4,
27 | "axisColor": "silver",
28 | }
29 | )
30 |
31 |
32 | run(App)
33 |
--------------------------------------------------------------------------------
/docs/source/guides/escape-hatches/_examples/super_simple_chart/super-simple-chart.js:
--------------------------------------------------------------------------------
1 | import { h, render } from "https://unpkg.com/preact?module";
2 | import htm from "https://unpkg.com/htm?module";
3 |
4 | const html = htm.bind(h);
5 |
6 | export function bind(node, config) {
7 | return {
8 | create: (component, props, children) => h(component, props, ...children),
9 | render: (element) => render(element, node),
10 | unmount: () => render(null, node),
11 | };
12 | }
13 |
14 | export function SuperSimpleChart(props) {
15 | const data = props.data;
16 | const lastDataIndex = data.length - 1;
17 |
18 | const options = {
19 | height: props.height || 100,
20 | width: props.width || 100,
21 | color: props.color || "blue",
22 | lineWidth: props.lineWidth || 2,
23 | axisColor: props.axisColor || "black",
24 | };
25 |
26 | const xData = data.map((point) => point.x);
27 | const yData = data.map((point) => point.y);
28 |
29 | const domain = {
30 | xMin: Math.min(...xData),
31 | xMax: Math.max(...xData),
32 | yMin: Math.min(...yData),
33 | yMax: Math.max(...yData),
34 | };
35 |
36 | return html`
41 | ${makePath(props, domain, data, options)} ${makeAxis(props, options)}
42 | `;
43 | }
44 |
45 | function makePath(props, domain, data, options) {
46 | const { xMin, xMax, yMin, yMax } = domain;
47 | const { width, height } = options;
48 | const getSvgX = (x) => ((x - xMin) / (xMax - xMin)) * width;
49 | const getSvgY = (y) => height - ((y - yMin) / (yMax - yMin)) * height;
50 |
51 | let pathD =
52 | `M ${getSvgX(data[0].x)} ${getSvgY(data[0].y)} ` +
53 | data.map(({ x, y }, i) => `L ${getSvgX(x)} ${getSvgY(y)}`).join(" ");
54 |
55 | return html` `;
63 | }
64 |
65 | function makeAxis(props, options) {
66 | return html`
67 |
74 |
81 | `;
82 | }
83 |
--------------------------------------------------------------------------------
/docs/source/guides/escape-hatches/index.rst:
--------------------------------------------------------------------------------
1 | Escape Hatches
2 | ==============
3 |
4 | .. toctree::
5 | :hidden:
6 |
7 | javascript-components
8 | distributing-javascript
9 | using-a-custom-backend
10 | using-a-custom-client
11 |
12 | .. note::
13 |
14 | Under construction 🚧
15 |
--------------------------------------------------------------------------------
/docs/source/guides/escape-hatches/using-a-custom-backend.rst:
--------------------------------------------------------------------------------
1 | .. _Writing Your Own Backend:
2 | .. _Using a Custom Backend:
3 |
4 | Using a Custom Backend 🚧
5 | =========================
6 |
7 | .. note::
8 |
9 | Under construction 🚧
10 |
--------------------------------------------------------------------------------
/docs/source/guides/escape-hatches/using-a-custom-client.rst:
--------------------------------------------------------------------------------
1 | .. _Writing Your Own Client:
2 | .. _Using a Custom Client:
3 |
4 | Using a Custom Client 🚧
5 | ========================
6 |
7 | .. note::
8 |
9 | Under construction 🚧
10 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_examples/debug_error_example.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def App():
6 | return html.div(GoodComponent(), BadComponent())
7 |
8 |
9 | @component
10 | def GoodComponent():
11 | return html.p("This component rendered successfully")
12 |
13 |
14 | @component
15 | def BadComponent():
16 | msg = "This component raised an error"
17 | raise RuntimeError(msg)
18 |
19 |
20 | run(App)
21 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_examples/hello_world.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, html, run
2 |
3 |
4 | @component
5 | def App():
6 | return html.h1("Hello, world!")
7 |
8 |
9 | run(App)
10 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_examples/run_fastapi.py:
--------------------------------------------------------------------------------
1 | # :lines: 11-
2 |
3 | from reactpy import run
4 | from reactpy.backend import fastapi as fastapi_server
5 |
6 | # the run() function is the entry point for examples
7 | fastapi_server.configure = lambda _, cmpt: run(cmpt)
8 |
9 |
10 | from fastapi import FastAPI
11 |
12 | from reactpy import component, html
13 | from reactpy.backend.fastapi import configure
14 |
15 |
16 | @component
17 | def HelloWorld():
18 | return html.h1("Hello, world!")
19 |
20 |
21 | app = FastAPI()
22 | configure(app, HelloWorld)
23 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_examples/run_flask.py:
--------------------------------------------------------------------------------
1 | # :lines: 11-
2 |
3 | from reactpy import run
4 | from reactpy.backend import flask as flask_server
5 |
6 | # the run() function is the entry point for examples
7 | flask_server.configure = lambda _, cmpt: run(cmpt)
8 |
9 |
10 | from flask import Flask
11 |
12 | from reactpy import component, html
13 | from reactpy.backend.flask import configure
14 |
15 |
16 | @component
17 | def HelloWorld():
18 | return html.h1("Hello, world!")
19 |
20 |
21 | app = Flask(__name__)
22 | configure(app, HelloWorld)
23 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_examples/run_sanic.py:
--------------------------------------------------------------------------------
1 | # :lines: 11-
2 |
3 | from reactpy import run
4 | from reactpy.backend import sanic as sanic_server
5 |
6 | # the run() function is the entry point for examples
7 | sanic_server.configure = lambda _, cmpt: run(cmpt)
8 |
9 |
10 | from sanic import Sanic
11 |
12 | from reactpy import component, html
13 | from reactpy.backend.sanic import configure
14 |
15 |
16 | @component
17 | def HelloWorld():
18 | return html.h1("Hello, world!")
19 |
20 |
21 | app = Sanic("MyApp")
22 | configure(app, HelloWorld)
23 |
24 |
25 | if __name__ == "__main__":
26 | app.run(port=8000)
27 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_examples/run_starlette.py:
--------------------------------------------------------------------------------
1 | # :lines: 10-
2 |
3 | from reactpy import run
4 | from reactpy.backend import starlette as starlette_server
5 |
6 | # the run() function is the entry point for examples
7 | starlette_server.configure = lambda _, cmpt: run(cmpt)
8 |
9 |
10 | from starlette.applications import Starlette
11 |
12 | from reactpy import component, html
13 | from reactpy.backend.starlette import configure
14 |
15 |
16 | @component
17 | def HelloWorld():
18 | return html.h1("Hello, world!")
19 |
20 |
21 | app = Starlette()
22 | configure(app, HelloWorld)
23 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_examples/run_tornado.py:
--------------------------------------------------------------------------------
1 | # :lines: 11-
2 |
3 | from reactpy import run
4 | from reactpy.backend import tornado as tornado_server
5 |
6 | # the run() function is the entry point for examples
7 | tornado_server.configure = lambda _, cmpt: run(cmpt)
8 |
9 |
10 | import tornado.ioloop
11 | import tornado.web
12 |
13 | from reactpy import component, html
14 | from reactpy.backend.tornado import configure
15 |
16 |
17 | @component
18 | def HelloWorld():
19 | return html.h1("Hello, world!")
20 |
21 |
22 | def make_app():
23 | app = tornado.web.Application()
24 | configure(app, HelloWorld)
25 | return app
26 |
27 |
28 | if __name__ == "__main__":
29 | app = make_app()
30 | app.listen(8000)
31 | tornado.ioloop.IOLoop.current().start()
32 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_examples/sample_app.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 |
3 | reactpy.run(reactpy.sample.SampleApp)
4 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_static/embed-doc-ex.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_static/embed-reactpy-view/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example App
6 |
7 |
8 | This is an Example App
9 | Just below is an embedded ReactPy view...
10 |
11 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_static/embed-reactpy-view/main.py:
--------------------------------------------------------------------------------
1 | from sanic import Sanic
2 | from sanic.response import file
3 |
4 | from reactpy import component, html
5 | from reactpy.backend.sanic import Options, configure
6 |
7 | app = Sanic("MyApp")
8 |
9 |
10 | @app.route("/")
11 | async def index(request):
12 | return await file("index.html")
13 |
14 |
15 | @component
16 | def ReactPyView():
17 | return html.code("This text came from an ReactPy App")
18 |
19 |
20 | configure(app, ReactPyView, Options(url_prefix="/_reactpy"))
21 |
22 | app.run(host="127.0.0.1", port=5000)
23 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_static/embed-reactpy-view/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/guides/getting-started/_static/embed-reactpy-view/screenshot.png
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_static/logo-django.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 | ]>
7 |
9 |
10 |
11 |
14 |
17 |
22 |
25 |
32 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_static/reactpy-in-jupyterlab.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/guides/getting-started/_static/reactpy-in-jupyterlab.gif
--------------------------------------------------------------------------------
/docs/source/guides/getting-started/_static/shared-client-state-server-slider.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/guides/getting-started/_static/shared-client-state-server-slider.gif
--------------------------------------------------------------------------------
/docs/source/guides/managing-state/combining-contexts-and-reducers/index.rst:
--------------------------------------------------------------------------------
1 | Combining Contexts and Reducers 🚧
2 | ==================================
3 |
4 | .. note::
5 |
6 | Under construction 🚧
7 |
--------------------------------------------------------------------------------
/docs/source/guides/managing-state/deeply-sharing-state-with-contexts/index.rst:
--------------------------------------------------------------------------------
1 | Deeply Sharing State with Contexts 🚧
2 | =====================================
3 |
4 | .. note::
5 |
6 | Under construction 🚧
7 |
--------------------------------------------------------------------------------
/docs/source/guides/managing-state/how-to-structure-state/index.rst:
--------------------------------------------------------------------------------
1 | .. _Structuring Your State:
2 |
3 | How to Structure State 🚧
4 | =========================
5 |
6 | .. note::
7 |
8 | Under construction 🚧
9 |
--------------------------------------------------------------------------------
/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/data.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Sushi",
4 | "description": "Sushi is a traditional Japanese dish of prepared vinegared rice"
5 | },
6 | {
7 | "name": "Dal",
8 | "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"
9 | },
10 | {
11 | "name": "Pierogi",
12 | "description": "Pierogi are filled dumplings made by wrapping unleavened dough around a savoury or sweet filling and cooking in boiling water"
13 | },
14 | {
15 | "name": "Shish Kebab",
16 | "description": "Shish kebab is a popular meal of skewered and grilled cubes of meat"
17 | },
18 | {
19 | "name": "Dim sum",
20 | "description": "Dim sum is a large range of small dishes that Cantonese people traditionally enjoy in restaurants for breakfast and lunch"
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/docs/source/guides/managing-state/sharing-component-state/_examples/filterable_list/main.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | from reactpy import component, hooks, html, run
5 |
6 | HERE = Path(__file__)
7 | DATA_PATH = HERE.parent / "data.json"
8 | food_data = json.loads(DATA_PATH.read_text())
9 |
10 |
11 | @component
12 | def FilterableList():
13 | value, set_value = hooks.use_state("")
14 | return html.p(Search(value, set_value), html.hr(), Table(value, set_value))
15 |
16 |
17 | @component
18 | def Search(value, set_value):
19 | def handle_change(event):
20 | set_value(event["target"]["value"])
21 |
22 | return html.label(
23 | "Search by Food Name: ",
24 | html.input({"value": value, "on_change": handle_change}),
25 | )
26 |
27 |
28 | @component
29 | def Table(value, set_value):
30 | rows = []
31 | for row in food_data:
32 | name = html.td(row["name"])
33 | descr = html.td(row["description"])
34 | tr = html.tr(name, descr, value)
35 | if not value:
36 | rows.append(tr)
37 | elif value.lower() in row["name"].lower():
38 | rows.append(tr)
39 | headers = html.tr(html.td(html.b("name")), html.td(html.b("description")))
40 | table = html.table(html.thead(headers), html.tbody(rows))
41 | return table
42 |
43 |
44 | run(FilterableList)
45 |
--------------------------------------------------------------------------------
/docs/source/guides/managing-state/sharing-component-state/_examples/synced_inputs/main.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, hooks, html, run
2 |
3 |
4 | @component
5 | def SyncedInputs():
6 | value, set_value = hooks.use_state("")
7 | return html.p(
8 | Input("First input", value, set_value),
9 | Input("Second input", value, set_value),
10 | )
11 |
12 |
13 | @component
14 | def Input(label, value, set_value):
15 | def handle_change(event):
16 | set_value(event["target"]["value"])
17 |
18 | return html.label(
19 | label + " ", html.input({"value": value, "on_change": handle_change})
20 | )
21 |
22 |
23 | run(SyncedInputs)
24 |
--------------------------------------------------------------------------------
/docs/source/guides/managing-state/sharing-component-state/index.rst:
--------------------------------------------------------------------------------
1 | Sharing Component State
2 | =======================
3 |
4 | .. note::
5 |
6 | Parts of this document are still under construction 🚧
7 |
8 | Sometimes, you want the state of two components to always change together. To do it,
9 | remove state from both of them, move it to their closest common parent, and then pass it
10 | down to them via props. This is known as “lifting state up”, and it’s one of the most
11 | common things you will do writing code with ReactPy.
12 |
13 |
14 | Synced Inputs
15 | -------------
16 |
17 | In the code below the two input boxes are synchronized, this happens because they share
18 | state. The state is shared via the parent component ``SyncedInputs``. Check the ``value``
19 | and ``set_value`` variables.
20 |
21 | .. reactpy:: _examples/synced_inputs
22 |
23 |
24 | Filterable List
25 | ----------------
26 |
27 | In the example below the search input and the list of elements below share the
28 | same state, the state represents the food name.
29 |
30 | Note how the component ``Table`` gets called at each change of state. The
31 | component is observing the state and reacting to state changes automatically,
32 | just like it would do in React.
33 |
34 | .. reactpy:: _examples/filterable_list
35 |
36 | .. note::
37 |
38 | Try typing a food name in the search bar.
39 |
--------------------------------------------------------------------------------
/docs/source/guides/managing-state/simplifying-updates-with-reducers/index.rst:
--------------------------------------------------------------------------------
1 | Simplifying Updates with Reducers 🚧
2 | ====================================
3 |
4 | .. note::
5 |
6 | Under construction 🚧
7 |
--------------------------------------------------------------------------------
/docs/source/guides/managing-state/when-and-how-to-reset-state/index.rst:
--------------------------------------------------------------------------------
1 | .. _When to Reset State:
2 |
3 | When and How to Reset State 🚧
4 | ==============================
5 |
6 | .. note::
7 |
8 | Under construction 🚧
9 |
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/_static/live-examples-in-docs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/guides/understanding-reactpy/_static/live-examples-in-docs.gif
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/_static/npm-download-trends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/guides/understanding-reactpy/_static/npm-download-trends.png
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/index.rst:
--------------------------------------------------------------------------------
1 | Understanding ReactPy
2 | =====================
3 |
4 | .. toctree::
5 | :hidden:
6 |
7 | representing-html
8 | what-are-components
9 | the-rendering-pipeline
10 | why-reactpy-needs-keys
11 | the-rendering-process
12 | layout-render-servers
13 | writing-tests
14 |
15 | .. note::
16 |
17 | Under construction 🚧
18 |
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/layout-render-servers.rst:
--------------------------------------------------------------------------------
1 | .. _Layout Render Servers:
2 |
3 | Layout Render Servers 🚧
4 | ========================
5 |
6 | .. note::
7 |
8 | Under construction 🚧
9 |
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/representing-html.rst:
--------------------------------------------------------------------------------
1 | .. _Representing HTML:
2 |
3 | Representing HTML 🚧
4 | ====================
5 |
6 | .. note::
7 |
8 | Under construction 🚧
9 |
10 | 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
12 | it. Let's reconsider the examples from before - on the top is some HTML and on the
13 | bottom is the corresponding code to create it in ReactPy:
14 |
15 | .. code-block:: html
16 |
17 |
18 |
My Todo List
19 |
20 | Build a cool new app
21 | Share it with the world!
22 |
23 |
24 |
25 | .. testcode::
26 |
27 | from reactpy import html
28 |
29 | layout = html.div(
30 | html.h1("My Todo List"),
31 | html.ul(
32 | html.li("Build a cool new app"),
33 | html.li("Share it with the world!"),
34 | )
35 | )
36 |
37 | Since we've captured our HTML into out the ``layout`` variable, we can inspect what it
38 | contains. And, as it turns out, it holds a dictionary. Printing it produces the
39 | following output:
40 |
41 | .. testsetup::
42 |
43 | from pprint import pprint
44 | print = lambda *args, **kwargs: pprint(*args, **kwargs, sort_dicts=False)
45 |
46 | .. testcode::
47 |
48 | assert layout == {
49 | 'tagName': 'div',
50 | 'children': [
51 | {
52 | 'tagName': 'h1',
53 | 'children': ['My Todo List']
54 | },
55 | {
56 | 'tagName': 'ul',
57 | 'children': [
58 | {'tagName': 'li', 'children': ['Build a cool new app']},
59 | {'tagName': 'li', 'children': ['Share it with the world!']}
60 | ]
61 | }
62 | ]
63 | }
64 |
65 | This may look complicated, but let's take a moment to consider what's going on here. We
66 | have a series of nested dictionaries that, in some way, represents the HTML structure
67 | given above. If we look at their contents we should see a common form. Each has a
68 | ``tagName`` key which contains, as the name would suggest, the tag name of an HTML
69 | element. Then within the ``children`` key is a list that either contains strings or
70 | other dictionaries that represent HTML elements.
71 |
72 | What we're seeing here is called a "virtual document object model" or :ref:`VDOM`. This
73 | is just a fancy way of saying we have a representation of the document object model or
74 | `DOM
75 | `__
76 | that is not the actual DOM.
77 |
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/the-rendering-pipeline.rst:
--------------------------------------------------------------------------------
1 | .. _The Rendering Pipeline:
2 |
3 | The Rendering Pipeline 🚧
4 | =========================
5 |
6 | .. talk about layouts and dispatchers
7 |
8 | .. note::
9 |
10 | Under construction 🚧
11 |
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/the-rendering-process.rst:
--------------------------------------------------------------------------------
1 | .. _The Rendering Process:
2 |
3 | The Rendering Process 🚧
4 | ========================
5 |
6 | .. refer to https://beta.reactjs.org/learn/render-and-commit
7 |
8 | .. note::
9 |
10 | Under construction 🚧
11 |
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/what-are-components.rst:
--------------------------------------------------------------------------------
1 | .. _What Are Components:
2 |
3 | What Are Components? 🚧
4 | =======================
5 |
6 | .. note::
7 |
8 | Under construction 🚧
9 |
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/why-reactpy-needs-keys.rst:
--------------------------------------------------------------------------------
1 | .. _Why ReactPy Needs Keys:
2 |
3 | Why ReactPy Needs Keys 🚧
4 | =========================
5 |
6 | .. note::
7 |
8 | Under construction 🚧
9 |
--------------------------------------------------------------------------------
/docs/source/guides/understanding-reactpy/writing-tests.rst:
--------------------------------------------------------------------------------
1 | .. _Writing Tests:
2 |
3 | Writing Tests 🚧
4 | ================
5 |
6 | .. note::
7 |
8 | Under construction 🚧
9 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/character_movement/main.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import NamedTuple
3 |
4 | from reactpy import component, html, run, use_state
5 | from reactpy.widgets import image
6 |
7 | HERE = Path(__file__)
8 | CHARACTER_IMAGE = (HERE.parent / "static" / "bunny.png").read_bytes()
9 |
10 |
11 | class Position(NamedTuple):
12 | x: int
13 | y: int
14 | angle: int
15 |
16 |
17 | def rotate(degrees):
18 | return lambda old_position: Position(
19 | old_position.x,
20 | old_position.y,
21 | old_position.angle + degrees,
22 | )
23 |
24 |
25 | def translate(x=0, y=0):
26 | return lambda old_position: Position(
27 | old_position.x + x,
28 | old_position.y + y,
29 | old_position.angle,
30 | )
31 |
32 |
33 | @component
34 | def Scene():
35 | position, set_position = use_state(Position(100, 100, 0))
36 |
37 | return html.div(
38 | {"style": {"width": "225px"}},
39 | html.div(
40 | {
41 | "style": {
42 | "width": "200px",
43 | "height": "200px",
44 | "background_color": "slategray",
45 | }
46 | },
47 | image(
48 | "png",
49 | CHARACTER_IMAGE,
50 | {
51 | "style": {
52 | "position": "relative",
53 | "left": f"{position.x}px",
54 | "top": f"{position.y}.px",
55 | "transform": f"rotate({position.angle}deg) scale(2, 2)",
56 | }
57 | },
58 | ),
59 | ),
60 | html.button(
61 | {"on_click": lambda e: set_position(translate(x=-10))}, "Move Left"
62 | ),
63 | html.button(
64 | {"on_click": lambda e: set_position(translate(x=10))}, "Move Right"
65 | ),
66 | html.button({"on_click": lambda e: set_position(translate(y=-10))}, "Move Up"),
67 | html.button({"on_click": lambda e: set_position(translate(y=10))}, "Move Down"),
68 | html.button({"on_click": lambda e: set_position(rotate(-30))}, "Rotate Left"),
69 | html.button({"on_click": lambda e: set_position(rotate(30))}, "Rotate Right"),
70 | )
71 |
72 |
73 | run(Scene)
74 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/character_movement/static/bunny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/docs/source/reference/_examples/character_movement/static/bunny.png
--------------------------------------------------------------------------------
/docs/source/reference/_examples/click_count.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 |
3 |
4 | @reactpy.component
5 | def ClickCount():
6 | count, set_count = reactpy.hooks.use_state(0)
7 |
8 | return reactpy.html.button(
9 | {"on_click": lambda event: set_count(count + 1)}, [f"Click count: {count}"]
10 | )
11 |
12 |
13 | reactpy.run(ClickCount)
14 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/material_ui_switch.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 |
3 | mui = reactpy.web.module_from_template("react", "@material-ui/core@^5.0", fallback="⌛")
4 | Switch = reactpy.web.export(mui, "Switch")
5 |
6 |
7 | @reactpy.component
8 | def DayNightSwitch():
9 | checked, set_checked = reactpy.hooks.use_state(False)
10 |
11 | return reactpy.html.div(
12 | Switch(
13 | {
14 | "checked": checked,
15 | "onChange": lambda event, checked: set_checked(checked),
16 | }
17 | ),
18 | "🌞" if checked else "🌚",
19 | )
20 |
21 |
22 | reactpy.run(DayNightSwitch)
23 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/matplotlib_plot.py:
--------------------------------------------------------------------------------
1 | from io import BytesIO
2 |
3 | import matplotlib.pyplot as plt
4 |
5 | import reactpy
6 | from reactpy.widgets import image
7 |
8 |
9 | @reactpy.component
10 | def PolynomialPlot():
11 | coefficients, set_coefficients = reactpy.hooks.use_state([0])
12 |
13 | x = list(linspace(-1, 1, 50))
14 | y = [polynomial(value, coefficients) for value in x]
15 |
16 | return reactpy.html.div(
17 | plot(f"{len(coefficients)} Term Polynomial", x, y),
18 | ExpandableNumberInputs(coefficients, set_coefficients),
19 | )
20 |
21 |
22 | @reactpy.component
23 | def ExpandableNumberInputs(values, set_values):
24 | inputs = []
25 | for i in range(len(values)):
26 |
27 | def set_value_at_index(event, index=i):
28 | new_value = float(event["target"]["value"] or 0)
29 | set_values(values[:index] + [new_value] + values[index + 1 :])
30 |
31 | inputs.append(poly_coef_input(i + 1, set_value_at_index))
32 |
33 | def add_input():
34 | set_values([*values, 0])
35 |
36 | def del_input():
37 | set_values(values[:-1])
38 |
39 | return reactpy.html.div(
40 | reactpy.html.div(
41 | "add/remove term:",
42 | reactpy.html.button({"on_click": lambda event: add_input()}, "+"),
43 | reactpy.html.button({"on_click": lambda event: del_input()}, "-"),
44 | ),
45 | inputs,
46 | )
47 |
48 |
49 | def plot(title, x, y):
50 | fig, axes = plt.subplots()
51 | axes.plot(x, y)
52 | axes.set_title(title)
53 | buffer = BytesIO()
54 | fig.savefig(buffer, format="png")
55 | plt.close(fig)
56 | return image("png", buffer.getvalue())
57 |
58 |
59 | def poly_coef_input(index, callback):
60 | return reactpy.html.div(
61 | {"style": {"margin-top": "5px"}, "key": index},
62 | reactpy.html.label(
63 | "C",
64 | reactpy.html.sub(index),
65 | " x X",
66 | reactpy.html.sup(index),
67 | ),
68 | reactpy.html.input({"type": "number", "on_change": callback}),
69 | )
70 |
71 |
72 | def polynomial(x, coefficients):
73 | return sum(c * (x ** (i + 1)) for i, c in enumerate(coefficients))
74 |
75 |
76 | def linspace(start, stop, n):
77 | if n == 1:
78 | yield stop
79 | return
80 | h = (stop - start) / (n - 1)
81 | for i in range(n):
82 | yield start + h * i
83 |
84 |
85 | reactpy.run(PolynomialPlot)
86 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/network_graph.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | import reactpy
4 |
5 | react_cytoscapejs = reactpy.web.module_from_template(
6 | "react",
7 | "react-cytoscapejs",
8 | fallback="⌛",
9 | )
10 | Cytoscape = reactpy.web.export(react_cytoscapejs, "default")
11 |
12 |
13 | @reactpy.component
14 | def RandomNetworkGraph():
15 | return Cytoscape(
16 | {
17 | "style": {"width": "100%", "height": "200px"},
18 | "elements": random_network(20),
19 | "layout": {"name": "cose"},
20 | }
21 | )
22 |
23 |
24 | def random_network(number_of_nodes):
25 | conns = []
26 | nodes = [{"data": {"id": 0, "label": 0}}]
27 |
28 | for src_node_id in range(1, number_of_nodes + 1):
29 | tgt_node = random.choice(nodes)
30 | src_node = {"data": {"id": src_node_id, "label": src_node_id}}
31 |
32 | new_conn = {"data": {"source": src_node_id, "target": tgt_node["data"]["id"]}}
33 |
34 | nodes.append(src_node)
35 | conns.append(new_conn)
36 |
37 | return nodes + conns
38 |
39 |
40 | reactpy.run(RandomNetworkGraph)
41 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/pigeon_maps.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 |
3 | pigeon_maps = reactpy.web.module_from_template("react", "pigeon-maps", fallback="⌛")
4 | Map, Marker = reactpy.web.export(pigeon_maps, ["Map", "Marker"])
5 |
6 |
7 | @reactpy.component
8 | def MapWithMarkers():
9 | marker_anchor, add_marker_anchor, remove_marker_anchor = use_set()
10 |
11 | markers = [
12 | Marker(
13 | {
14 | "anchor": anchor,
15 | "onClick": lambda event, a=anchor: remove_marker_anchor(a),
16 | },
17 | key=str(anchor),
18 | )
19 | for anchor in marker_anchor
20 | ]
21 |
22 | return Map(
23 | {
24 | "defaultCenter": (37.774, -122.419),
25 | "defaultZoom": 12,
26 | "height": "300px",
27 | "metaWheelZoom": True,
28 | "onClick": lambda event: add_marker_anchor(tuple(event["latLng"])),
29 | },
30 | markers,
31 | )
32 |
33 |
34 | def use_set(initial_value=None):
35 | values, set_values = reactpy.hooks.use_state(initial_value or set())
36 |
37 | def add_value(lat_lon):
38 | set_values(values.union({lat_lon}))
39 |
40 | def remove_value(lat_lon):
41 | set_values(values.difference({lat_lon}))
42 |
43 | return values, add_value, remove_value
44 |
45 |
46 | reactpy.run(MapWithMarkers)
47 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/simple_dashboard.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import random
3 | import time
4 |
5 | import reactpy
6 | from reactpy.widgets import Input
7 |
8 | victory = reactpy.web.module_from_template(
9 | "react",
10 | "victory-line",
11 | fallback="⌛",
12 | # not usually required (see issue #461 for more info)
13 | unmount_before_update=True,
14 | )
15 | VictoryLine = reactpy.web.export(victory, "VictoryLine")
16 |
17 |
18 | @reactpy.component
19 | def RandomWalk():
20 | mu = reactpy.hooks.use_ref(0)
21 | sigma = reactpy.hooks.use_ref(1)
22 |
23 | return reactpy.html.div(
24 | RandomWalkGraph(mu, sigma),
25 | reactpy.html.style(
26 | """
27 | .number-input-container {margin-bottom: 20px}
28 | .number-input-container input {width: 48%;float: left}
29 | .number-input-container input + input {margin-left: 4%}
30 | """
31 | ),
32 | NumberInput(
33 | "Mean",
34 | mu.current,
35 | mu.set_current,
36 | (-1, 1, 0.01),
37 | ),
38 | NumberInput(
39 | "Standard Deviation",
40 | sigma.current,
41 | sigma.set_current,
42 | (0, 1, 0.01),
43 | ),
44 | )
45 |
46 |
47 | @reactpy.component
48 | def RandomWalkGraph(mu, sigma):
49 | interval = use_interval(0.5)
50 | data, set_data = reactpy.hooks.use_state([{"x": 0, "y": 0}] * 50)
51 |
52 | @reactpy.hooks.use_async_effect
53 | async def animate():
54 | await interval
55 | last_data_point = data[-1]
56 | next_data_point = {
57 | "x": last_data_point["x"] + 1,
58 | "y": last_data_point["y"] + random.gauss(mu.current, sigma.current),
59 | }
60 | set_data(data[1:] + [next_data_point])
61 |
62 | return VictoryLine(
63 | {
64 | "data": data,
65 | "style": {
66 | "parent": {"width": "100%"},
67 | "data": {"stroke": "royalblue"},
68 | },
69 | }
70 | )
71 |
72 |
73 | @reactpy.component
74 | def NumberInput(label, value, set_value_callback, domain):
75 | minimum, maximum, step = domain
76 | attrs = {"min": minimum, "max": maximum, "step": step}
77 |
78 | value, set_value = reactpy.hooks.use_state(value)
79 |
80 | def update_value(value):
81 | set_value(value)
82 | set_value_callback(value)
83 |
84 | return reactpy.html.fieldset(
85 | {"class_name": "number-input-container"},
86 | reactpy.html.legend({"style": {"font-size": "medium"}}, label),
87 | Input(update_value, "number", value, attributes=attrs, cast=float),
88 | Input(update_value, "range", value, attributes=attrs, cast=float),
89 | )
90 |
91 |
92 | def use_interval(rate):
93 | usage_time = reactpy.hooks.use_ref(time.time())
94 |
95 | async def interval() -> None:
96 | await asyncio.sleep(rate - (time.time() - usage_time.current))
97 | usage_time.current = time.time()
98 |
99 | return asyncio.ensure_future(interval())
100 |
101 |
102 | reactpy.run(RandomWalk)
103 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/slideshow.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 |
3 |
4 | @reactpy.component
5 | def Slideshow():
6 | index, set_index = reactpy.hooks.use_state(0)
7 |
8 | def next_image(event):
9 | set_index(index + 1)
10 |
11 | return reactpy.html.img(
12 | {
13 | "src": f"https://picsum.photos/id/{index}/800/300",
14 | "style": {"cursor": "pointer"},
15 | "on_click": next_image,
16 | }
17 | )
18 |
19 |
20 | reactpy.run(Slideshow)
21 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/todo.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 |
3 |
4 | @reactpy.component
5 | def Todo():
6 | items, set_items = reactpy.hooks.use_state([])
7 |
8 | async def add_new_task(event):
9 | if event["key"] == "Enter":
10 | set_items([*items, event["target"]["value"]])
11 |
12 | tasks = []
13 |
14 | for index, text in enumerate(items):
15 |
16 | async def remove_task(event, index=index):
17 | set_items(items[:index] + items[index + 1 :])
18 |
19 | task_text = reactpy.html.td(reactpy.html.p(text))
20 | delete_button = reactpy.html.td(
21 | {"on_click": remove_task}, reactpy.html.button(["x"])
22 | )
23 | tasks.append(reactpy.html.tr(task_text, delete_button))
24 |
25 | task_input = reactpy.html.input({"on_key_down": add_new_task})
26 | task_table = reactpy.html.table(tasks)
27 |
28 | return reactpy.html.div(
29 | reactpy.html.p("press enter to add a task:"),
30 | task_input,
31 | task_table,
32 | )
33 |
34 |
35 | reactpy.run(Todo)
36 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/use_reducer_counter.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 |
3 |
4 | def reducer(count, action):
5 | if action == "increment":
6 | return count + 1
7 | elif action == "decrement":
8 | return count - 1
9 | elif action == "reset":
10 | return 0
11 | else:
12 | msg = f"Unknown action '{action}'"
13 | raise ValueError(msg)
14 |
15 |
16 | @reactpy.component
17 | def Counter():
18 | count, dispatch = reactpy.hooks.use_reducer(reducer, 0)
19 | return reactpy.html.div(
20 | f"Count: {count}",
21 | reactpy.html.button({"on_click": lambda event: dispatch("reset")}, "Reset"),
22 | reactpy.html.button({"on_click": lambda event: dispatch("increment")}, "+"),
23 | reactpy.html.button({"on_click": lambda event: dispatch("decrement")}, "-"),
24 | )
25 |
26 |
27 | reactpy.run(Counter)
28 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/use_state_counter.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 |
3 |
4 | def increment(last_count):
5 | return last_count + 1
6 |
7 |
8 | def decrement(last_count):
9 | return last_count - 1
10 |
11 |
12 | @reactpy.component
13 | def Counter():
14 | initial_count = 0
15 | count, set_count = reactpy.hooks.use_state(initial_count)
16 | return reactpy.html.div(
17 | f"Count: {count}",
18 | reactpy.html.button(
19 | {"on_click": lambda event: set_count(initial_count)}, "Reset"
20 | ),
21 | reactpy.html.button({"on_click": lambda event: set_count(increment)}, "+"),
22 | reactpy.html.button({"on_click": lambda event: set_count(decrement)}, "-"),
23 | )
24 |
25 |
26 | reactpy.run(Counter)
27 |
--------------------------------------------------------------------------------
/docs/source/reference/_examples/victory_chart.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 |
3 | victory = reactpy.web.module_from_template("react", "victory-bar", fallback="⌛")
4 | VictoryBar = reactpy.web.export(victory, "VictoryBar")
5 |
6 | bar_style = {"parent": {"width": "500px"}, "data": {"fill": "royalblue"}}
7 | reactpy.run(reactpy.component(lambda: VictoryBar({"style": bar_style})))
8 |
--------------------------------------------------------------------------------
/docs/source/reference/_static/vdom-json-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$ref": "#/definitions/element",
3 | "$schema": "http://json-schema.org/draft-07/schema",
4 | "definitions": {
5 | "element": {
6 | "dependentSchemas": {
7 | "error": {
8 | "properties": {
9 | "tagName": {
10 | "maxLength": 0
11 | }
12 | }
13 | }
14 | },
15 | "properties": {
16 | "attributes": {
17 | "type": "object"
18 | },
19 | "children": {
20 | "$ref": "#/definitions/elementChildren"
21 | },
22 | "error": {
23 | "type": "string"
24 | },
25 | "eventHandlers": {
26 | "$ref": "#/definitions/elementEventHandlers"
27 | },
28 | "importSource": {
29 | "$ref": "#/definitions/importSource"
30 | },
31 | "key": {
32 | "type": "string"
33 | },
34 | "tagName": {
35 | "type": "string"
36 | }
37 | },
38 | "required": ["tagName"],
39 | "type": "object"
40 | },
41 | "elementChildren": {
42 | "items": {
43 | "$ref": "#/definitions/elementOrString"
44 | },
45 | "type": "array"
46 | },
47 | "elementEventHandlers": {
48 | "patternProperties": {
49 | ".*": {
50 | "$ref": "#/definitions/eventHander"
51 | }
52 | },
53 | "type": "object"
54 | },
55 | "elementOrString": {
56 | "if": {
57 | "type": "object"
58 | },
59 | "then": {
60 | "$ref": "#/definitions/element"
61 | },
62 | "type": ["object", "string"]
63 | },
64 | "eventHandler": {
65 | "properties": {
66 | "preventDefault": {
67 | "type": "boolean"
68 | },
69 | "stopPropagation": {
70 | "type": "boolean"
71 | },
72 | "target": {
73 | "type": "string"
74 | }
75 | },
76 | "required": ["target"],
77 | "type": "object"
78 | },
79 | "importSource": {
80 | "properties": {
81 | "fallback": {
82 | "if": {
83 | "not": {
84 | "type": "null"
85 | }
86 | },
87 | "then": {
88 | "$ref": "#/definitions/elementOrString"
89 | },
90 | "type": ["object", "string", "null"]
91 | },
92 | "source": {
93 | "type": "string"
94 | },
95 | "sourceType": {
96 | "enum": ["URL", "NAME"]
97 | },
98 | "unmountBeforeUpdate": {
99 | "type": "boolean"
100 | }
101 | },
102 | "required": ["source"],
103 | "type": "object"
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/docs/source/reference/browser-events.rst:
--------------------------------------------------------------------------------
1 | .. _Browser Events:
2 |
3 | Browser Events 🚧
4 | =================
5 |
6 | The event types below are triggered by an event in the bubbling phase. To register an
7 | event handler for the capture phase, append Capture to the event name; for example,
8 | instead of using ``onClick``, you would use ``onClickCapture`` to handle the click event
9 | in the capture phase.
10 |
11 | .. note::
12 |
13 | Under construction 🚧
14 |
15 |
16 | Clipboard Events
17 | ----------------
18 |
19 | Composition Events
20 | ------------------
21 |
22 | Keyboard Events
23 | ---------------
24 |
25 | Focus Events
26 | ------------
27 |
28 | Form Events
29 | -----------
30 |
31 | Generic Events
32 | --------------
33 |
34 | Mouse Events
35 | ------------
36 |
37 | Pointer Events
38 | --------------
39 |
40 | Selection Events
41 | ----------------
42 |
43 | Touch Events
44 | ------------
45 |
46 | UI Events
47 | ---------
48 |
49 | Wheel Events
50 | ------------
51 |
52 | Media Events
53 | ------------
54 |
55 | Image Events
56 | ------------
57 |
58 | Animation Events
59 | ----------------
60 |
61 | Transition Events
62 | -----------------
63 |
64 | Other Events
65 | ------------
66 |
--------------------------------------------------------------------------------
/docs/source/reference/javascript-api.rst:
--------------------------------------------------------------------------------
1 | .. _Javascript API:
2 |
3 | Javascript API 🚧
4 | =================
5 |
6 | .. note::
7 |
8 | Under construction 🚧
9 |
--------------------------------------------------------------------------------
/src/build_scripts/clean_js_dir.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # requires-python = ">=3.11"
3 | # dependencies = []
4 | # ///
5 |
6 | # Deletes `dist`, `node_modules`, and `tsconfig.tsbuildinfo` from all JS packages in the JS source directory.
7 |
8 | import contextlib
9 | import glob
10 | import os
11 | import pathlib
12 | import shutil
13 |
14 | # Get the path to the JS source directory
15 | js_src_dir = pathlib.Path(__file__).parent.parent / "js"
16 |
17 | # Get the paths to all `dist` folders in the JS source directory
18 | dist_dirs = glob.glob(str(js_src_dir / "**/dist"), recursive=True)
19 |
20 | # Get the paths to all `node_modules` folders in the JS source directory
21 | node_modules_dirs = glob.glob(str(js_src_dir / "**/node_modules"), recursive=True)
22 |
23 | # Get the paths to all `tsconfig.tsbuildinfo` files in the JS source directory
24 | tsconfig_tsbuildinfo_files = glob.glob(
25 | str(js_src_dir / "**/tsconfig.tsbuildinfo"), recursive=True
26 | )
27 |
28 | # Delete all `dist` folders
29 | for dist_dir in dist_dirs:
30 | with contextlib.suppress(FileNotFoundError):
31 | shutil.rmtree(dist_dir)
32 |
33 | # Delete all `node_modules` folders
34 | for node_modules_dir in node_modules_dirs:
35 | with contextlib.suppress(FileNotFoundError):
36 | shutil.rmtree(node_modules_dir)
37 |
38 | # Delete all `tsconfig.tsbuildinfo` files
39 | for tsconfig_tsbuildinfo_file in tsconfig_tsbuildinfo_files:
40 | with contextlib.suppress(FileNotFoundError):
41 | os.remove(tsconfig_tsbuildinfo_file)
42 |
--------------------------------------------------------------------------------
/src/build_scripts/copy_dir.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # requires-python = ">=3.11"
3 | # dependencies = []
4 | # ///
5 |
6 | # ruff: noqa: INP001
7 | import logging
8 | import shutil
9 | import sys
10 | from pathlib import Path
11 |
12 |
13 | def copy_files(source: Path, destination: Path) -> None:
14 | if destination.exists():
15 | shutil.rmtree(destination)
16 | destination.mkdir()
17 |
18 | for file in source.iterdir():
19 | if file.is_file():
20 | shutil.copy(file, destination / file.name)
21 | else:
22 | copy_files(file, destination / file.name)
23 |
24 |
25 | if __name__ == "__main__":
26 | if len(sys.argv) != 3: # noqa
27 | logging.error(
28 | "Script used incorrectly!\nUsage: python copy_dir.py "
29 | )
30 | sys.exit(1)
31 |
32 | root_dir = Path(__file__).parent.parent.parent
33 | src = Path(root_dir / sys.argv[1])
34 | dest = Path(root_dir / sys.argv[2])
35 |
36 | if not src.exists():
37 | logging.error("Source directory %s does not exist", src)
38 | sys.exit(1)
39 |
40 | copy_files(src, dest)
41 |
--------------------------------------------------------------------------------
/src/js/.gitignore:
--------------------------------------------------------------------------------
1 | tsconfig.tsbuildinfo
2 | packages/**/package-lock.json
3 | **/dist/*
4 | node_modules
5 | *.tgz
6 |
--------------------------------------------------------------------------------
/src/js/README.md:
--------------------------------------------------------------------------------
1 | # ReactPy Client
2 |
3 | An ES6 Javascript client for ReactPy
4 |
--------------------------------------------------------------------------------
/src/js/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/src/js/bun.lockb
--------------------------------------------------------------------------------
/src/js/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import react from "eslint-plugin-react";
2 | import typescriptEslint from "@typescript-eslint/eslint-plugin";
3 | import globals from "globals";
4 | import tsParser from "@typescript-eslint/parser";
5 | import path from "node:path";
6 | import { fileURLToPath } from "node:url";
7 | import js from "@eslint/js";
8 | import { FlatCompat } from "@eslint/eslintrc";
9 |
10 | const __filename = fileURLToPath(import.meta.url);
11 | const __dirname = path.dirname(__filename);
12 | const compat = new FlatCompat({
13 | baseDirectory: __dirname,
14 | recommendedConfig: js.configs.recommended,
15 | allConfig: js.configs.all,
16 | });
17 |
18 | export default [
19 | ...compat.extends(
20 | "eslint:recommended",
21 | "plugin:react/recommended",
22 | "plugin:@typescript-eslint/recommended",
23 | ),
24 | {
25 | ignores: ["**/node_modules/", "**/dist/"],
26 | },
27 | {
28 | plugins: {
29 | react,
30 | "@typescript-eslint": typescriptEslint,
31 | },
32 |
33 | languageOptions: {
34 | globals: {
35 | ...globals.browser,
36 | ...globals.node,
37 | },
38 |
39 | parser: tsParser,
40 | ecmaVersion: "latest",
41 | sourceType: "module",
42 | },
43 |
44 | settings: {
45 | react: {
46 | version: "detect",
47 | },
48 | },
49 |
50 | rules: {
51 | "@typescript-eslint/ban-ts-comment": "off",
52 | "@typescript-eslint/no-explicit-any": "off",
53 | "@typescript-eslint/no-non-null-assertion": "off",
54 | "@typescript-eslint/no-empty-function": "off",
55 | "react/prop-types": "off",
56 | },
57 | },
58 | ];
59 |
--------------------------------------------------------------------------------
/src/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@eslint/eslintrc": "^3.2.0",
4 | "@eslint/js": "^9.18.0",
5 | "@typescript-eslint/eslint-plugin": "^8.21.0",
6 | "@typescript-eslint/parser": "^8.21.0",
7 | "eslint": "^9.18.0",
8 | "eslint-plugin-react": "^7.37.4",
9 | "globals": "^15.14.0",
10 | "prettier": "^3.4.2"
11 | },
12 | "license": "MIT",
13 | "scripts": {
14 | "lint": "prettier --check . && eslint",
15 | "format": "prettier --write . && eslint --fix"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/app/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/src/js/packages/@reactpy/app/bun.lockb
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/app/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import react from "eslint-plugin-react";
2 | import typescriptEslint from "@typescript-eslint/eslint-plugin";
3 | import globals from "globals";
4 | import tsParser from "@typescript-eslint/parser";
5 | import path from "node:path";
6 | import { fileURLToPath } from "node:url";
7 | import js from "@eslint/js";
8 | import { FlatCompat } from "@eslint/eslintrc";
9 |
10 | const __filename = fileURLToPath(import.meta.url);
11 | const __dirname = path.dirname(__filename);
12 | const compat = new FlatCompat({
13 | baseDirectory: __dirname,
14 | recommendedConfig: js.configs.recommended,
15 | allConfig: js.configs.all,
16 | });
17 |
18 | export default [
19 | ...compat.extends(
20 | "eslint:recommended",
21 | "plugin:react/recommended",
22 | "plugin:@typescript-eslint/recommended",
23 | ),
24 | {
25 | plugins: {
26 | react,
27 | "@typescript-eslint": typescriptEslint,
28 | },
29 |
30 | languageOptions: {
31 | globals: {
32 | ...globals.browser,
33 | },
34 |
35 | parser: tsParser,
36 | ecmaVersion: "latest",
37 | sourceType: "module",
38 | },
39 |
40 | rules: {},
41 | },
42 | ];
43 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@reactpy/app",
3 | "description": "ReactPy's client-side entry point. This is strictly for internal use and is not designed to be distributed.",
4 | "license": "MIT",
5 | "dependencies": {
6 | "@reactpy/client": "file:../client",
7 | "event-to-object": "file:../../event-to-object",
8 | "preact": "^10.25.4"
9 | },
10 | "devDependencies": {
11 | "typescript": "^5.7.3",
12 | "@pyscript/core": "^0.6",
13 | "morphdom": "^2"
14 | },
15 | "scripts": {
16 | "build": "bun build \"src/index.ts\" --outdir=\"../../../../reactpy/static/\" --minify --sourcemap=\"linked\"",
17 | "checkTypes": "tsc --noEmit"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/app/src/index.ts:
--------------------------------------------------------------------------------
1 | export { mountReactPy } from "@reactpy/client";
2 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"],
9 | "references": [
10 | {
11 | "path": "../client"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/client/README.md:
--------------------------------------------------------------------------------
1 | # @reactpy/client
2 |
3 | A client for ReactPy implemented in React
4 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/client/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/src/js/packages/@reactpy/client/bun.lockb
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@reactpy/client",
3 | "version": "1.0.0",
4 | "description": "A client for ReactPy implemented in React",
5 | "author": "Ryan Morshead",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/reactive-python/reactpy"
10 | },
11 | "keywords": [
12 | "react",
13 | "reactive",
14 | "python",
15 | "reactpy"
16 | ],
17 | "type": "module",
18 | "main": "dist/index.js",
19 | "dependencies": {
20 | "json-pointer": "^0.6.2",
21 | "preact": "^10.25.4"
22 | },
23 | "devDependencies": {
24 | "@types/json-pointer": "^1.0.34",
25 | "@types/react": "^17.0",
26 | "@types/react-dom": "^17.0",
27 | "typescript": "^5.7.3"
28 | },
29 | "peerDependencies": {
30 | "event-to-object": "<1.0.0"
31 | },
32 | "scripts": {
33 | "build": "tsc -b",
34 | "checkTypes": "tsc --noEmit"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/client/src/client.ts:
--------------------------------------------------------------------------------
1 | import logger from "./logger";
2 | import {
3 | ReactPyClientInterface,
4 | ReactPyModule,
5 | GenericReactPyClientProps,
6 | ReactPyUrls,
7 | } from "./types";
8 | import { createReconnectingWebSocket } from "./websocket";
9 |
10 | export abstract class BaseReactPyClient implements ReactPyClientInterface {
11 | private readonly handlers: { [key: string]: ((message: any) => void)[] } = {};
12 | protected readonly ready: Promise;
13 | private resolveReady: (value: undefined) => void;
14 |
15 | constructor() {
16 | this.resolveReady = () => {};
17 | this.ready = new Promise((resolve) => (this.resolveReady = resolve));
18 | }
19 |
20 | onMessage(type: string, handler: (message: any) => void): () => void {
21 | (this.handlers[type] || (this.handlers[type] = [])).push(handler);
22 | this.resolveReady(undefined);
23 | return () => {
24 | this.handlers[type] = this.handlers[type].filter((h) => h !== handler);
25 | };
26 | }
27 |
28 | abstract sendMessage(message: any): void;
29 | abstract loadModule(moduleName: string): Promise;
30 |
31 | /**
32 | * Handle an incoming message.
33 | *
34 | * This should be called by subclasses when a message is received.
35 | *
36 | * @param message The message to handle. The message must have a `type` property.
37 | */
38 | protected handleIncoming(message: any): void {
39 | if (!message.type) {
40 | logger.warn("Received message without type", message);
41 | return;
42 | }
43 |
44 | const messageHandlers: ((m: any) => void)[] | undefined =
45 | this.handlers[message.type];
46 | if (!messageHandlers) {
47 | logger.warn("Received message without handler", message);
48 | return;
49 | }
50 |
51 | messageHandlers.forEach((h) => h(message));
52 | }
53 | }
54 |
55 | export class ReactPyClient
56 | extends BaseReactPyClient
57 | implements ReactPyClientInterface
58 | {
59 | urls: ReactPyUrls;
60 | socket: { current?: WebSocket };
61 | mountElement: HTMLElement;
62 |
63 | constructor(props: GenericReactPyClientProps) {
64 | super();
65 |
66 | this.urls = props.urls;
67 | this.mountElement = props.mountElement;
68 | this.socket = createReconnectingWebSocket({
69 | url: this.urls.componentUrl,
70 | readyPromise: this.ready,
71 | ...props.reconnectOptions,
72 | onMessage: (event) => this.handleIncoming(JSON.parse(event.data)),
73 | });
74 | }
75 |
76 | sendMessage(message: any): void {
77 | this.socket.current?.send(JSON.stringify(message));
78 | }
79 |
80 | loadModule(moduleName: string): Promise {
81 | return import(`${this.urls.jsModulesPath}${moduleName}`);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/client/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./client";
2 | export * from "./components";
3 | export * from "./mount";
4 | export * from "./types";
5 | export * from "./vdom";
6 | export * from "./websocket";
7 | export { default as React } from "preact/compat";
8 | export { default as ReactDOM } from "preact/compat";
9 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/client/src/logger.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | log: (...args: any[]): void => console.log("[ReactPy]", ...args),
3 | info: (...args: any[]): void => console.info("[ReactPy]", ...args),
4 | warn: (...args: any[]): void => console.warn("[ReactPy]", ...args),
5 | error: (...args: any[]): void => console.error("[ReactPy]", ...args),
6 | };
7 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/client/src/mount.tsx:
--------------------------------------------------------------------------------
1 | import { default as React, default as ReactDOM } from "preact/compat";
2 | import { ReactPyClient } from "./client";
3 | import { Layout } from "./components";
4 | import { MountProps } from "./types";
5 |
6 | export function mountReactPy(props: MountProps) {
7 | // WebSocket route for component rendering
8 | const wsProtocol = `ws${window.location.protocol === "https:" ? "s" : ""}:`;
9 | const wsOrigin = `${wsProtocol}//${window.location.host}`;
10 | const componentUrl = new URL(
11 | `${wsOrigin}${props.pathPrefix}${props.componentPath || ""}`,
12 | );
13 |
14 | // Embed the initial HTTP path into the WebSocket URL
15 | componentUrl.searchParams.append("http_pathname", window.location.pathname);
16 | if (window.location.search) {
17 | componentUrl.searchParams.append(
18 | "http_query_string",
19 | window.location.search,
20 | );
21 | }
22 |
23 | // Configure a new ReactPy client
24 | const client = new ReactPyClient({
25 | urls: {
26 | componentUrl: componentUrl,
27 | jsModulesPath: `${window.location.origin}${props.pathPrefix}modules/`,
28 | },
29 | reconnectOptions: {
30 | interval: props.reconnectInterval || 750,
31 | maxInterval: props.reconnectMaxInterval || 60000,
32 | maxRetries: props.reconnectMaxRetries || 150,
33 | backoffMultiplier: props.reconnectBackoffMultiplier || 1.25,
34 | },
35 | mountElement: props.mountElement,
36 | });
37 |
38 | // Start rendering the component
39 | // eslint-disable-next-line react/no-deprecated
40 | ReactDOM.render( , props.mountElement);
41 | }
42 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/client/src/websocket.ts:
--------------------------------------------------------------------------------
1 | import { CreateReconnectingWebSocketProps } from "./types";
2 | import log from "./logger";
3 |
4 | export function createReconnectingWebSocket(
5 | props: CreateReconnectingWebSocketProps,
6 | ) {
7 | const { interval, maxInterval, maxRetries, backoffMultiplier } = props;
8 | let retries = 0;
9 | let currentInterval = interval;
10 | let everConnected = false;
11 | const closed = false;
12 | const socket: { current?: WebSocket } = {};
13 |
14 | const connect = () => {
15 | if (closed) {
16 | return;
17 | }
18 | socket.current = new WebSocket(props.url);
19 | socket.current.onopen = () => {
20 | everConnected = true;
21 | log.info("Connected!");
22 | currentInterval = interval;
23 | retries = 0;
24 | if (props.onOpen) {
25 | props.onOpen();
26 | }
27 | };
28 | socket.current.onmessage = (event) => {
29 | if (props.onMessage) {
30 | props.onMessage(event);
31 | }
32 | };
33 | socket.current.onclose = () => {
34 | if (props.onClose) {
35 | props.onClose();
36 | }
37 | if (!everConnected) {
38 | log.info("Failed to connect!");
39 | return;
40 | }
41 | log.info("Disconnected!");
42 | if (retries >= maxRetries) {
43 | log.info("Connection max retries exhausted!");
44 | return;
45 | }
46 | log.info(
47 | `Reconnecting in ${(currentInterval / 1000).toPrecision(4)} seconds...`,
48 | );
49 | setTimeout(connect, currentInterval);
50 | currentInterval = nextInterval(
51 | currentInterval,
52 | backoffMultiplier,
53 | maxInterval,
54 | );
55 | retries++;
56 | };
57 | };
58 |
59 | props.readyPromise.then(() => log.info("Starting client...")).then(connect);
60 |
61 | return socket;
62 | }
63 |
64 | export function nextInterval(
65 | currentInterval: number,
66 | backoffMultiplier: number,
67 | maxInterval: number,
68 | ): number {
69 | return Math.min(
70 | // increase interval by backoff multiplier
71 | currentInterval * backoffMultiplier,
72 | // don't exceed max interval
73 | maxInterval,
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/src/js/packages/@reactpy/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"],
9 | "references": [
10 | {
11 | "path": "../../event-to-object"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/js/packages/event-to-object/README.md:
--------------------------------------------------------------------------------
1 | # Event to Object
2 |
3 | Converts a JavaScript events to JSON serializable objects.
4 |
--------------------------------------------------------------------------------
/src/js/packages/event-to-object/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/src/js/packages/event-to-object/bun.lockb
--------------------------------------------------------------------------------
/src/js/packages/event-to-object/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "event-to-object",
3 | "version": "0.1.2",
4 | "description": "Converts a JavaScript events to JSON serializable objects.",
5 | "author": "Ryan Morshead",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/reactive-python/reactpy"
10 | },
11 | "keywords": [
12 | "event",
13 | "json",
14 | "object",
15 | "convert"
16 | ],
17 | "type": "module",
18 | "main": "dist/index.js",
19 | "dependencies": {
20 | "json-pointer": "^0.6.2"
21 | },
22 | "devDependencies": {
23 | "happy-dom": "^8.9.0",
24 | "lodash": "^4.17.21",
25 | "tsm": "^2.3.0",
26 | "typescript": "^5.7.3",
27 | "uvu": "^0.5.6"
28 | },
29 | "scripts": {
30 | "build": "tsc -b",
31 | "checkTypes": "tsc --noEmit",
32 | "test": "uvu -r tsm tests"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/js/packages/event-to-object/tests/tooling/check.ts:
--------------------------------------------------------------------------------
1 | import * as assert from "uvu/assert";
2 | import { Event } from "happy-dom";
3 | // @ts-ignore
4 | import lodash from "lodash";
5 | import convert from "../../src/index";
6 |
7 | export function checkEventConversion(
8 | givenEvent: Event,
9 | expectedConversion: any,
10 | ): void {
11 | const actualSerializedEvent = convert(
12 | // @ts-ignore
13 | givenEvent,
14 | );
15 |
16 | if (!actualSerializedEvent) {
17 | assert.equal(actualSerializedEvent, expectedConversion);
18 | return;
19 | }
20 |
21 | // too hard to compare
22 | assert.equal(typeof actualSerializedEvent.timeStamp, "number");
23 |
24 | assert.equal(
25 | actualSerializedEvent,
26 | lodash.merge(
27 | { timeStamp: actualSerializedEvent.timeStamp, type: givenEvent.type },
28 | expectedConversionDefaults,
29 | expectedConversion,
30 | ),
31 | );
32 |
33 | // verify result is JSON serializable
34 | JSON.stringify(actualSerializedEvent);
35 | }
36 |
37 | const expectedConversionDefaults = {
38 | target: null,
39 | currentTarget: null,
40 | bubbles: false,
41 | composed: false,
42 | defaultPrevented: false,
43 | eventPhase: undefined,
44 | isTrusted: undefined,
45 | selection: null,
46 | };
47 |
--------------------------------------------------------------------------------
/src/js/packages/event-to-object/tests/tooling/mock.ts:
--------------------------------------------------------------------------------
1 | export const mockBoundingRect = {
2 | left: 0,
3 | top: 0,
4 | right: 0,
5 | bottom: 0,
6 | x: 0,
7 | y: 0,
8 | height: 0,
9 | width: 0,
10 | };
11 |
12 | export const mockElementObject = {
13 | tagName: null,
14 | boundingClientRect: mockBoundingRect,
15 | };
16 |
17 | export const mockElement = {
18 | tagName: null,
19 | getBoundingClientRect: () => mockBoundingRect,
20 | };
21 |
22 | export const mockGamepad = {
23 | id: "test",
24 | index: 0,
25 | connected: true,
26 | mapping: "standard",
27 | axes: [],
28 | buttons: [
29 | {
30 | pressed: false,
31 | touched: false,
32 | value: 0,
33 | },
34 | ],
35 | timestamp: undefined,
36 | };
37 |
38 | export const mockTouch = {
39 | identifier: 0,
40 | pageX: 0,
41 | pageY: 0,
42 | screenX: 0,
43 | screenY: 0,
44 | clientX: 0,
45 | clientY: 0,
46 | force: 0,
47 | radiusX: 0,
48 | radiusY: 0,
49 | rotationAngle: 0,
50 | target: mockElement,
51 | };
52 |
53 | export const mockTouchObject = {
54 | ...mockTouch,
55 | target: mockElementObject,
56 | };
57 |
--------------------------------------------------------------------------------
/src/js/packages/event-to-object/tests/tooling/setup.js:
--------------------------------------------------------------------------------
1 | import { test } from "uvu";
2 | import { Window } from "happy-dom";
3 |
4 | export const window = new Window();
5 |
6 | export function setup() {
7 | global.window = window;
8 | global.document = window.document;
9 | global.navigator = window.navigator;
10 | global.getComputedStyle = window.getComputedStyle;
11 | global.requestAnimationFrame = null;
12 | }
13 |
14 | export function reset() {
15 | window.document.title = "";
16 | window.document.head.innerHTML = "";
17 | window.document.body.innerHTML = " ";
18 | window.getSelection().removeAllRanges();
19 | }
20 |
21 | test.before(setup);
22 | test.before.each(reset);
23 |
--------------------------------------------------------------------------------
/src/js/packages/event-to-object/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"]
9 | }
10 |
--------------------------------------------------------------------------------
/src/js/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": false,
4 | "allowSyntheticDefaultImports": true,
5 | "declaration": true,
6 | "declarationMap": true,
7 | "esModuleInterop": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "isolatedModules": true,
10 | "jsx": "react",
11 | "lib": ["DOM", "DOM.Iterable", "esnext"],
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "noEmitOnError": true,
15 | "noUnusedLocals": true,
16 | "resolveJsonModule": true,
17 | "skipLibCheck": false,
18 | "sourceMap": true,
19 | "strict": true,
20 | "target": "esnext"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/reactpy/__init__.py:
--------------------------------------------------------------------------------
1 | from reactpy import config, logging, types, web, widgets
2 | from reactpy._html import html
3 | from reactpy.core import hooks
4 | from reactpy.core.component import component
5 | from reactpy.core.events import event
6 | from reactpy.core.hooks import (
7 | create_context,
8 | use_async_effect,
9 | use_callback,
10 | use_connection,
11 | use_context,
12 | use_debug_value,
13 | use_effect,
14 | use_location,
15 | use_memo,
16 | use_reducer,
17 | use_ref,
18 | use_scope,
19 | use_state,
20 | )
21 | from reactpy.core.layout import Layout
22 | from reactpy.core.vdom import Vdom
23 | from reactpy.pyscript.components import pyscript_component
24 | from reactpy.utils import Ref, reactpy_to_string, string_to_reactpy
25 |
26 | __author__ = "The Reactive Python Team"
27 | __version__ = "2.0.0b2"
28 |
29 | __all__ = [
30 | "Layout",
31 | "Ref",
32 | "Vdom",
33 | "component",
34 | "config",
35 | "create_context",
36 | "event",
37 | "hooks",
38 | "html",
39 | "logging",
40 | "pyscript_component",
41 | "reactpy_to_string",
42 | "string_to_reactpy",
43 | "types",
44 | "use_async_effect",
45 | "use_callback",
46 | "use_connection",
47 | "use_context",
48 | "use_debug_value",
49 | "use_effect",
50 | "use_location",
51 | "use_memo",
52 | "use_reducer",
53 | "use_ref",
54 | "use_scope",
55 | "use_state",
56 | "web",
57 | "widgets",
58 | ]
59 |
--------------------------------------------------------------------------------
/src/reactpy/_console/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/src/reactpy/_console/__init__.py
--------------------------------------------------------------------------------
/src/reactpy/_console/cli.py:
--------------------------------------------------------------------------------
1 | """Entry point for the ReactPy CLI."""
2 |
3 | import click
4 |
5 | import reactpy
6 | from reactpy._console.rewrite_props import rewrite_props
7 |
8 |
9 | @click.group()
10 | @click.version_option(version=reactpy.__version__, prog_name=reactpy.__name__)
11 | def entry_point() -> None:
12 | pass
13 |
14 |
15 | entry_point.add_command(rewrite_props)
16 |
17 |
18 | if __name__ == "__main__":
19 | entry_point()
20 |
--------------------------------------------------------------------------------
/src/reactpy/_warnings.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterator
2 | from functools import wraps
3 | from inspect import currentframe
4 | from types import FrameType
5 | from typing import TYPE_CHECKING, Any, cast
6 | from warnings import warn as _warn
7 |
8 |
9 | @wraps(_warn)
10 | def warn(*args: Any, **kwargs: Any) -> Any:
11 | # warn at call site outside of ReactPy
12 | _warn(*args, stacklevel=_frame_depth_in_module() + 1, **kwargs) # type: ignore
13 |
14 |
15 | if TYPE_CHECKING:
16 | warn = cast(Any, _warn)
17 |
18 |
19 | def _frame_depth_in_module() -> int:
20 | depth = 0
21 | for frame in _iter_frames(2):
22 | module_name = frame.f_globals.get("__name__")
23 | if not module_name or not module_name.startswith("reactpy."):
24 | break
25 | depth += 1
26 | return depth
27 |
28 |
29 | def _iter_frames(index: int = 1) -> Iterator[FrameType]:
30 | frame = currentframe()
31 | while frame is not None:
32 | if index == 0:
33 | yield frame
34 | else:
35 | index -= 1
36 | frame = frame.f_back
37 |
--------------------------------------------------------------------------------
/src/reactpy/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/src/reactpy/core/__init__.py
--------------------------------------------------------------------------------
/src/reactpy/core/_f_back.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import inspect
4 | from types import FrameType
5 |
6 |
7 | def f_module_name(index: int = 0) -> str:
8 | frame = f_back(index + 1)
9 | if frame is None:
10 | return "" # nocov
11 | name = frame.f_globals.get("__name__", "")
12 | if not isinstance(name, str):
13 | raise TypeError("Expected module name to be a string") # nocov
14 | return name
15 |
16 |
17 | def f_back(index: int = 0) -> FrameType | None:
18 | frame = inspect.currentframe()
19 | while frame is not None:
20 | if index < 0:
21 | return frame
22 | frame = frame.f_back
23 | index -= 1
24 | return None # nocov
25 |
--------------------------------------------------------------------------------
/src/reactpy/core/_thread_local.py:
--------------------------------------------------------------------------------
1 | from threading import Thread, current_thread
2 | from typing import Callable, Generic, TypeVar
3 | from weakref import WeakKeyDictionary
4 |
5 | _StateType = TypeVar("_StateType")
6 |
7 |
8 | class ThreadLocal(Generic[_StateType]): # nocov
9 | """Utility for managing per-thread state information. This is only used in
10 | environments where ContextVars are not available, such as the `pyodide`
11 | executor."""
12 |
13 | def __init__(self, default: Callable[[], _StateType]):
14 | self._default = default
15 | self._state: WeakKeyDictionary[Thread, _StateType] = WeakKeyDictionary()
16 |
17 | def get(self) -> _StateType:
18 | thread = current_thread()
19 | if thread not in self._state:
20 | state = self._state[thread] = self._default()
21 | else:
22 | state = self._state[thread]
23 | return state
24 |
--------------------------------------------------------------------------------
/src/reactpy/core/component.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import inspect
4 | from functools import wraps
5 | from typing import Any, Callable
6 |
7 | from reactpy.types import ComponentType, VdomDict
8 |
9 |
10 | def component(
11 | function: Callable[..., ComponentType | VdomDict | str | None],
12 | ) -> Callable[..., Component]:
13 | """A decorator for defining a new component.
14 |
15 | Parameters:
16 | function: The component's :meth:`reactpy.core.proto.ComponentType.render` function.
17 | """
18 | sig = inspect.signature(function)
19 |
20 | if "key" in sig.parameters and sig.parameters["key"].kind in (
21 | inspect.Parameter.KEYWORD_ONLY,
22 | inspect.Parameter.POSITIONAL_OR_KEYWORD,
23 | ):
24 | msg = f"Component render function {function} uses reserved parameter 'key'"
25 | raise TypeError(msg)
26 |
27 | @wraps(function)
28 | def constructor(*args: Any, key: Any | None = None, **kwargs: Any) -> Component:
29 | return Component(function, key, args, kwargs, sig)
30 |
31 | return constructor
32 |
33 |
34 | class Component:
35 | """An object for rending component models."""
36 |
37 | __slots__ = "__weakref__", "_func", "_args", "_kwargs", "_sig", "key", "type"
38 |
39 | def __init__(
40 | self,
41 | function: Callable[..., ComponentType | VdomDict | str | None],
42 | key: Any | None,
43 | args: tuple[Any, ...],
44 | kwargs: dict[str, Any],
45 | sig: inspect.Signature,
46 | ) -> None:
47 | self.key = key
48 | self.type = function
49 | self._args = args
50 | self._kwargs = kwargs
51 | self._sig = sig
52 |
53 | def render(self) -> ComponentType | VdomDict | str | None:
54 | return self.type(*self._args, **self._kwargs)
55 |
56 | def __repr__(self) -> str:
57 | try:
58 | args = self._sig.bind(*self._args, **self._kwargs).arguments
59 | except TypeError:
60 | return f"{self.type.__name__}(...)"
61 | else:
62 | items = ", ".join(f"{k}={v!r}" for k, v in args.items())
63 | if items:
64 | return f"{self.type.__name__}({id(self):02x}, {items})"
65 | else:
66 | return f"{self.type.__name__}({id(self):02x})"
67 |
--------------------------------------------------------------------------------
/src/reactpy/executors/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/src/reactpy/executors/__init__.py
--------------------------------------------------------------------------------
/src/reactpy/executors/asgi/__init__.py:
--------------------------------------------------------------------------------
1 | from reactpy.executors.asgi.middleware import ReactPyMiddleware
2 | from reactpy.executors.asgi.pyscript import ReactPyPyscript
3 | from reactpy.executors.asgi.standalone import ReactPy
4 |
5 | __all__ = ["ReactPy", "ReactPyMiddleware", "ReactPyPyscript"]
6 |
--------------------------------------------------------------------------------
/src/reactpy/executors/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | from collections.abc import Iterable
5 | from typing import Any
6 |
7 | from reactpy._option import Option
8 | from reactpy.config import (
9 | REACTPY_PATH_PREFIX,
10 | REACTPY_RECONNECT_BACKOFF_MULTIPLIER,
11 | REACTPY_RECONNECT_INTERVAL,
12 | REACTPY_RECONNECT_MAX_INTERVAL,
13 | REACTPY_RECONNECT_MAX_RETRIES,
14 | )
15 | from reactpy.types import ReactPyConfig, VdomDict
16 | from reactpy.utils import import_dotted_path, reactpy_to_string
17 |
18 | logger = logging.getLogger(__name__)
19 |
20 |
21 | def import_components(dotted_paths: Iterable[str]) -> dict[str, Any]:
22 | """Imports a list of dotted paths and returns the callables."""
23 | return {
24 | dotted_path: import_dotted_path(dotted_path) for dotted_path in dotted_paths
25 | }
26 |
27 |
28 | def check_path(url_path: str) -> str: # nocov
29 | """Check that a path is valid URL path."""
30 | if not url_path:
31 | return "URL path must not be empty."
32 | if not isinstance(url_path, str):
33 | return "URL path is must be a string."
34 | if not url_path.startswith("/"):
35 | return "URL path must start with a forward slash."
36 | if not url_path.endswith("/"):
37 | return "URL path must end with a forward slash."
38 |
39 | return ""
40 |
41 |
42 | def vdom_head_to_html(head: VdomDict) -> str:
43 | if isinstance(head, dict) and head.get("tagName") == "head":
44 | return reactpy_to_string(head)
45 |
46 | raise ValueError(
47 | "Invalid head element! Element must be either `html.head` or a string."
48 | )
49 |
50 |
51 | def process_settings(settings: ReactPyConfig) -> None:
52 | """Process the settings and return the final configuration."""
53 | from reactpy import config
54 |
55 | for setting in settings:
56 | config_name = f"REACTPY_{setting.upper()}"
57 | config_object: Option[Any] | None = getattr(config, config_name, None)
58 | if config_object:
59 | config_object.set_current(settings[setting]) # type: ignore
60 | else:
61 | raise ValueError(f'Unknown ReactPy setting "{setting}".')
62 |
63 |
64 | def server_side_component_html(
65 | element_id: str, class_: str, component_path: str
66 | ) -> str:
67 | return (
68 | f'
'
69 | '"
81 | )
82 |
--------------------------------------------------------------------------------
/src/reactpy/logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 | from logging.config import dictConfig
4 |
5 | from reactpy.config import REACTPY_DEBUG
6 |
7 | dictConfig(
8 | {
9 | "version": 1,
10 | "disable_existing_loggers": False,
11 | "loggers": {
12 | "reactpy": {"handlers": ["console"]},
13 | },
14 | "handlers": {
15 | "console": {
16 | "class": "logging.StreamHandler",
17 | "formatter": "generic",
18 | "stream": sys.stdout,
19 | }
20 | },
21 | "formatters": {"generic": {"datefmt": r"%Y-%m-%dT%H:%M:%S%z"}},
22 | }
23 | )
24 |
25 |
26 | ROOT_LOGGER = logging.getLogger("reactpy")
27 | """ReactPy's root logger instance"""
28 |
29 |
30 | @REACTPY_DEBUG.subscribe
31 | def _set_debug_level(debug: bool) -> None:
32 | if debug:
33 | ROOT_LOGGER.setLevel("DEBUG")
34 | ROOT_LOGGER.debug("ReactPy is in debug mode")
35 | else:
36 | ROOT_LOGGER.setLevel("INFO")
37 |
--------------------------------------------------------------------------------
/src/reactpy/py.typed:
--------------------------------------------------------------------------------
1 | # Marker file for PEP 561
2 |
--------------------------------------------------------------------------------
/src/reactpy/pyscript/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/src/reactpy/pyscript/__init__.py
--------------------------------------------------------------------------------
/src/reactpy/pyscript/component_template.py:
--------------------------------------------------------------------------------
1 | # ruff: noqa: TC004, N802, N816, RUF006
2 | # type: ignore
3 | from typing import TYPE_CHECKING
4 |
5 | if TYPE_CHECKING:
6 | import asyncio
7 |
8 | from reactpy.pyscript.layout_handler import ReactPyLayoutHandler
9 |
10 |
11 | # User component is inserted below by regex replacement
12 | def user_workspace_UUID():
13 | """Encapsulate the user's code with a completely unique function (workspace)
14 | to prevent overlapping imports and variable names between different components.
15 |
16 | This code is designed to be run directly by PyScript, and is not intended to be run
17 | in a normal Python environment.
18 |
19 | ReactPy-Django performs string substitutions to turn this file into valid PyScript.
20 | """
21 |
22 | def root(): ...
23 |
24 | return root()
25 |
26 |
27 | # Create a task to run the user's component workspace
28 | task_UUID = asyncio.create_task(ReactPyLayoutHandler("UUID").run(user_workspace_UUID))
29 |
--------------------------------------------------------------------------------
/src/reactpy/pyscript/components.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 | from typing import TYPE_CHECKING
5 |
6 | from reactpy import component, hooks
7 | from reactpy.pyscript.utils import pyscript_component_html
8 | from reactpy.types import ComponentType, Key
9 | from reactpy.utils import string_to_reactpy
10 |
11 | if TYPE_CHECKING:
12 | from reactpy.types import VdomDict
13 |
14 |
15 | @component
16 | def _pyscript_component(
17 | *file_paths: str | Path,
18 | initial: str | VdomDict = "",
19 | root: str = "root",
20 | ) -> None | VdomDict:
21 | if not file_paths:
22 | raise ValueError("At least one file path must be provided.")
23 |
24 | rendered, set_rendered = hooks.use_state(False)
25 | initial = string_to_reactpy(initial) if isinstance(initial, str) else initial
26 |
27 | if not rendered:
28 | # FIXME: This is needed to properly re-render PyScript during a WebSocket
29 | # disconnection / reconnection. There may be a better way to do this in the future.
30 | set_rendered(True)
31 | return None
32 |
33 | component_vdom = string_to_reactpy(
34 | pyscript_component_html(tuple(str(fp) for fp in file_paths), initial, root)
35 | )
36 | component_vdom["tagName"] = ""
37 | return component_vdom
38 |
39 |
40 | def pyscript_component(
41 | *file_paths: str | Path,
42 | initial: str | VdomDict | ComponentType = "",
43 | root: str = "root",
44 | key: Key | None = None,
45 | ) -> ComponentType:
46 | """
47 | Args:
48 | file_paths: File path to your client-side ReactPy component. If multiple paths are \
49 | provided, the contents are automatically merged.
50 |
51 | Kwargs:
52 | initial: The initial HTML that is displayed prior to the PyScript component \
53 | loads. This can either be a string containing raw HTML, a \
54 | `#!python reactpy.html` snippet, or a non-interactive component.
55 | root: The name of the root component function.
56 | """
57 | return _pyscript_component(
58 | *file_paths,
59 | initial=initial,
60 | root=root,
61 | key=key,
62 | )
63 |
--------------------------------------------------------------------------------
/src/reactpy/static/pyscript-hide-debug.css:
--------------------------------------------------------------------------------
1 | .py-error {
2 | display: none;
3 | }
4 |
--------------------------------------------------------------------------------
/src/reactpy/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 | from reactpy.templatetags.jinja import Jinja
2 |
3 | __all__ = ["Jinja"]
4 |
--------------------------------------------------------------------------------
/src/reactpy/templatetags/jinja.py:
--------------------------------------------------------------------------------
1 | from typing import ClassVar
2 | from uuid import uuid4
3 |
4 | from jinja2_simple_tags import StandaloneTag
5 |
6 | from reactpy.executors.utils import server_side_component_html
7 | from reactpy.pyscript.utils import pyscript_component_html, pyscript_setup_html
8 |
9 |
10 | class Jinja(StandaloneTag): # type: ignore
11 | safe_output = True
12 | tags: ClassVar[set[str]] = {"component", "pyscript_component", "pyscript_setup"}
13 |
14 | def render(self, *args: str, **kwargs: str) -> str:
15 | if self.tag_name == "component":
16 | return component(*args, **kwargs)
17 |
18 | if self.tag_name == "pyscript_component":
19 | return pyscript_component(*args, **kwargs)
20 |
21 | if self.tag_name == "pyscript_setup":
22 | return pyscript_setup(*args, **kwargs)
23 |
24 | # This should never happen, but we validate it for safety.
25 | raise ValueError(f"Unknown tag: {self.tag_name}") # nocov
26 |
27 |
28 | def component(dotted_path: str, **kwargs: str) -> str:
29 | class_ = kwargs.pop("class", "")
30 | if kwargs:
31 | raise ValueError(f"Unexpected keyword arguments: {', '.join(kwargs)}")
32 | return server_side_component_html(
33 | element_id=uuid4().hex, class_=class_, component_path=f"{dotted_path}/"
34 | )
35 |
36 |
37 | def pyscript_component(*file_paths: str, initial: str = "", root: str = "root") -> str:
38 | return pyscript_component_html(file_paths=file_paths, initial=initial, root=root)
39 |
40 |
41 | def pyscript_setup(*extra_py: str, extra_js: str = "", config: str = "") -> str:
42 | return pyscript_setup_html(extra_py=extra_py, extra_js=extra_js, config=config)
43 |
--------------------------------------------------------------------------------
/src/reactpy/testing/__init__.py:
--------------------------------------------------------------------------------
1 | from reactpy.testing.backend import BackendFixture
2 | from reactpy.testing.common import (
3 | HookCatcher,
4 | StaticEventHandler,
5 | clear_reactpy_web_modules_dir,
6 | poll,
7 | )
8 | from reactpy.testing.display import DisplayFixture
9 | from reactpy.testing.logs import (
10 | LogAssertionError,
11 | assert_reactpy_did_log,
12 | assert_reactpy_did_not_log,
13 | capture_reactpy_logs,
14 | )
15 |
16 | __all__ = [
17 | "BackendFixture",
18 | "DisplayFixture",
19 | "HookCatcher",
20 | "LogAssertionError",
21 | "StaticEventHandler",
22 | "assert_reactpy_did_log",
23 | "assert_reactpy_did_not_log",
24 | "capture_reactpy_logs",
25 | "clear_reactpy_web_modules_dir",
26 | "poll",
27 | ]
28 |
--------------------------------------------------------------------------------
/src/reactpy/testing/display.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from contextlib import AsyncExitStack
4 | from types import TracebackType
5 | from typing import Any
6 |
7 | from playwright.async_api import (
8 | Browser,
9 | BrowserContext,
10 | Page,
11 | async_playwright,
12 | )
13 |
14 | from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT
15 | from reactpy.testing.backend import BackendFixture
16 | from reactpy.types import RootComponentConstructor
17 |
18 |
19 | class DisplayFixture:
20 | """A fixture for running web-based tests using ``playwright``"""
21 |
22 | _exit_stack: AsyncExitStack
23 |
24 | def __init__(
25 | self,
26 | backend: BackendFixture | None = None,
27 | driver: Browser | BrowserContext | Page | None = None,
28 | ) -> None:
29 | if backend is not None:
30 | self.backend = backend
31 | if driver is not None:
32 | if isinstance(driver, Page):
33 | self.page = driver
34 | else:
35 | self._browser = driver
36 |
37 | async def show(
38 | self,
39 | component: RootComponentConstructor,
40 | ) -> None:
41 | self.backend.mount(component)
42 | await self.goto("/")
43 |
44 | async def goto(self, path: str, query: Any | None = None) -> None:
45 | await self.page.goto(self.backend.url(path, query))
46 |
47 | async def __aenter__(self) -> DisplayFixture:
48 | es = self._exit_stack = AsyncExitStack()
49 |
50 | browser: Browser | BrowserContext
51 | if not hasattr(self, "page"):
52 | if not hasattr(self, "_browser"):
53 | pw = await es.enter_async_context(async_playwright())
54 | browser = await pw.chromium.launch()
55 | else:
56 | browser = self._browser
57 | self.page = await browser.new_page()
58 |
59 | self.page.set_default_timeout(REACTPY_TESTS_DEFAULT_TIMEOUT.current * 1000)
60 |
61 | if not hasattr(self, "backend"): # nocov
62 | self.backend = BackendFixture()
63 | await es.enter_async_context(self.backend)
64 |
65 | return self
66 |
67 | async def __aexit__(
68 | self,
69 | exc_type: type[BaseException] | None,
70 | exc_value: BaseException | None,
71 | traceback: TracebackType | None,
72 | ) -> None:
73 | self.backend.mount(None)
74 | await self._exit_stack.aclose()
75 |
--------------------------------------------------------------------------------
/src/reactpy/testing/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import socket
4 | import sys
5 | from contextlib import closing
6 |
7 |
8 | def find_available_port(
9 | host: str, port_min: int = 8000, port_max: int = 9000
10 | ) -> int: # nocov
11 | """Get a port that's available for the given host and port range"""
12 | for port in range(port_min, port_max):
13 | with closing(socket.socket()) as sock:
14 | try:
15 | if sys.platform in ("linux", "darwin"):
16 | # Fixes bug on Unix-like systems where every time you restart the
17 | # server you'll get a different port on Linux. This cannot be set
18 | # on Windows otherwise address will always be reused.
19 | # Ref: https://stackoverflow.com/a/19247688/3159288
20 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
21 | sock.bind((host, port))
22 | except OSError:
23 | pass
24 | else:
25 | return port
26 | msg = f"Host {host!r} has no available port in range {port_max}-{port_max}"
27 | raise RuntimeError(msg)
28 |
--------------------------------------------------------------------------------
/src/reactpy/web/__init__.py:
--------------------------------------------------------------------------------
1 | from reactpy.web.module import (
2 | export,
3 | module_from_file,
4 | module_from_string,
5 | module_from_url,
6 | )
7 |
8 | __all__ = [
9 | "export",
10 | "module_from_file",
11 | "module_from_string",
12 | "module_from_url",
13 | ]
14 |
--------------------------------------------------------------------------------
/src/reactpy/web/templates/react.js:
--------------------------------------------------------------------------------
1 | export * from "$CDN/$PACKAGE";
2 |
3 | import * as React from "$CDN/react$VERSION";
4 | import * as ReactDOM from "$CDN/react-dom$VERSION";
5 |
6 | export default ({ children, ...props }) => {
7 | const [{ component }, setComponent] = React.useState({});
8 | React.useEffect(() => {
9 | import("$CDN/$PACKAGE").then((module) => {
10 | // dynamically load the default export since we don't know if it's exported.
11 | setComponent({ component: module.default });
12 | });
13 | });
14 | return component
15 | ? React.createElement(component, props, ...(children || []))
16 | : null;
17 | };
18 |
19 | export function bind(node, config) {
20 | const root = ReactDOM.createRoot(node);
21 | return {
22 | create: (component, props, children) =>
23 | React.createElement(component, wrapEventHandlers(props), ...children),
24 | render: (element) => root.render(element),
25 | unmount: () => root.unmount()
26 | };
27 | }
28 |
29 | function wrapEventHandlers(props) {
30 | const newProps = Object.assign({}, props);
31 | for (const [key, value] of Object.entries(props)) {
32 | if (typeof value === "function" && value.isHandler) {
33 | newProps[key] = makeJsonSafeEventHandler(value);
34 | }
35 | }
36 | return newProps;
37 | }
38 |
39 | function makeJsonSafeEventHandler(oldHandler) {
40 | // Since we can't really know what the event handlers get passed we have to check if
41 | // they are JSON serializable or not. We can allow normal synthetic events to pass
42 | // through since the original handler already knows how to serialize those for us.
43 | return function safeEventHandler() {
44 | oldHandler(
45 | ...Array.from(arguments).filter((value) => {
46 | if (typeof value === "object" && value.nativeEvent) {
47 | // this is probably a standard React synthetic event
48 | return true;
49 | } else {
50 | try {
51 | JSON.stringify(value);
52 | } catch (err) {
53 | console.error("Failed to serialize some event data");
54 | return false;
55 | }
56 | return true;
57 | }
58 | }),
59 | );
60 | };
61 | }
62 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/tests/__init__.py
--------------------------------------------------------------------------------
/tests/sample.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from reactpy import html
4 | from reactpy.core.component import component
5 |
6 |
7 | @component
8 | def SampleApp():
9 | return html.div(
10 | {"id": "sample", "style": {"padding": "15px"}},
11 | html.h1("Sample Application"),
12 | html.p(
13 | "This is a basic application made with ReactPy. Click ",
14 | html.a(
15 | {"href": "https://pypi.org/project/reactpy/", "target": "_blank"},
16 | "here",
17 | ),
18 | " to learn more.",
19 | ),
20 | )
21 |
--------------------------------------------------------------------------------
/tests/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% component "reactpy.testing.backend.root_hotswap_component" %}
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/templates/jinja_bad_kwargs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% component "this.doesnt.matter", bad_kwarg='foo-bar' %}
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/templates/pyscript.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% pyscript_setup %}
6 |
7 |
8 |
9 | {% pyscript_component "tests/test_asgi/pyscript_components/root.py", initial='Loading...
' %}
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/test_asgi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/tests/test_asgi/__init__.py
--------------------------------------------------------------------------------
/tests/test_asgi/pyscript_components/load_first.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 |
3 | from reactpy import component
4 |
5 | if TYPE_CHECKING:
6 | from .load_second import child
7 |
8 |
9 | @component
10 | def root():
11 | return child()
12 |
--------------------------------------------------------------------------------
/tests/test_asgi/pyscript_components/load_second.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, hooks, html
2 |
3 |
4 | @component
5 | def child():
6 | count, set_count = hooks.use_state(0)
7 |
8 | def increment(event):
9 | set_count(count + 1)
10 |
11 | return html.div(
12 | html.button(
13 | {"onClick": increment, "id": "incr", "data-count": count}, "Increment"
14 | ),
15 | html.p(f"PyScript Count: {count}"),
16 | )
17 |
--------------------------------------------------------------------------------
/tests/test_asgi/pyscript_components/root.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, hooks, html
2 |
3 |
4 | @component
5 | def root():
6 | count, set_count = hooks.use_state(0)
7 |
8 | def increment(event):
9 | set_count(count + 1)
10 |
11 | return html.div(
12 | html.button(
13 | {"onClick": increment, "id": "incr", "data-count": count}, "Increment"
14 | ),
15 | html.p(f"PyScript Count: {count}"),
16 | )
17 |
--------------------------------------------------------------------------------
/tests/test_asgi/test_utils.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from reactpy import config
4 | from reactpy.executors import utils
5 |
6 |
7 | def test_invalid_vdom_head():
8 | with pytest.raises(ValueError, match="Invalid head element!*"):
9 | utils.vdom_head_to_html({"tagName": "invalid"})
10 |
11 |
12 | def test_process_settings():
13 | utils.process_settings({"async_rendering": False})
14 | assert config.REACTPY_ASYNC_RENDERING.current is False
15 | utils.process_settings({"async_rendering": True})
16 | assert config.REACTPY_ASYNC_RENDERING.current is True
17 |
18 |
19 | def test_invalid_setting():
20 | with pytest.raises(ValueError, match='Unknown ReactPy setting "foobar".'):
21 | utils.process_settings({"foobar": True})
22 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from reactpy import config
4 | from reactpy._option import Option
5 |
6 |
7 | @pytest.fixture(autouse=True)
8 | def reset_options():
9 | options = [value for value in config.__dict__.values() if isinstance(value, Option)]
10 |
11 | should_unset = object()
12 | original_values = []
13 | for opt in options:
14 | original_values.append(opt.current if opt.is_set() else should_unset)
15 |
16 | yield
17 |
18 | for opt, val in zip(options, original_values):
19 | if val is should_unset:
20 | if opt.is_set():
21 | opt.unset()
22 | else:
23 | opt.current = val
24 |
25 |
26 | def test_reactpy_debug_toggle():
27 | # just check that nothing breaks
28 | config.REACTPY_DEBUG.current = True
29 | config.REACTPY_DEBUG.current = False
30 |
31 |
32 | def test_boolean():
33 | assert config.boolean(True) is True
34 | assert config.boolean(False) is False
35 | assert config.boolean(1) is True
36 | assert config.boolean(0) is False
37 | assert config.boolean("true") is True
38 | assert config.boolean("false") is False
39 | assert config.boolean("True") is True
40 | assert config.boolean("False") is False
41 | assert config.boolean("TRUE") is True
42 | assert config.boolean("FALSE") is False
43 | assert config.boolean("1") is True
44 | assert config.boolean("0") is False
45 |
46 | with pytest.raises(ValueError):
47 | config.boolean("2")
48 |
49 | with pytest.raises(ValueError):
50 | config.boolean("")
51 |
52 | with pytest.raises(TypeError):
53 | config.boolean(None)
54 |
--------------------------------------------------------------------------------
/tests/test_console/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/tests/test_console/__init__.py
--------------------------------------------------------------------------------
/tests/test_core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/tests/test_core/__init__.py
--------------------------------------------------------------------------------
/tests/test_core/test_component.py:
--------------------------------------------------------------------------------
1 | import reactpy
2 | from reactpy.testing import DisplayFixture
3 |
4 |
5 | def test_component_repr():
6 | @reactpy.component
7 | def MyComponent(a, *b, **c):
8 | pass
9 |
10 | mc1 = MyComponent(1, 2, 3, x=4, y=5)
11 |
12 | expected = f"MyComponent({id(mc1):02x}, a=1, b=(2, 3), c={{'x': 4, 'y': 5}})"
13 | assert repr(mc1) == expected
14 |
15 | # not enough args supplied to function
16 | assert repr(MyComponent()) == "MyComponent(...)"
17 |
18 |
19 | async def test_simple_component():
20 | @reactpy.component
21 | def SimpleDiv():
22 | return reactpy.html.div()
23 |
24 | assert SimpleDiv().render() == {"tagName": "div"}
25 |
26 |
27 | async def test_simple_parameterized_component():
28 | @reactpy.component
29 | def SimpleParamComponent(tag):
30 | return reactpy.Vdom(tag)()
31 |
32 | assert SimpleParamComponent("div").render() == {"tagName": "div"}
33 |
34 |
35 | async def test_component_with_var_args():
36 | @reactpy.component
37 | def ComponentWithVarArgsAndKwargs(*args, **kwargs):
38 | return reactpy.html.div(kwargs, args)
39 |
40 | assert ComponentWithVarArgsAndKwargs("hello", "world", my_attr=1).render() == {
41 | "tagName": "div",
42 | "attributes": {"my_attr": 1},
43 | "children": ["hello", "world"],
44 | }
45 |
46 |
47 | async def test_display_simple_hello_world(display: DisplayFixture):
48 | @reactpy.component
49 | def Hello():
50 | return reactpy.html.p({"id": "hello"}, ["Hello World"])
51 |
52 | await display.show(Hello)
53 |
54 | await display.page.wait_for_selector("#hello")
55 |
56 |
57 | async def test_pre_tags_are_rendered_correctly(display: DisplayFixture):
58 | @reactpy.component
59 | def PreFormatted():
60 | return reactpy.html.pre(
61 | {"id": "pre-form-test"},
62 | reactpy.html.span("this", reactpy.html.span("is"), "some"),
63 | "pre-formatted",
64 | " text",
65 | )
66 |
67 | await display.show(PreFormatted)
68 |
69 | pre = await display.page.wait_for_selector("#pre-form-test")
70 |
71 | assert (
72 | await pre.evaluate("node => node.innerHTML")
73 | ) == "thisis some pre-formatted text"
74 |
--------------------------------------------------------------------------------
/tests/test_pyscript/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/tests/test_pyscript/__init__.py
--------------------------------------------------------------------------------
/tests/test_pyscript/pyscript_components/custom_root_name.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, hooks, html
2 |
3 |
4 | @component
5 | def custom():
6 | count, set_count = hooks.use_state(0)
7 |
8 | def increment(event):
9 | set_count(count + 1)
10 |
11 | return html.div(
12 | html.button(
13 | {"onClick": increment, "id": "incr", "data-count": count}, "Increment"
14 | ),
15 | html.p(f"PyScript Count: {count}"),
16 | )
17 |
--------------------------------------------------------------------------------
/tests/test_pyscript/pyscript_components/root.py:
--------------------------------------------------------------------------------
1 | from reactpy import component, hooks, html
2 |
3 |
4 | @component
5 | def root():
6 | count, set_count = hooks.use_state(0)
7 |
8 | def increment(event):
9 | set_count(count + 1)
10 |
11 | return html.div(
12 | html.button(
13 | {"onClick": increment, "id": "incr", "data-count": count}, "Increment"
14 | ),
15 | html.p(f"PyScript Count: {count}"),
16 | )
17 |
--------------------------------------------------------------------------------
/tests/test_pyscript/test_components.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 |
5 | import reactpy
6 | from reactpy import html, pyscript_component
7 | from reactpy.executors.asgi import ReactPy
8 | from reactpy.testing import BackendFixture, DisplayFixture
9 | from reactpy.testing.backend import root_hotswap_component
10 |
11 |
12 | @pytest.fixture()
13 | async def display(page):
14 | """Override for the display fixture that uses ReactPyMiddleware."""
15 | app = ReactPy(root_hotswap_component, pyscript_setup=True)
16 |
17 | async with BackendFixture(app) as server:
18 | async with DisplayFixture(backend=server, driver=page) as new_display:
19 | yield new_display
20 |
21 |
22 | async def test_pyscript_component(display: DisplayFixture):
23 | @reactpy.component
24 | def Counter():
25 | return pyscript_component(
26 | Path(__file__).parent / "pyscript_components" / "root.py",
27 | initial=html.div({"id": "loading"}, "Loading..."),
28 | )
29 |
30 | await display.show(Counter)
31 |
32 | await display.page.wait_for_selector("#loading")
33 | await display.page.wait_for_selector("#incr")
34 |
35 | await display.page.click("#incr")
36 | await display.page.wait_for_selector("#incr[data-count='1']")
37 |
38 | await display.page.click("#incr")
39 | await display.page.wait_for_selector("#incr[data-count='2']")
40 |
41 | await display.page.click("#incr")
42 | await display.page.wait_for_selector("#incr[data-count='3']")
43 |
44 |
45 | async def test_custom_root_name(display: DisplayFixture):
46 | @reactpy.component
47 | def CustomRootName():
48 | return pyscript_component(
49 | Path(__file__).parent / "pyscript_components" / "custom_root_name.py",
50 | initial=html.div({"id": "loading"}, "Loading..."),
51 | root="custom",
52 | )
53 |
54 | await display.show(CustomRootName)
55 |
56 | await display.page.wait_for_selector("#loading")
57 | await display.page.wait_for_selector("#incr")
58 |
59 | await display.page.click("#incr")
60 | await display.page.wait_for_selector("#incr[data-count='1']")
61 |
62 | await display.page.click("#incr")
63 | await display.page.wait_for_selector("#incr[data-count='2']")
64 |
65 | await display.page.click("#incr")
66 | await display.page.wait_for_selector("#incr[data-count='3']")
67 |
68 |
69 | def test_bad_file_path():
70 | with pytest.raises(ValueError):
71 | pyscript_component(initial=html.div({"id": "loading"}, "Loading...")).render()
72 |
--------------------------------------------------------------------------------
/tests/test_pyscript/test_utils.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from uuid import uuid4
3 |
4 | import orjson
5 | import pytest
6 |
7 | from reactpy.pyscript import utils
8 |
9 |
10 | def test_bad_root_name():
11 | file_path = str(
12 | Path(__file__).parent / "pyscript_components" / "custom_root_name.py"
13 | )
14 |
15 | with pytest.raises(ValueError):
16 | utils.pyscript_executor_html((file_path,), uuid4().hex, "bad")
17 |
18 |
19 | def test_extend_pyscript_config():
20 | extra_py = ["orjson", "tabulate"]
21 | extra_js = {"/static/foo.js": "bar"}
22 | config = {"packages_cache": "always"}
23 |
24 | result = utils.extend_pyscript_config(extra_py, extra_js, config)
25 | result = orjson.loads(result)
26 |
27 | # Check whether `packages` have been combined
28 | assert "orjson" in result["packages"]
29 | assert "tabulate" in result["packages"]
30 | assert any("reactpy" in package for package in result["packages"])
31 |
32 | # Check whether `js_modules` have been combined
33 | assert "/static/foo.js" in result["js_modules"]["main"]
34 | assert any("morphdom" in module for module in result["js_modules"]["main"])
35 |
36 | # Check whether `packages_cache` has been overridden
37 | assert result["packages_cache"] == "always"
38 |
39 |
40 | def test_extend_pyscript_config_string_values():
41 | extra_py = []
42 | extra_js = {"/static/foo.js": "bar"}
43 | config = {"packages_cache": "always"}
44 |
45 | # Try using string based `extra_js` and `config`
46 | extra_js_string = orjson.dumps(extra_js).decode()
47 | config_string = orjson.dumps(config).decode()
48 | result = utils.extend_pyscript_config(extra_py, extra_js_string, config_string)
49 | result = orjson.loads(result)
50 |
51 | # Make sure `packages` is unmangled
52 | assert any("reactpy" in package for package in result["packages"])
53 |
54 | # Check whether `js_modules` have been combined
55 | assert "/static/foo.js" in result["js_modules"]["main"]
56 | assert any("morphdom" in module for module in result["js_modules"]["main"])
57 |
58 | # Check whether `packages_cache` has been overridden
59 | assert result["packages_cache"] == "always"
60 |
--------------------------------------------------------------------------------
/tests/test_sample.py:
--------------------------------------------------------------------------------
1 | from reactpy.testing import DisplayFixture
2 | from tests.sample import SampleApp
3 |
4 |
5 | async def test_sample_app(display: DisplayFixture):
6 | await display.show(SampleApp)
7 | h1 = await display.page.wait_for_selector("h1")
8 | assert (await h1.text_content()) == "Sample Application"
9 |
--------------------------------------------------------------------------------
/tests/test_web/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/tests/test_web/__init__.py
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/callable-prop.js:
--------------------------------------------------------------------------------
1 | import { h, render } from "https://unpkg.com/preact?module";
2 | import htm from "https://unpkg.com/htm?module";
3 |
4 | const html = htm.bind(h);
5 |
6 | export function bind(node, config) {
7 | return {
8 | create: (type, props, children) => h(type, props, ...children),
9 | render: (element) => render(element, node),
10 | unmount: () => render(null, node),
11 | };
12 | }
13 |
14 | export function Component(props) {
15 | var text = "DEFAULT";
16 | if (props.setText && typeof props.setText === "function") {
17 | text = props.setText("PREFIX TEXT: ");
18 | }
19 | return html`
20 |
21 | ${text}
22 |
23 | `;
24 | }
25 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/component-can-have-child.js:
--------------------------------------------------------------------------------
1 | import { h, render } from "https://unpkg.com/preact?module";
2 | import htm from "https://unpkg.com/htm?module";
3 |
4 | const html = htm.bind(h);
5 |
6 | export function bind(node, config) {
7 | return {
8 | create: (type, props, children) => h(type, props, ...children),
9 | render: (element) => render(element, node),
10 | unmount: () => render(null, node),
11 | };
12 | }
13 |
14 | // The intention here is that Child components are passed in here so we check that the
15 | // children of "the-parent" are "child-1" through "child-N"
16 | export function Parent(props) {
17 | return html`
18 |
21 |
22 | `;
23 | }
24 |
25 | export function Child({ index }) {
26 | return html`child ${index} `;
27 | }
28 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/export-resolution/index.js:
--------------------------------------------------------------------------------
1 | export { index as Index };
2 | export * from "./one.js";
3 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/export-resolution/one.js:
--------------------------------------------------------------------------------
1 | export { one as One };
2 | // use ../ just to check that it works
3 | export * from "../export-resolution/two.js";
4 | // this default should not be exported by the * re-export in index.js
5 | export default 0;
6 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/export-resolution/two.js:
--------------------------------------------------------------------------------
1 | export { two as Two };
2 | export * from "https://some.external.url";
3 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/exports-syntax.js:
--------------------------------------------------------------------------------
1 | // Copied from: https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export
2 |
3 | // Exporting individual features
4 | export let name1, name2, name3; // also var, const
5 | export let name4 = 4, name5 = 5, name6; // also var, const
6 | export function functionName(){...}
7 | export class ClassName {...}
8 |
9 | // Export list
10 | export { name7, name8, name9 };
11 |
12 | // Renaming exports
13 | export { variable1 as name10, variable2 as name11, name12 };
14 |
15 | // Exporting destructured assignments with renaming
16 | export const { name13, name14: bar } = o;
17 |
18 | // Aggregating modules
19 | export * from "https://source1.com"; // does not set the default export
20 | export * from "https://source2.com"; // does not set the default export
21 | export * as name15 from "https://source3.com"; // Draft ECMAScript® 2O21
22 | export { name16, name17 } from "https://source4.com";
23 | export { import1 as name18, import2 as name19, name20 } from "https://source5.com";
24 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/exports-two-components.js:
--------------------------------------------------------------------------------
1 | import { h, render } from "https://unpkg.com/preact?module";
2 | import htm from "https://unpkg.com/htm?module";
3 |
4 | const html = htm.bind(h);
5 |
6 | export function bind(node, config) {
7 | return {
8 | create: (type, props, children) => h(type, props, ...children),
9 | render: (element) => render(element, node),
10 | unmount: () => render(null, node),
11 | };
12 | }
13 |
14 | export function Header1(props) {
15 | return h("h1", { id: props.id }, props.text);
16 | }
17 |
18 | export function Header2(props) {
19 | return h("h2", { id: props.id }, props.text);
20 | }
21 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/keys-properly-propagated.js:
--------------------------------------------------------------------------------
1 | import React from "https://esm.sh/react@19.0"
2 | import ReactDOM from "https://esm.sh/react-dom@19.0/client"
3 | import GridLayout from "https://esm.sh/react-grid-layout@1.5.0";
4 | export {GridLayout};
5 |
6 | export function bind(node, config) {
7 | const root = ReactDOM.createRoot(node);
8 | return {
9 | create: (type, props, children) =>
10 | React.createElement(type, props, children),
11 | render: (element) => root.render(element, node),
12 | unmount: () => root.unmount()
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/set-flag-when-unmount-is-called.js:
--------------------------------------------------------------------------------
1 | export function bind(node, config) {
2 | return {
3 | create: (type, props, children) => type(props),
4 | render: (element) => renderElement(element, node),
5 | unmount: () => unmountElement(node),
6 | };
7 | }
8 |
9 | export function renderElement(element, container) {
10 | if (container.firstChild) {
11 | container.removeChild(container.firstChild);
12 | }
13 | container.appendChild(element);
14 | }
15 |
16 | export function unmountElement(container) {
17 | // We add an element to the document.body to indicate that this function was called.
18 | // Thus allowing Selenium to see communicate to server-side code that this effect
19 | // did indeed occur.
20 | const unmountFlag = document.createElement("h1");
21 | unmountFlag.setAttribute("id", "unmount-flag");
22 | document.body.appendChild(unmountFlag);
23 | container.innerHTML = "";
24 | }
25 |
26 | export function SomeComponent(props) {
27 | const element = document.createElement("h1");
28 | element.appendChild(document.createTextNode(props.text));
29 | element.setAttribute("id", props.id);
30 | return element;
31 | }
32 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/simple-button.js:
--------------------------------------------------------------------------------
1 | import { h, render } from "https://unpkg.com/preact?module";
2 | import htm from "https://unpkg.com/htm?module";
3 |
4 | const html = htm.bind(h);
5 |
6 | export function bind(node, config) {
7 | return {
8 | create: (type, props, children) => h(type, props, ...children),
9 | render: (element) => render(element, node),
10 | unmount: () => render(null, node),
11 | };
12 | }
13 |
14 | export function SimpleButton(props) {
15 | return h(
16 | "button",
17 | {
18 | id: props.id,
19 | onClick(event) {
20 | props.onClick({ data: props.eventResponseData });
21 | },
22 | },
23 | "simple button",
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/tests/test_web/js_fixtures/subcomponent-notation.js:
--------------------------------------------------------------------------------
1 | import React from "https://esm.sh/react@19.0"
2 | import ReactDOM from "https://esm.sh/react-dom@19.0/client"
3 | import {InputGroup, Form} from "https://esm.sh/react-bootstrap@2.10.2?deps=react@19.0,react-dom@19.0,react-is@19.0&exports=InputGroup,Form";
4 | export {InputGroup, Form};
5 |
6 | export function bind(node, config) {
7 | const root = ReactDOM.createRoot(node);
8 | return {
9 | create: (type, props, children) =>
10 | React.createElement(type, props, ...children),
11 | render: (element) => root.render(element),
12 | unmount: () => root.unmount()
13 | };
14 | }
--------------------------------------------------------------------------------
/tests/tooling/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactive-python/reactpy/6930fc2848946243a59710415b80fcff18a24565/tests/tooling/__init__.py
--------------------------------------------------------------------------------
/tests/tooling/aio.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from asyncio import Event as _Event
4 | from asyncio import wait_for
5 |
6 | from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT
7 |
8 |
9 | class Event(_Event):
10 | """An event with a ``wait_for`` method."""
11 |
12 | async def wait(self, timeout: float | None = None):
13 | return await wait_for(
14 | super().wait(),
15 | timeout=timeout or REACTPY_TESTS_DEFAULT_TIMEOUT.current,
16 | )
17 |
--------------------------------------------------------------------------------
/tests/tooling/common.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from reactpy.testing.common import GITHUB_ACTIONS
4 | from reactpy.types import LayoutEventMessage, LayoutUpdateMessage
5 |
6 | DEFAULT_TYPE_DELAY = 250 if GITHUB_ACTIONS else 50
7 |
8 |
9 | def event_message(target: str, *data: Any) -> LayoutEventMessage:
10 | return {"type": "layout-event", "target": target, "data": data}
11 |
12 |
13 | def update_message(path: str, model: Any) -> LayoutUpdateMessage:
14 | return {"type": "layout-update", "path": path, "model": model}
15 |
--------------------------------------------------------------------------------
/tests/tooling/hooks.py:
--------------------------------------------------------------------------------
1 | from reactpy.core.hooks import HOOK_STACK, use_state
2 |
3 |
4 | def use_force_render():
5 | return HOOK_STACK.current_hook().schedule_render
6 |
7 |
8 | def use_toggle(init=False):
9 | state, set_state = use_state(init)
10 | return state, lambda: set_state(lambda old: not old)
11 |
12 |
13 | # TODO: Remove this
14 | def use_counter(initial_value):
15 | state, set_state = use_state(initial_value)
16 | return state, lambda: set_state(lambda old: old + 1)
17 |
--------------------------------------------------------------------------------
/tests/tooling/layout.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | from collections.abc import AsyncIterator
5 | from contextlib import asynccontextmanager
6 | from typing import Any
7 |
8 | from jsonpointer import set_pointer
9 |
10 | from reactpy.core.layout import Layout
11 | from reactpy.types import VdomJson
12 | from tests.tooling.common import event_message
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | @asynccontextmanager
18 | async def layout_runner(layout: Layout) -> AsyncIterator[LayoutRunner]:
19 | async with layout:
20 | yield LayoutRunner(layout)
21 |
22 |
23 | class LayoutRunner:
24 | def __init__(self, layout: Layout) -> None:
25 | self.layout = layout
26 | self.model = {}
27 |
28 | async def render(self) -> VdomJson:
29 | update = await self.layout.render()
30 | logger.info(f"Rendering element at {update['path'] or '/'!r}")
31 | if not update["path"]:
32 | self.model = update["model"]
33 | else:
34 | self.model = set_pointer(
35 | self.model, update["path"], update["model"], inplace=False
36 | )
37 | return self.model
38 |
39 | async def trigger(self, element: VdomJson, event_name: str, *data: Any) -> None:
40 | event_handler = element.get("eventHandlers", {}).get(event_name, {})
41 | logger.info(f"Triggering {event_name!r} with target {event_handler['target']}")
42 | if not event_handler:
43 | raise ValueError(f"Element has no event handler for {event_name}")
44 | await self.layout.deliver(event_message(event_handler["target"], *data))
45 |
--------------------------------------------------------------------------------
/tests/tooling/select.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Iterator, Sequence
4 | from dataclasses import dataclass
5 | from typing import Callable
6 |
7 | from reactpy.types import VdomJson
8 |
9 | Selector = Callable[[VdomJson, "ElementInfo"], bool]
10 |
11 |
12 | def id_equals(id: str) -> Selector:
13 | return lambda element, _: element.get("attributes", {}).get("id") == id
14 |
15 |
16 | def class_equals(class_name: str) -> Selector:
17 | return (
18 | lambda element, _: class_name
19 | in element.get("attributes", {}).get("class", "").split()
20 | )
21 |
22 |
23 | def text_equals(text: str) -> Selector:
24 | return lambda element, _: _element_text(element) == text
25 |
26 |
27 | def _element_text(element: VdomJson) -> str:
28 | if isinstance(element, str):
29 | return element
30 | return "".join(_element_text(child) for child in element.get("children", []))
31 |
32 |
33 | def element_exists(element: VdomJson, selector: Selector) -> bool:
34 | return next(find_elements(element, selector), None) is not None
35 |
36 |
37 | def find_element(
38 | element: VdomJson,
39 | selector: Selector,
40 | *,
41 | first: bool = False,
42 | ) -> tuple[VdomJson, ElementInfo]:
43 | """Find an element by a selector.
44 |
45 | Parameters:
46 | element:
47 | The tree to search.
48 | selector:
49 | A function that returns True if the element matches.
50 | first:
51 | If True, return the first element found. If False, raise an error if
52 | multiple elements are found.
53 |
54 | Returns:
55 | Element info, or None if not found.
56 | """
57 | find_iter = find_elements(element, selector)
58 | found = next(find_iter, None)
59 | if found is None:
60 | raise ValueError("Element not found")
61 | if not first:
62 | try:
63 | next(find_iter)
64 | raise ValueError("Multiple elements found")
65 | except StopIteration:
66 | pass
67 | return found
68 |
69 |
70 | def find_elements(
71 | element: VdomJson, selector: Selector
72 | ) -> Iterator[tuple[VdomJson, ElementInfo]]:
73 | """Find an element by a selector.
74 |
75 | Parameters:
76 | element:
77 | The tree to search.
78 | selector:
79 | A function that returns True if the element matches.
80 |
81 | Returns:
82 | Element info, or None if not found.
83 | """
84 | return _find_elements(element, selector, (), ())
85 |
86 |
87 | def _find_elements(
88 | element: VdomJson,
89 | selector: Selector,
90 | parents: Sequence[VdomJson],
91 | path: Sequence[int],
92 | ) -> tuple[VdomJson, ElementInfo] | None:
93 | info = ElementInfo(parents, path)
94 | if selector(element, info):
95 | yield element, info
96 |
97 | for index, child in enumerate(element.get("children", [])):
98 | if isinstance(child, dict):
99 | yield from _find_elements(
100 | child, selector, (*parents, element), (*path, index)
101 | )
102 |
103 |
104 | @dataclass
105 | class ElementInfo:
106 | parents: Sequence[VdomJson]
107 | path: Sequence[int]
108 |
--------------------------------------------------------------------------------